Quickstart
This walks through the complete flow: create a key, find a product, price a run, generate a
creative, wait for it, and download the result. Every request below is runnable as shown — replace
$API_KEY, $BRAND_ID and the ids pulled from earlier responses as you go.
All paths are relative to the base URL:
https://api.mocart.io/api/public/v1
1. Create an API key
In the Mocart dashboard, go to Settings → API keys and create a key with both read and
runs:write scopes (the latter implies runs:read). Copy it once — it's shown only at creation
time. See Authentication for scopes and key management. (A key that should
also approve or reject campaign creatives needs creatives:review as well — see
Reviewing creatives.)
export API_KEY="mcf_74b5dd1e6a29150ab5ab6081d0b30ac043aadf7d2c948e80d330775d0b6382e8"
export BRAND_ID="brand_9c1e2f7a4b3d"
2. Find a product — GET /products (listProducts)
curl -s "https://api.mocart.io/api/public/v1/products?brandId=$BRAND_ID&limit=20" \
-H "Authorization: Bearer $API_KEY"
{
"data": [
{
"productId": "prod_8f2a1c9b4d6e",
"sku": "CPD-100",
"title": "Ceramic Pour-Over Coffee Dripper",
"brandId": "brand_9c1e2f7a4b3d",
"productUrl": "https://example-store.com/products/ceramic-pour-over-dripper",
"image": "https://storage.googleapis.com/.../prod_8f2a1c9b4d6e/primary.jpg",
"createdAt": "2026-06-01T12:00:00.000Z"
}
],
"pagination": { "nextCursor": null, "hasMore": false, "pageSize": 20 }
}
Note there's no object field here — that tag is a Generation-plane convention (see the run and
preset examples below). Catalog, Campaigns & Creatives, and Deliveries objects and their list
envelopes don't carry one.
brandId scopes the search to one brand. See Pagination for how to
page through more results than fit in one call.
3. Optional: browse styles — GET /presets (listPresets)
A run needs either a preset id, your own prompt, or both. If you want a ready-made style:
curl -s https://api.mocart.io/api/public/v1/presets \
-H "Authorization: Bearer $API_KEY"
{
"object": "list",
"data": [
{ "object": "preset", "presetId": "preset_studio_softbox", "controlIds": ["lighting", "background"] },
{ "object": "preset", "presetId": "preset_lifestyle_outdoor", "controlIds": ["lighting", "setting", "props"] }
],
"pagination": { "nextCursor": null, "hasMore": false, "pageSize": 20 }
}
controlIds lists the keys you may set in selections for that preset. You can skip this
entirely and send your own prompt instead.
4. Price the run — POST /runs/estimate (estimateRun)
This is a dry run: it validates and prices the exact body you're about to send to POST /runs,
and writes nothing. See Try it safely for why this is the only "test mode" the
API has.
curl -s -X POST https://api.mocart.io/api/public/v1/runs/estimate \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brandId": "'"$BRAND_ID"'",
"presetId": "preset_studio_softbox",
"productIds": ["prod_8f2a1c9b4d6e"],
"modality": "image",
"aspectRatio": "1:1",
"creativesPerProduct": 2
}'
{
"object": "run_estimate",
"estimate": { "perItemCredits": 4, "totalCredits": 8, "rateCardVersion": "2026-08-01" }
}
Note totalCredits: 8 — you'll use it twice in the next step: as expectedTotalCredits, so a
rate-card change between now and create is caught instead of silently charged, and as the basis
for maxCredits.
5. Create the run — POST /runs (createRun)
Every create needs an Idempotency-Key — a value you can reproduce for this exact logical
request (a UUID you generate once is a good default). Without it, a retried request risks creating
a second run and a second charge. See Idempotency before you build a retry loop.
This example also caps spend with maxCredits, a sensible default for a first call against a new
key:
curl -s -X POST https://api.mocart.io/api/public/v1/runs \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 2ab6c9e1-4f7d-4b3a-9c2e-8d1f4c8e6b3d" \
-d '{
"brandId": "'"$BRAND_ID"'",
"presetId": "preset_studio_softbox",
"productIds": ["prod_8f2a1c9b4d6e"],
"modality": "image",
"aspectRatio": "1:1",
"creativesPerProduct": 2,
"selections": { "lighting": "SOFT", "background": "WHITE" },
"expectedTotalCredits": 8,
"maxCredits": 8
}'
202 Accepted:
{
"runId": "run_a26e94e8f3ae95313f4d46c5",
"object": "run",
"status": "QUEUED",
"total": 2,
"credits": { "estimated": 8, "rateCardVersion": "2026-08-01" },
"counts": { "succeeded": 0, "failed": 0, "refused": 0, "stopped": 0, "pending": 2 },
"createdAt": "2026-09-28T12:00:00.000Z",
"finalizedAt": null,
"estimatedCompletion": {
"completesAt": "2026-09-28T12:00:20.000Z",
"durationSeconds": 20,
"concurrency": 2,
"perItemSeconds": 20
}
}
With maxCredits: 8 equal to the quoted totalCredits, this run is allowed to run to completion
exactly as quoted — a quote that came in even one credit higher would have been refused before
anything was charged. Raise maxCredits (or omit it) once you're comfortable with real spend.
6. Wait for it — poll GET /runs/{id} (getRun)
A run is asynchronous: POST /runs returns before any creative exists. Poll until status is
terminal.
curl -s https://api.mocart.io/api/public/v1/runs/run_a26e94e8f3ae95313f4d46c5 \
-H "Authorization: Bearer $API_KEY"
Keep polling every few seconds (back off on 429, see Rate limits) while
status is QUEUED, RUNNING, or STOPPING. Stop once it's one of the six terminal values —
SUCCEEDED, PARTIAL, FAILED, REFUSED, STOPPED, EXPIRED. The full lifecycle, what each
status means, and how to size your poll interval are in Runs. If you have a webhook
endpoint configured, you can skip polling entirely — see Webhooks.
7. Collect the results — GET /runs/{id}/items (listRunItems)
curl -s "https://api.mocart.io/api/public/v1/runs/run_a26e94e8f3ae95313f4d46c5/items?limit=50" \
-H "Authorization: Bearer $API_KEY"
{
"object": "list",
"data": [
{
"jobId": "job_5c1e8b3a2f7d",
"object": "run_item",
"productId": "prod_8f2a1c9b4d6e",
"itemIndex": 0,
"outcome": "succeeded",
"errorCode": null,
"assetId": "job_5c1e8b3a2f7d",
"assetUrl": "https://storage.googleapis.com/users-assets-a/.../job_5c1e8b3a2f7d.png"
},
{
"jobId": "job_7a3f9c1e5b8d",
"object": "run_item",
"productId": "prod_8f2a1c9b4d6e",
"itemIndex": 1,
"outcome": "succeeded",
"errorCode": null,
"assetId": "job_7a3f9c1e5b8d",
"assetUrl": "https://storage.googleapis.com/users-assets-a/.../job_7a3f9c1e5b8d.png"
}
],
"pagination": { "nextCursor": null, "hasMore": false, "pageSize": 50 }
}
Each assetUrl is a permanent public link — download it (or store the link) and you're done. See
Runs for what a non-succeeded outcome means and what to do about it.
The whole flow in Node
const BASE_URL = 'https://api.mocart.io/api/public/v1';
const API_KEY = process.env.MOCART_API_KEY;
const BRAND_ID = process.env.MOCART_BRAND_ID;
async function mocart(path, options = {}) {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (!res.ok) {
const problem = await res.json();
throw new Error(`${problem.code}: ${problem.detail} (requestId: ${problem.requestId})`);
}
return res.json();
}
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const TERMINAL_STATUSES = new Set(['SUCCEEDED', 'PARTIAL', 'FAILED', 'REFUSED', 'STOPPED', 'EXPIRED']);
async function main() {
// 1. Find a product.
const products = await mocart(`/products?brandId=${BRAND_ID}&limit=1`);
const product = products.data[0];
// 2. Price the run.
const body = {
brandId: BRAND_ID,
presetId: 'preset_studio_softbox',
productIds: [product.productId],
modality: 'image',
aspectRatio: '1:1',
creativesPerProduct: 2,
selections: { lighting: 'SOFT', background: 'WHITE' },
};
const { estimate } = await mocart('/runs/estimate', { method: 'POST', body: JSON.stringify(body) });
// 3. Create the run, capped at the quoted price.
const run = await mocart('/runs', {
method: 'POST',
headers: { 'Idempotency-Key': crypto.randomUUID() },
body: JSON.stringify({
...body,
expectedTotalCredits: estimate.totalCredits,
maxCredits: estimate.totalCredits,
}),
});
// 4. Poll until terminal.
let current = run;
while (!TERMINAL_STATUSES.has(current.status)) {
await sleep(3000);
current = await mocart(`/runs/${current.runId}`);
}
// 5. Collect the results.
const items = await mocart(`/runs/${current.runId}/items`);
for (const item of items.data) {
if (item.outcome === 'succeeded') {
console.log(item.assetUrl);
} else {
console.log(`item ${item.itemIndex} did not produce a creative: ${item.errorCode}`);
}
}
}
main().catch(console.error);
The SDK version
The same flow using @mocart-io/api, the
generated TypeScript client — request/response types included, no hand-written fetch wrapper.
This is docs-site/snippets/quickstart.ts in the repo, kept honest by CI: npm run sdk:check
type-checks it against the SDK on every change to either.
npm install @mocart-io/api
import { randomUUID } from 'node:crypto';
import { client, listProducts, estimateRun, createRun, getRun, type Run } from '@mocart-io/api';
client.setConfig({
baseUrl: 'https://api.mocart.io/api/public/v1',
auth: () => process.env.MOCART_API_KEY,
});
const TERMINAL_STATUSES = new Set<Run['status']>(['SUCCEEDED', 'PARTIAL', 'FAILED', 'REFUSED', 'STOPPED', 'EXPIRED']);
async function quickstart(brandId: string): Promise<Run> {
// 1. Find a product.
const { data: products } = await listProducts({ query: { brandId } });
const product = products!.data[0];
// 2. Price the run — a dry run; nothing is written or charged.
const body = {
brandId,
presetId: 'preset_studio_softbox',
productIds: [product.productId],
modality: 'image' as const,
aspectRatio: '1:1' as const,
creativesPerProduct: 2,
selections: { lighting: 'SOFT', background: 'WHITE' },
};
const { data: estimate } = await estimateRun({ body });
// 3. Create the run, capped at the quoted price, keyed for a safe retry.
const { data: run } = await createRun({
body: {
...body,
expectedTotalCredits: estimate!.estimate.totalCredits,
maxCredits: estimate!.estimate.totalCredits,
},
headers: { 'Idempotency-Key': randomUUID() },
});
// 4. Poll until terminal.
let current = run!;
while (!TERMINAL_STATUSES.has(current.status)) {
await new Promise((resolve) => setTimeout(resolve, 3000));
current = (await getRun({ path: { runId: current.runId } })).data!;
}
return current;
}
The full version — with error handling on every call instead of ! — is
docs-site/snippets/quickstart.ts; see Webhooks for the SDK's signature
verification helper instead of polling.
Next: review campaign creatives
Creatives generated under a campaign wait in PENDING until someone reviews them. Besides the
dashboard, you can approve or reject them through the API with
POST /creatives/{creativeId}/approve (approveCreative) and
POST /creatives/{creativeId}/reject (rejectCreative) — see
Reviewing creatives.