Craftkitdocs

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, not message. Messages are human-readable and may change for clarity. Codes are part of the API contract.
  • Surface fieldErrors to your form UI. Each key in fieldErrors is 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 /render after a 502 won't enqueue twice.