Skip to main content

Managing campaigns

A campaign groups the creatives made for a set of your products. You can manage campaigns through the API:

  • POST /campaigns — create a campaign for some of your products.
  • PATCH /campaigns/{id} — rename it and attach your own metadata.
  • DELETE /campaigns/{id} — delete the campaign and its creatives. Nothing is destroyed.
  • POST /campaigns/{campaignId}/undelete — restore it.
  • ?showDeleted=true on GET /campaigns and GET /campaigns/{id} — read deleted campaigns too.

None of these spends credits. Every write returns the campaign as GET /campaigns/{id} returns it, with an ETag header.

1. Mint a key​

In the Mocart dashboard, go to Settings → API keys and create a key with the campaigns:write permission. Like every write key it must expire, within 365 days. campaigns:write implies read, so the same key can read back what it changed. See Authentication.

Your plan must include campaigns. Without it, every campaign write answers 403 TIER_REQUIRED.

export API_KEY="mcf_74b5dd1e6a29150ab5ab6081d0b30ac043aadf7d2c948e80d330775d0b6382e8"

2. Create a campaign — POST /campaigns​

Name the campaign, choose a brand, say what it produces, and list the products it covers:

curl -s -X POST https://api.mocart.io/api/public/v1/campaigns \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7c2e9f40-1a5d-4b86-93e7-d0f4a6c81b25" \
-d '{
"name": "Autumn launch",
"brandId": "brand_9c1e2f7a4b3d",
"assetType": "STATIC",
"format": "1:1",
"productIds": ["prod_8f2a1c9b4d6e", "prod_1a4c7e9b2d5f"],
"metadata": { "externalId": "cmp-2026-autumn" }
}'
FieldRequiredWhat it is
nameyes1–200 characters after leading and trailing spaces are trimmed.
brandIdyesOne of your brands.
assetTypeyesWhat the campaign produces: STATIC (images), VIDEO, 3D or PLAYABLE.
formatyesThe output aspect ratio: 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9 or 21:9. A VIDEO campaign accepts only 16:9 and 9:16.
productIdsyesAt least one product of that brand. Unlike the dashboard, an empty list does not mean "every product" — name the products you want.
metadatanoYour own string labels, up to 50 keys. See Metadata.

Any other field is a 422. Idempotency-Key is required.

201 Created, with an ETag header, a Location header pointing at the new campaign, and the campaign:

{
"object": "campaign",
"campaignId": "cmp_4d2a9e7c1b3f",
"name": "Autumn launch",
"status": "IN_PROGRESS",
"brandId": "brand_9c1e2f7a4b3d",
"creativeCount": 2,
"createdAt": "2026-10-01T08:12:40.000Z",
"metadata": { "externalId": "cmp-2026-autumn" }
}

What a new campaign contains​

The campaign opens one empty creative per product. These creatives have the status NOT_GENERATED: they hold a place for the product and have no image or video yet. Creating a campaign spends no credits and starts no generation.

Because an empty creative has no media, it isn't returned by GET /creatives or GET /creatives/{id} (see Managing creatives), and it doesn't trigger a creative webhook. It appears there once it has been generated.

Retrying a create​

Send the same Idempotency-Key with the same body and you get the same campaign back — never a second one. A replay returns 201 with the same Location. The campaign's id is derived from the key, so a replay always names the same campaign — even after you delete it: the replay then still answers 201, with the campaign as it is now, including deletedAt. If you reuse a key for a different campaign, that's 409 IDEMPOTENCY_KEY_REUSED. See Idempotency.

Plan limits: 403 LIMIT_EXCEEDED​

Your plan caps how many campaigns an account can have, and how many products one campaign can cover. Go over either and the create is refused with 403, and nothing is created:

{
"type": "/errors/LIMIT_EXCEEDED",
"title": "Forbidden",
"status": 403,
"code": "LIMIT_EXCEEDED",
"detail": "Your plan allows 10 campaigns and this account already has 10.",
"instance": "/api/public/v1/campaigns",
"requestId": "a1b2c3d4-e5f6-4a3b-8c9d-0e1f2a3b4c5d",
"limitKey": "maxCampaigns",
"limit": 10,
"current": 10
}

limitKey is maxCampaigns or maxProductsPerCampaign; limit is what your plan allows and current is where you stand. It is a 403, not a 429: a plan limit isn't a rate, so waiting won't help. Delete a campaign you no longer need, name fewer products, or move to a plan with a higher limit. Deleted campaigns don't count against the limit until you restore them.

3. Rename a campaign — PATCH /campaigns/{id}​

Send name, metadata, or both. Nothing else is writable, and any other field is a 422.

curl -s -X PATCH https://api.mocart.io/api/public/v1/campaigns/cmp_4d2a9e7c1b3f \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Autumn launch — EU", "metadata": { "region": "eu" } }'

200 OK, with a fresh ETag and the campaign. A request that changes nothing — the same name, an empty metadata, no body — is a 200 that records nothing. Idempotency-Key is optional.

Metadata​

metadata is a map of your own string labels — an id from your system, a region, a batch name. Mocart stores it and returns it; it has no other meaning. On POST you send the whole map. On PATCH what you send is merged into the stored map, key by key:

You sendResult
"key": "value"The key is added, or its value is replaced.
"key": nullThe key is removed.
A key you leave outUntouched.
"metadata": {}Nothing changes.

There is no "replace everything": to clear the map, send null for each key. The limits: at most 50 keys in the stored map, key names of 1–40 characters (letters, digits, _ and -, and not starting with __), and values of up to 500 characters. One PATCH may name up to 100 keys. A merge that would leave more than 50 keys is a 422, and nothing is changed. null is never a value on POST.

metadata appears in the response only when the campaign has at least one key.

4. Delete a campaign — DELETE /campaigns/{id}​

curl -s -X DELETE https://api.mocart.io/api/public/v1/campaigns/cmp_4d2a9e7c1b3f \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: e4a81c93-5b72-4d06-8f2a-39c7d0b1e654"

200 OK, with the campaign and deletedAt set:

{
"object": "campaign",
"campaignId": "cmp_4d2a9e7c1b3f",
"name": "Autumn launch — EU",
"status": "IN_PROGRESS",
"brandId": "brand_9c1e2f7a4b3d",
"creativeCount": 2,
"createdAt": "2026-10-01T08:12:40.000Z",
"metadata": { "externalId": "cmp-2026-autumn", "region": "eu" },
"deletedAt": "2026-10-01T09:45:03.000Z"
}

Deleting a campaign deletes every creative in it too. Nothing is destroyed: you can restore the campaign, and the creatives that went with it, with no time limit. Deleting spends and refunds no credits, and never cancels a run.

The campaign is deleted at once, and its creatives follow. Each creative records its own deletion, so each one also sends a creative.deleted webhook.

Refusals: busy and still finishing​

A delete can be refused with a 409, in three ways. In each, nothing changed:

CAMPAIGN_NOT_DELETABLE — the campaign is busy. It holds creatives that are QUEUED or GENERATING. The problem body says how many in generatingCount:

{
"type": "/errors/CAMPAIGN_NOT_DELETABLE",
"title": "Conflict",
"status": 409,
"code": "CAMPAIGN_NOT_DELETABLE",
"detail": "Cannot delete a campaign with 3 creative(s) generating",
"instance": "/api/public/v1/campaigns/cmp_4d2a9e7c1b3f",
"requestId": "a1b2c3d4-e5f6-4a3b-8c9d-0e1f2a3b4c5d",
"generatingCount": 3
}

Wait for generation to finish, then retry. The same code, with "reason": "still_being_created" in place of generatingCount, means the campaign's own creation is still being written. Try again in a few seconds.

CAMPAIGN_CASCADE_PENDING — the previous change is still finishing. A delete or restore commits the campaign immediately and finishes its creatives right after. If you change the campaign again while that is still going on, you get this 409 with a Retry-After header. Wait that long and retry the same request; it succeeds shortly.

IDEMPOTENCY_IN_PROGRESS / IDEMPOTENCY_KEY_REUSED — the usual key conflicts. See Idempotency.

5. Restore a campaign — POST /campaigns/{campaignId}/undelete​

curl -s -X POST https://api.mocart.io/api/public/v1/campaigns/cmp_4d2a9e7c1b3f/undelete \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2f9d6b18-7c04-4e53-a1b8-5d3e0c7f9a42" \
-d '{}'

200 OK, with the campaign as GET /campaigns/{id} returns it — no deletedAt. As on every POST, send a body ({}): one with none is refused by the load balancer with 411.

  • It restores the creatives that were deleted with the campaign. A creative you deleted on its own before stays deleted — restore it with undelete on the creative.
  • The campaign's review links stay revoked.
  • Restoring a campaign that is already live is a 200 that changes nothing.
  • The plan limit applies again. A restored campaign counts against your plan's campaign limit like a new one, so if you are at the limit, the restore is refused with 403 LIMIT_EXCEEDED — the same body as on create.
  • If the previous delete is still finishing, you get 409 CAMPAIGN_CASCADE_PENDING. Wait Retry-After and retry.
  • If the restore loses a race with other changes on your account, you get 503 SERVICE_UNAVAILABLE with Retry-After. Nothing was restored; resend the same request with the same key.

Read deleted campaigns — ?showDeleted=true​

curl -s "https://api.mocart.io/api/public/v1/campaigns?showDeleted=true" \
-H "Authorization: Bearer $API_KEY"

With showDeleted=true, every campaign in the response carries deletedAt: an ISO 8601 time for a deleted campaign, null for a live one. Without it, deleted campaigns are hidden and the response has no deletedAt. Any value other than true is treated as if you had left it out.

Guard a write with If-Match​

GET /campaigns/{id} returns a strong ETag. Send it back as If-Match on PATCH or DELETE, and the write only goes through if the campaign is still exactly as you read it. Otherwise: 412 PRECONDITION_FAILED, and nothing is changed.

The ETag covers the whole campaign as you read it — including creativeCount and status, not just the fields you can edit. Those change by themselves: a creative finishing generation changes the campaign's status and counts. So a 412 doesn't always mean someone edited your campaign. When you get one, read the campaign again and retry with the new ETag — and if your change is still right, it will now go through.

curl -s -X PATCH https://api.mocart.io/api/public/v1/campaigns/cmp_4d2a9e7c1b3f \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "b2e47d90c1a6f3858d0e2a9c4b7f1e63a5d8c0b9f2e4a7d1c6b3e8f0a9d2c5b7"' \
-d '{ "name": "Autumn launch — EU" }'
  • If-Match is optional. Leave it out to write regardless of what changed.
  • If-Match: * matches any campaign that exists.
  • Send the ETag exactly as you received it, quotes included. A value starting with W/ never matches.
  • GET /campaigns/{id}?showDeleted=true returns a weak ETag you can't use for If-Match; read the campaign without the parameter.
  • undelete doesn't take If-Match.

Retries and Idempotency-Key​

OperationIdempotency-KeySame key, same request
POST /campaignsRequired201, the campaign the key already created
PATCH /campaigns/{id}Optional200, the campaign; nothing recorded again
DELETE /campaigns/{id}Optional200, the original delete answer
POST .../undeleteRequired200, the campaign; nothing recorded again

Without a key, a DELETE you retry after a lost response finds the campaign already deleted and gets 404; send a key, and the retry returns the original answer instead. Rules for the key itself (16–255 characters) are in Idempotency.

Handling each error​

Every error is application/problem+json except 429. Branch on code.

StatuscodeWhat happenedWhat to do
400IDEMPOTENCY_KEY_REQUIREDPOST /campaigns or undelete without an Idempotency-Key.Add one.
400INVALID_IDEMPOTENCY_KEYThe key is under 16 or over 255 characters, or the header was sent twice.Send exactly one header, 16–255 characters.
401—The key is missing, unknown, revoked or expired, or the account no longer has API access.Mint a new key. See Authentication.
403API_TOKEN_SCOPE_DENIEDThe key doesn't carry campaigns:write.Mint a key with campaigns:write.
403TIER_REQUIREDYour plan doesn't include campaigns.Upgrade the plan.
403LIMIT_EXCEEDEDA create, or a restore, would put you over the plan's campaign limit, or a campaign over its products limit. limitKey, limit and current say which.Delete a campaign, name fewer products, or upgrade. Don't retry unchanged.
404CAMPAIGN_NOT_FOUNDNo campaign with this id that you can change: it doesn't exist, belongs to another account, or is deleted (except for undelete, and for a DELETE retried under its Idempotency-Key).Check the id against GET /campaigns.
409CAMPAIGN_NOT_DELETABLEThe campaign has QUEUED or GENERATING creatives (generatingCount), or is still being created (reason).Wait, then retry.
409CAMPAIGN_CASCADE_PENDINGThe campaign's previous delete or restore is still finishing.Wait Retry-After, then retry the same request.
409IDEMPOTENCY_KEY_REUSEDThis key was already used for a different request.Use a new key.
409IDEMPOTENCY_IN_PROGRESSAn earlier attempt under this key hasn't finished.Wait Retry-After, then resend the identical request with the same key.
412PRECONDITION_FAILEDIf-Match no longer matches the campaign. Nothing was changed.Read the campaign again and retry with the new ETag.
422VALIDATION_ERRORThe body is invalid: an unknown field, an empty productIds, a VIDEO format other than 16:9 or 9:16, a bad name or metadata, or a brandId or product id that isn't yours. errors[] names the field.Fix the body.
429— (plain-text body)Too many writes in the current window.Wait Retry-After, then resend. See Rate limits.
503WRITES_PAUSEDWrites are paused for your account. Nothing changed and the key wasn't used.Wait Retry-After, then resend with the same key.
503SERVICE_UNAVAILABLEA create or a restore lost a race with other changes on your account. Nothing was changed.Wait Retry-After, then resend the same request with the same key.

A brandId or product id that doesn't exist and one that belongs to another account get the same 422: you can't probe for other accounts' ids.

Rate limits​

Campaign creates, edits, deletes and restores share one budget of 60 requests per minute per key, separate from reads and from creative and asset writes. Read the RateLimit headers rather than hard-coding it — see Rate limits.

Webhooks​

Campaign changes — from the API or the dashboard — can notify your own endpoint: campaign.created, campaign.updated, campaign.deleted and campaign.restored. Deleting or restoring a campaign also sends creative.deleted or creative.restored for each creative that changed. See Webhooks.