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
{
"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/app/integrations"
}
}#Every error code
| Status | Code | Meaning | Charged? |
|---|---|---|---|
| 400 | invalid_json | The body is not valid JSON. | No |
| 400 | invalid_body | The body is valid JSON but not an object. | No |
| 400 | missing_id | No card code in the path. | No |
| 401 | missing_key | No bearer token. | No |
| 401 | malformed_key | The token is not a Scribble key. | No |
| 401 | invalid_key | Unknown key. | No |
| 401 | revoked_key | The key was revoked. | No |
| 401 | invalid_hook | The inbound hook URL is not recognised. | No |
| 403 | missing_scope | The key lacks the scope this endpoint needs. | No |
| 403 | hook_disabled | The inbound hook was switched off. | No |
| 404 | card_not_found | No card with that code belongs to this organisation. | No |
| 404 | design_not_found | No design with that id belongs to this organisation. | No |
| 402 | no_credits | The balance is empty, or ran out between the check and the spend. Nothing was created. | No |
| 415 | unsupported_media_type | Send Content-Type: application/json. | No |
| 422 | invalid_recipient | A required address field is missing or malformed. | No |
| 422 | missing_message | message was empty. | No |
| 422 | message_too_long | message exceeds 500 characters. | No |
| 422 | invalid_date | arrive_on is not YYYY-MM-DD. | No |
| 422 | missing_name | A contact needs a name. | No |
| 422 | invalid_birthday | birthday is not MM-DD or YYYY-MM-DD. | No |
| 422 | template_incomplete | An inbound hook's message template needs a field the payload has no value for. | No |
| 429 | rate_limited | See rate limits. | No |
| 500 | read_failed | A read failed on our side. | No |
| 500 | create_failed | A contact could not be created. | No |
| 500 | update_failed | A contact could not be updated. | No |
| 500 | order_failed | The card could not be created. Nothing was charged. | No |
| 500 | production_failed | The card was accepted but could not be prepared for production. Nothing was charged. | No |
| 500 | internal_error | Something went wrong on our side. | No |
#Retrying safely
| Status | Safe to retry? | Why |
|---|---|---|
| 4xx | Yes, after fixing the request | Nothing was created. |
| 429 | Yes, after Retry-After seconds | The request never reached the handler. |
500 with order_failed, production_failed, no_credits | Yes | These explicitly state nothing was charged. |
500 with internal_error | Check first | The failure happened after the handler started. Call GET /api/v1/cards and look for the recipient before retrying. |
| A timeout or a dropped connection | Check first | You do not know whether the card was created. Look before you retry. |
async function sendCard(event, body) {
// The guard lives in YOUR system, because Scribble has no
// idempotency key. Write the claim before the call.
if (await alreadySent(event.id)) return;
await claim(event.id);
const res = await fetch("https://scribblecards.com/api/v1/cards", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SCRIBBLE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after") ?? 60);
await releaseClaim(event.id);
throw new RetryAfter(wait);
}
if (!res.ok) {
const { error } = await res.json();
await releaseClaim(event.id); // nothing was charged on a 4xx
throw new Error(`${error.code}: ${error.message}`);
}
const card = await res.json();
await recordSent(event.id, card.id); // e.g. "SC-NH4DERB"
return card;
}Last checked against the product on . Something wrong or missing? Tell us.