Skip to main content
Browse documentation

Type to search the documentation. Press Esc to close.

What do Scribble API errors look like?

For developers

Every error is the same shape: { "error": { "code", "message", "docs" } }. The code is stable and safe to switch on; the message is written for a person reading a terminal and may change. A 4xx means nothing was created and nothing was charged.

On this page
The error envelope
{
  "error": {
    "code": "invalid_recipient",
    "message": "recipient.line1, recipient.state, recipient.zip are required. A card cannot be written for an address we cannot address an envelope to.",
    "docs": "https://scribblecards.com/docs/api",
    "request_id": "req_9e2de6b00c73aff81e462b18"
  }
}

#Headers on every response

Response headers worth reading
HeaderWhat it tells you
x-request-idThis request, in our logs. Quote it when asking us anything.
x-ratelimit-limitRequests allowed per minute for this key.
x-ratelimit-remainingHow many are left in the current minute. Pace yourself on this rather than waiting for a 429.
x-ratelimit-resetUnix seconds at which the window rolls over.
idempotent-replayPresent and true when the response is a replay of an earlier identical request rather than a new card.
retry-afterOn a 429 or a 409 request_in_progress: seconds to wait.

#Every error code

Scribble API error codes by status
StatusCodeMeaningCharged?
400invalid_jsonThe body is not valid JSON.No
400invalid_bodyThe body is valid JSON but not an object.No
400missing_idNo card code in the path.No
400invalid_cursorstarting_after does not match anything in this organization.No
400invalid_idempotency_keyThe key is longer than 255 characters, or the replay cache could not be reached — in which case the request is refused rather than risking a duplicate card.No
401missing_keyNo bearer token.No
401malformed_keyThe token is not a Scribble key.No
401invalid_keyUnknown key.No
401revoked_keyThe key was revoked.No
401invalid_hookThe inbound hook URL is not recognized.No
403missing_scopeThe key lacks the scope this endpoint needs.No
403hook_disabledThe inbound hook was switched off.No
404card_not_foundNo card with that code belongs to this organization.No
404design_not_foundNo design with that id belongs to this organization — or, for a card: id, no card in Scribble's public collection has that slug.No
402no_creditsThe balance is empty, or ran out between the check and the spend. Nothing was created.No
409request_in_progressAnother request with this Idempotency-Key is still running. Wait Retry-After seconds and retry with the same key.No
409too_late_to_cancelThe card has already reached a machine. The response says where it actually is.Yes — it was charged when it was sent
415unsupported_media_typeSend Content-Type: application/json.No
422invalid_recipientA required address field is missing or malformed.No
422missing_messagemessage was empty.No
422message_too_longmessage exceeds 500 characters.No
422broken_merge_fieldmessage still contains something field-shaped — {{first_name}}, {{First Name}}, [[company]]. This endpoint takes finished text: substitute your merge fields before sending. Nothing was written.No
422invalid_datearrive_on is not YYYY-MM-DD.No
422unknown_handwritingThe handwriting slug is not one we offer. Call GET /api/v1/handwriting.No
422idempotency_key_reusedThis key was already used for a request with a different body. Nothing was sent.No
422missing_nameA contact needs a name.No
422invalid_birthdaybirthday is not MM-DD or YYYY-MM-DD.No
422template_incompleteAn inbound hook's message template needs a field the payload has no value for.No
429rate_limitedSee rate limits.No
500read_failedA read failed on our side.No
500create_failedA contact could not be created.No
500update_failedA contact could not be updated.No
500order_failedThe card could not be created. Nothing was charged.No
500production_failedThe card was accepted but could not be prepared for production. Nothing was charged.No
500internal_errorSomething went wrong on our side.No

#Retrying safely

Keys are remembered for 24 hours, scoped to your organization. Within that window the same key always returns the same answer, and the replay carries an Idempotent-Replay: true header so you can tell a fresh send from a repeat.

How the API answers a repeated key
SituationResponseWhat happened
Same key, same body, first request finishedThe original response, with Idempotent-Replay: trueNothing new was created. No credit was spent.
Same key, same body, first request still running409 request_in_progress with Retry-AfterWait the suggested seconds and retry with the same key.
Same key, different body422 idempotency_key_reusedAlmost always a loop that forgot to advance the key. Nothing was sent.
New keyA new cardThis is a different send, by definition.
When it is safe to retry
StatusSafe to retry?Why
4xxYes, after fixing the requestNothing was created.
429Yes, after Retry-After secondsThe request never reached the handler.
402 no_creditsYes, after topping upNothing was created, and the key is released so the same one works.
500 of any codeYesThe order is rolled back before the error is returned. Nothing was charged and nothing will be sent.
A timeout or a dropped connectionYes, with the same keyThis is exactly what the key is for. Without one, look before you retry.
A retry that cannot double-send
async function sendCard(event, body) {
  const res = await fetch("https://scribblecards.com/api/v1/cards", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.SCRIBBLE_KEY}`,
      "Content-Type": "application/json",
      // The whole guard. Same event, same key, forever — so a retry
      // after a timeout returns the first card instead of making
      // a second one. No bookkeeping needed on your side.
      "Idempotency-Key": event.id,
    },
    body: JSON.stringify(body),
  });

  if (res.status === 409) {
    // The first attempt is still in flight. Wait and use the SAME key.
    const wait = Number(res.headers.get("retry-after") ?? 5);
    throw new RetryAfter(wait);
  }

  if (res.status === 429) {
    throw new RetryAfter(Number(res.headers.get("retry-after") ?? 60));
  }

  if (!res.ok) {
    const { error } = await res.json();
    // Quote request_id if you ever need to ask us what happened.
    throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
  }

  return res.json();  // { id: "SC-NH4DERB", ... }
}

Last checked against the product on . Something wrong or missing? Tell us.