> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnicommerce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless campaign automation

> Import, validate, approve, run and maintain campaigns through the SDKs without a browser session.

Use TypeScript SDK 0.20.0 or Python SDK 0.15.0 with an authorized organization API key. Computer executions use their injected current-user credentials. Both SDKs call the same campaign and promotion domain services as the application; browser cookies and internal application routes are unnecessary.

## Choose the executing approver

`campaigns.getOptions()` / `campaigns.get_options()` returns `actorId`, eligible campaign actors in `members`, and exact connected stores. An organization API key acts as `oauth:<clientId>`. Explicitly name that eligible actor in a new campaign's `brief.ownerId` and `brief.approverId` for headless approval. A key cannot approve on behalf of a named human. Current grants and named-approver checks remain enforced. Do not change an existing approver without authorization.

## Import Excel

Upload the workbook with `documents.upload` and retain `upload.data.document.id`. Call `campaigns.importBriefs({documentIds, preview: true})` or Python `campaigns.import_briefs(document_ids=document_ids, preview=True)`. Inspect every warning and issue before repeating with `preview=false` to save. Owner/approver default to the executing actor. Supply exact connected stores when the workbook needs disambiguation.

Uploading requires `documents:write`; importing requires `documents:read` and `promotions:write`. Re-upload one document with `campaignId` to update a campaign. Existing running campaigns receive a pending proposal, preserving their current execution.

## Approve and run now with Full sync

These examples assume an authenticated `client` and the authorized saved `campaignIds` / `campaign_ids`. Validation returns a proposed shifted plan; saving it creates the revision that the named approver starts. The date shift preserves window/offer durations and relative gaps. Marketplace lead times still apply. Full sync prepares all eligible offers at campaign start; later windows retain their later sale dates.

### TypeScript / JavaScript

```js theme={null}
const review = await client.campaigns.validate({
  campaignIds,
  shiftToNow: true,
  syncMode: "campaign_start",
});
if (!review.success) throw new Error(JSON.stringify(review.results));
const targets = [];
for (const result of review.results) {
  if (
    !result.campaignId ||
    !result.plan ||
    !result.revision ||
    !["draft", "approved"].includes(result.stage)
  ) {
    throw new Error("Expected a reviewed saved draft or approved campaign");
  }
  const saved = await client.campaigns.replace({
    campaignId: result.campaignId,
    expectedRevision: result.revision,
    expectedStage: result.stage,
    plan: result.plan,
  });
  targets.push({
    campaignId: saved.campaign.id,
    expectedRevision: saved.campaign.revision,
  });
}
const started = await client.campaigns.actBatch({
  action: "approve_and_run",
  campaigns: targets,
});
console.log(JSON.stringify(started));
```

### Python

```python theme={null}
import json
review = client.campaigns.validate(body={
    "campaignIds": campaign_ids, "shiftToNow": True, "syncMode": "campaign_start"
})
if not review["success"]:
    raise RuntimeError(json.dumps(review["results"]))
targets = []
for result in review["results"]:
    if not result.get("campaignId") or result.get("stage") not in ("draft", "approved"):
        raise RuntimeError("Expected a reviewed saved draft or approved campaign")
    saved = client.campaigns.replace(
        campaign_id=result["campaignId"], expected_revision=result["revision"],
        expected_stage=result["stage"], plan=result["plan"]
    )
    targets.append({"campaignId": saved["campaign"]["id"],
                    "expectedRevision": saved["campaign"]["revision"]})
started = client.campaigns.act_batch(action="approve_and_run", campaigns=targets)
print(json.dumps(started))
```

Python `AsyncOmni` exposes the same methods with `await`. Union-payload validators take a `body` dictionary with camelCase wire keys; other keyword arguments use snake\_case.

## Campaign controls

| Operation | JavaScript | Python |
| - | - | - |
| Create/read/replace/list | `campaigns.create/get/replace/list` | Same names |
| Validate saved IDs or proposed plans | `campaigns.validate` | `campaigns.validate(body={...})` |
| Lifecycle batch | `campaigns.actBatch` | `campaigns.act_batch` |
| Offer filters/pagination | `campaigns.listOffers` | `campaigns.list_offers` |
| Resolve draft promotion selection | `campaigns.resolvePromotionOptions` | `campaigns.resolve_promotion_options` |
| Analytics | `campaigns.getAnalytics` | `campaigns.get_analytics` |
| Personal notifications | `campaigns.getNotifications/setNotifications` | `get_notifications/set_notifications` |
| Archive/restore/delete selection | `campaigns.archive/delete` | Same names |

Lifecycle batches accept one action and 1–20 `{campaignId, expectedRevision, expectedPendingVersion?}` targets: `approve`, `run`, `approve_and_run`, `end`, `resume`, `retry`, `approve_change`, `discard_change`, `archive`, `restore`, or `delete`. Live-change decisions require the reviewed pending version. Duplicates run once, conflicting versions are rejected, and per-target failures remain explicit.

Read every returned campaign ID with `campaigns.get`. Check executions, progress, failed/pending schedules and cleanup. A successful start means durable work was queued; it does not prove marketplace success. Ending starts cleanup. Resume is available only for eligible untouched future windows after cleanup; completed windows require a new campaign. Personal notification preferences require a member credential; API clients monitor campaign status and configured event workflows.

## Promotion controls

Use `promotions.getCapabilities()` / `get_capabilities()` for every canonical marketplace and promotion kind, voucher subtypes and concrete unsupported limitations. Validate saved IDs or proposed definitions/products with `promotions.validate` (Python: `body={...}`). This catches canonical marketplace terms and targeting issues before publishing; provider-time rejections can still occur.

Create drafts with `promotions.create` and `status: "draft"`, assign qualifying products with `addProducts` / `add_products`, and publish with `setStatuses({promotionIds, targetStatus: "active"})` / `set_statuses(promotion_ids=..., target_status="active")`. `createBatch` / `create_batch` creates promotions with products using the flat write contract; explicitly set `promotion.status="draft"` when preparing drafts, because the canonical batch creation flow otherwise activates them.

Use `update` for changes, `preview` for effective prices, `getProductOptions` / `get_product_options` for eligibility, and `listProducts/removeProducts` (Python snake\_case) for assignments. Pause/archive through `setStatuses`; delete through `deleteBatch` / `delete_batch`. Status and deletion batches accept up to 500 IDs.

Read `getDelivery({promotionIds})` / `get_delivery(promotion_ids=...)` for up to 50 IDs and inspect every exact store's `deliveryStatuses`, errors and unavailable/unsupported states. `retry` returns durable job/correlation IDs: await all returned IDs and re-read delivery. Retain saved IDs after partial failures and retry those IDs instead of creating duplicates.

See the generated [API reference](https://docs.omnicommerce.ai/api-reference.md) for canonical input bounds and schemas.

## Apply an updated campaign workbook immediately

Change the discount value in its promotion row or remove qualifying SKU rows from Prices / Promotion Items. Products is a reference sheet, so deleting a reference row alone does not unassign a product. Removing the final qualifying row stops that offer. An omitted sheet preserves its existing membership; an unresolved SKU blocks the update.

```ts theme={null}
const preview = await client.campaigns.importBriefs({
  campaignId,
  documentIds: [updatedDocumentId],
  preview: true,
});
const review = preview.results[0];
if (!preview.success || review.issueCount || review.revision == null) {
  throw new Error(JSON.stringify(preview));
}
const applied = await client.campaigns.importBriefs({
  campaignId,
  documentIds: [updatedDocumentId],
  preview: false,
  applyChanges: true,
  expectedRevision: review.revision,
  expectedPendingVersion: review.pendingChangeVersion ?? null,
});
```

Python uses `apply_changes=True`, `expected_revision=review["revision"]`, and `expected_pending_version=review["pendingChangeVersion"]` in `campaigns.import_briefs`. A null pending version is explicit `None`.

The named approver must have current publish permission. The server validates marketplace edit rules, saves the proposal, approves that exact version and queues durable sync immediately. Default uploads remain proposals. Closed windows and marketplace fields that cannot change live return validation errors before applying. A saved proposal remains identifiable if approval fails.

`applied:true` means the approved revision committed. Inspect `scheduling` and campaign execution/delivery results to confirm every marketplace change. The API does not claim delivery success merely because work was queued.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.