Skip to main content

Envelope

Every error response is JSON with an error object:
{
  "error": {
    "code": "invalid_input_data",
    "message": "Variable data did not match the template manifest.",
    "issues": { "fieldErrors": { "customer.name": ["Required"] } }
  }
}
  • code — stable, machine-readable. Branch on this.
  • message — human-readable; do not parse.
  • issues — present on validation errors; a Zod flatten() with fieldErrors/formErrors.

Codes

StatuscodeMeaningRetry?
400invalid_jsonBody is not valid JSON.No
400invalid_requestBody failed schema validation.No
400invalid_input_datadata did not match the manifest. Inspect issues.fieldErrors.No
401unauthorizedMissing, invalid, revoked, or wrong-environment key.No
403forbiddenKey’s project was not found.No
404template_not_foundNo such template in this key’s project.No
404version_not_foundPinned versionNumber does not exist.No
404not_foundRender (or other resource) not found in this project.No
409no_published_versionTemplate has no published version.After publishing
409not_readyShare requested before the render succeeded.After completion
503queue_unavailableRender queue temporarily unreachable.Yes — backoff
500internalUnexpected server error.Yes — backoff

Retry semantics

  • Never retry 4xx except where noted — the request will fail again unchanged. Fix the input.
  • Retry 503 / 5xx with exponential backoff (e.g. 1s → 2s → 4s → 8s → 16s, cap ~30s, ~5 attempts).
  • For render submission, pair retries with an Idempotency-Key so a retried submit never produces a duplicate document.
async function craftkit(url: string, init: RequestInit) {
  const res = await fetch(url, init);
  if (res.ok) return res.json();
  const { error } = await res.json();
  switch (error.code) {
    case 'template_not_found':
      throw new Error('Wrong slug or wrong project key');
    case 'no_published_version':
      throw new Error('Publish a template version first');
    case 'invalid_input_data':
      throw new Error(`Data mismatch: ${JSON.stringify(error.issues?.fieldErrors)}`);
    case 'queue_unavailable':
    case 'internal':
      return backoffRetry(url, init); // your backoff wrapper
    default:
      throw new Error(`${error.code}: ${error.message}`);
  }
}