Try it safely
There is no test mode
Some APIs offer test keys that return simulated data — a sk_test_ key that returns fake charges,
for example. The Mocart API doesn't do this: there are no test-mode keys and no "live mode" flag.
Every key generates real creatives and spends real credits.
This is deliberate, not an oversight. A simulated run would have to return fake creatives, and Mocart's product rules treat test data in production collections as something to avoid entirely — there's no isolated sandbox to write fake rows into. A "do not actually charge" branch at the same code path that charges real accounts is also a standing risk of its own: it's one more thing that can go wrong at the exact place money moves.
The estimate is the dry run
POST /runs/estimate (estimateRun) is the safe way to try a request before it costs anything:
- It takes the same body you'd send to
POST /runs(createRun). - It validates that body with the same rules
createRunwould apply — a shape error, a missing field, a product with no usable image, all surface here exactly as they would on create. - It writes nothing: no run is created, no credits are held, no
Idempotency-Keyis consumed. Call it as many times as you like.
A 200 from estimateRun means: this exact body would be accepted by createRun, at this price.
That's the whole guarantee, and it's a strong one — every shape problem in your request surfaces
on the call that costs nothing, before you ever spend a key's one shot at an idempotency claim.
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_9c1e2f7a4b3d",
"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" }
}
If your account has a daily spend budget, the response also carries it:
{
"object": "run_estimate",
"estimate": { "perItemCredits": 4, "totalCredits": 8, "rateCardVersion": "2026-08-01" },
"budget": {
"capCredits": 2250,
"spentCredits": 2246,
"remainingCredits": 4,
"resetsAt": "2026-09-24T00:00:00.000Z"
}
}
Check budget.remainingCredits against estimate.totalCredits before you commit — here
remainingCredits (4) is less than totalCredits (8), so POST /runs would refuse this exact
run with 409 ACCOUNT_SPEND_CAP_EXCEEDED. budget is absent (never null, never -1) when the
account has no daily cap.
If the run doesn't fit the budget
POST /runs refuses a run that doesn't fit what's left of the account's daily budget, before
anything is charged and before your Idempotency-Key is consumed:
code | status | Meaning |
|---|---|---|
ACCOUNT_SPEND_CAP_EXCEEDED | 409 | The quote doesn't fit what's left of the budget. detail names the budget, what's used, what's left, the quote, and the reset instant — the same numbers as the typed budget object above. |
ACCOUNT_SPEND_UNAVAILABLE | 503 | The budget couldn't be checked at that moment — not a statement that it's already spent. Carries Retry-After; safe to retry with the same key. |
Both refusals leave the Idempotency-Key unclaimed — reuse it once you've waited for
budget.resetsAt or reduced the request to fit. The account owner can raise the daily cap, up to
the plan's default, with PUT /credits/api-spend-cap from a signed-in dashboard session — an API
key can't call it.
maxCredits: a hard ceiling on what one run may spend
Pass maxCredits on POST /runs to cap what that run is allowed to cost, independent of any
account-level budget:
maxCredits(optional, on create) — a hard ceiling on what this run may spend, checked twice, not only at create:- At create. If the quoted price exceeds
maxCredits, the create is refused with409 PRICE_EXCEEDS_MAXbefore anything is charged and before yourIdempotency-Keyis consumed. The run is never silently shrunk to fit — you get exactly what you asked for, or a refusal. - Per item, as the run executes. Each item is checked against
maxCreditsas spend accumulates. An item that would push the run past the ceiling is neither charged nor produced — it gets a row withSPEND_CEILING_REACHEDinstead.
- At create. If the quoted price exceeds
A first call against a new key is a good place to set maxCredits equal to the quoted
totalCredits — the run either completes exactly as quoted, or doesn't start:
{
"brandId": "brand_9c1e2f7a4b3d",
"presetId": "preset_studio_softbox",
"productIds": ["prod_8f2a1c9b4d6e"],
"modality": "image",
"aspectRatio": "1:1",
"creativesPerProduct": 2,
"expectedTotalCredits": 8,
"maxCredits": 8
}
expectedTotalCredits is a companion assertion: pass the totalCredits you got from estimateRun
here, and if the rate card moved between your estimate and your create — even to a cheaper price
— the create is refused with 409 PRICE_CHANGED rather than silently charging a different amount
than you agreed to.
Both checks happen before any charge and before your key is claimed, so a refusal here costs
nothing and the same Idempotency-Key is safe to reuse once you've adjusted the value.
Putting it together for a first real run
- Build your request body.
- Send it to
estimateRun. Confirm the price and any budget headroom. - Send the same body to
createRun, addingIdempotency-Key,expectedTotalCredits(from step 2), andmaxCreditsset to that same number. - The run either completes at exactly the price you saw, or refuses before spending anything.
See the Quickstart for this as a complete, runnable sequence, and Runs for what happens after create.