Rate limits and errors
How fast you may call the API, and what every error looks like.
Rate limits
- Each key may make 10 requests per second sustained, with a burst of 60.
- Each organisation may trigger up to 60 flow runs per minute and begin up to 120 uploads per minute (every file in a batch counts). These caps apply across all of the organisation's keys.
Over a limit the API answers 429 rate_limited with a Retry-After header (in seconds). Wait that long and retry. Polling a run once every few seconds, or uploading files one after another, stays comfortably inside the limits.
The error shape
Every error has the same shape:
{
"error": {
"code": "validation_failed",
"message": "The request is invalid.",
"details": [{ "field": "limit", "message": "Number must be greater than or equal to 1" }],
"request_id": "0b2d1c4e-..."
}
}
details is present when there is something specific to say: a list of { field, message } entries for a validation failure, or an object such as { "reason": "render_locked" } for a conflict.
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_key | The key is missing, unknown, expired or revoked. |
| 403 | plan_required | The organisation is not on the Business+ plan, or its billing needs attention. |
| 403 | scope_required | The key lacks the route's scope (details.required_scope). |
| 404 | not_found | No such resource in this organisation. |
| 409 | conflict | The request is valid but the resource's state does not allow it yet; details.reason says which: storage_full (no storage left, see below), render_locked or locked (a creative mid-render), upload_incomplete / not_uploading (an upload's bytes are not there yet, or there is nothing to complete), duplicate (a name or id already taken), idempotency_conflict (an Idempotency-Key reused with a different body), no_trigger / flow_unavailable / actor_unavailable (a flow that cannot be triggered). |
| 422 | validation_failed | The request itself is wrong; details lists each problem. |
| 413 | payload_too_large | The request body is over 1 MB. File bytes go to the upload URL, never through the API. |
| 429 | rate_limited | Over a limit; Retry-After says how long to wait. |
| 500 | internal_error | Something failed on Nubu's side. Retry, and quote the request id if it persists. |
A path that does not exist under /api/v1 answers 404 not_found in the same shape. Every error carries only these fields; nothing about Nubu's internals rides an error.
When storage is full
The API never bypasses the organisation's storage allowance: it stops exactly where the app would, and says so.
- Beginning an upload or a replacement (assets and templates, single or batch) whose file would not fit the remaining storage answers
409 conflictwithdetails.reasonstorage_full; the message says how much is free. In a batch, that file's entry carries the error and the others proceed. - Approving or re-rendering a creative while storage is full answers
409 conflictwithdetails.reasonstorage_full: new renders are paused. - Triggering a flow with
auto_queue: truestill builds (202), but the creatives wait in review: the answer and the run carry"queue_fallback": "storage"and anoticesaying why. Approve them once space is free. - If an organisation goes over its allowance while renders are queued (output sizes are only known when they land), the queued creatives are failed with the storage message and the run reports them as
render_failed; re-render them once space is free.
Free space by deleting assets, templates or renders, through the API or the app: the accounting updates at once and the gates lift with it.
Retrying
- Retry
429afterRetry-After, and500with a short backoff. - Never retry a
4xxother than429without changing the request. - Completing an upload twice, or triggering the same flow twice, creates nothing twice: a completed upload answers the same asset again, and each trigger is its own run. Use
external_idon a trigger to recognise your own runs.