Errors
Error envelope, codes, and retries.
Every Craftkit error response uses one envelope. Switch on error.code (stable, machine-readable) in your code; surface error.message to developers; inspect error.issues for validation problems.
Quick Start
The envelope:
{
"error": {
"code": "invalid_input_data",
"message": "Variable data did not match the template manifest.",
"issues": {
"formErrors": [],
"fieldErrors": {
"customer": ["Required"]
}
}
}
}Handling it:
Node.js
const res = await fetch(url, options);
if (!res.ok) {
const { error } = await res.json();
if (error.code === 'invalid_input_data') {
console.error('Field issues:', error.issues.fieldErrors);
} else if (error.code === 'rate_limited') {
await backoff();
}
throw new Error(`${error.code}: ${error.message}`);
}Python
res = requests.post(url, json=body, headers=headers)
if not res.ok:
error = res.json()["error"]
if error["code"] == "invalid_input_data":
print("Field issues:", error["issues"]["fieldErrors"])
elif error["code"] == "rate_limited":
backoff()
raise RuntimeError(f"{error['code']}: {error['message']}")Envelope shape
| Field | Type | Description |
|---|---|---|
code |
string | Stable, machine-readable identifier. Switch on this. |
message |
string | Human-readable. Safe for developers; safe-ish for end users. |
issues |
object | undefined | Present for validation errors only. Mirrors Zod's flatten() output: { formErrors: string[], fieldErrors: Record<string, string[]> }. |
Common codes
| Code | HTTP | When | Action |
|---|---|---|---|
missing_authorization |
401 | No bearer header | Send the API key in Authorization: Bearer ... |
invalid_credentials |
401 | Key revoked, unknown, or (on /v1/embed/*) embed not enabled for the project |
Mint a new key in the target environment; for embed endpoints, also enable embed mode in the dashboard |
partner_suspended |
403 | Embed partnership suspended | Contact support |
template_not_found |
404 | Slug doesn't exist in this project | Check the slug + the API key's project scope |
version_not_found |
404 | Pinned version doesn't exist | Omit options.versionNumber or pick a real one |
no_published_version |
409 | Template has no published version | Publish a version in the dashboard |
invalid_json |
400 | Body wasn't JSON | Set Content-Type: application/json and stringify |
invalid_request |
400 | Top-level shape wrong | See the API reference for the surface you called |
invalid_input_data |
400 | Data didn't match manifest | Inspect issues.fieldErrors |
invalid_signature |
401 | HMAC mismatch on inbound webhook | Recompute signature from the raw body |
rate_limited |
429 | Too many requests | Back off with exponential jitter |
internal_error |
500 | Something is wrong on our side | Retry with backoff; check status page |
Retry semantics
| HTTP | Retry? | Notes |
|---|---|---|
| 4xx (except 429) | No | Fix the request first |
| 429 | Yes | Exponential backoff with jitter |
| 5xx | Yes | POST /render is safe to retry — the worker dedupes by jobId |
| Network error | Yes | Same backoff strategy |
Recommended backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s, max 5 attempts.
async function withRetry(fn, max = 5) {
for (let i = 0; i < max; i++) {
try {
const res = await fn();
if (res.status < 500 && res.status !== 429) return res;
} catch (err) {
if (i === max - 1) throw err;
}
const delay = Math.min(1000 * 2 ** i, 30000) + Math.random() * 250;
await new Promise((r) => setTimeout(r, delay));
}
}Tips
- Always switch on
code, notmessage. Messages are human-readable and may change for clarity. Codes are part of the API contract. - Surface
fieldErrorsto your form UI. Each key infieldErrorsis a manifest path; map them straight onto the corresponding input. - Don't retry 4xx (except 429). They mean the request itself is wrong. Retrying just burns quota.
- Idempotency for 5xx retries. The render worker dedupes by job hash, so retrying a
POST /renderafter a 502 won't enqueue twice.
Related
- Authentication —
missing_authorizationandinvalid_credentials - POST /v1/templates/:slug/render —
invalid_input_dataand friends - Inbound webhook —
invalid_signature