Scribble cards API: send and look up a handwritten card
For developers
POST /api/v1/cards sends one handwritten card and spends one credit. GET /api/v1/cards lists what you have sent, and GET /api/v1/cards/{card_code} returns one card with its per-step production progress and its arrival window.
On this page
#POST/api/v1/cards
Send one handwritten card. Spends one credit.
Requires scope cards:send
Body parameters
| Field | Type | Description |
|---|---|---|
recipientrequired | object | The postal address. See the sub-fields below. |
recipient.namerequired | string | Who the envelope is addressed to. |
recipient.line1required | string | Street line. |
recipient.line2 | string | Apartment, suite, floor or unit. |
recipient.cityrequired | string | City. |
recipient.staterequired | string | Two-letter US state code. Upper-cased for you. |
recipient.ziprequired | string | Five digits, optionally with a +4. |
recipient.country | string | Defaults to US. Scribble mails inside the US only. |
messagerequired | string | What gets written on the card, in ink. Send the finished text with merge fields already substituted — a message still containing something field-shaped, like {{first_name}} or [[company]], fails with 422 broken_merge_field and nothing is sent and nothing is charged. It used to be accepted and written on the card exactly as it appeared, behind a 201 that said it had worked. Maximum 500 characters; line breaks are honored. Aim for 250–400 — 500 is the ceiling, not a target, and a long name can push a message that fits in testing over the edge in production. |
handwriting | string | Which hand to write in, by name — theodore, ruth, miriam. Case does not matter. Call GET /v1/handwriting for the full list. An unrecognized name is refused, not substituted: you get 422 unknown_handwriting and nothing is sent. A typo should cost you an error, not a thousand cards in the wrong hand. Leaving the field out is not an error — that means your house hand. |
design_id | string | Which card front this goes on. Two kinds of id work. One of your saved designs, from GET /api/v1/designs — its artwork and inside layout are both used. Or a card from Scribble's public collection, written card: and the slug from the card's page address: card:brushstroke-thanks for scribblecards.com/cards/business/brushstroke-thanks. A range card is copied into your own designs the first time you send it, so it shows up in GET /api/v1/designs from then on and you can use its uuid after that. Any active card in the public collection is available this way. An unavailable card or an unknown id returns 404 design_not_found; saved design ids are still restricted to your organization. Leaving the field out uses your most recent saved design; an organization with none at all gets 422 no_design_available, because every card is a greeting card with a printed front. Fronts of your own are uploaded in the web app, not over the API; see uploading your own artwork for the size. |
arrive_on | string | YYYY-MM-DD. The date the card should arrive by. Production is planned backwards from it and the quoted window ends on it. Leave it out for the soonest date we can honor. |
reference | string | Free text, echoed back in the response so you can correlate without storing our ids. It does not deduplicate. Truncated to 40 characters. |
curl -X POST https://scribblecards.com/api/v1/cards \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"recipient": {
"name": "Priya Raman",
"line1": "1 Fifth Avenue",
"line2": "Apt 12B",
"city": "New York",
"state": "NY",
"zip": "10003"
},
"message": "Congratulations on the new role, Priya.\n\nEveryone at Northwind",
"reference": "crm-deal-8812"
}'{
"id": "SC-NH4DERB",
"object": "card",
"status": "scheduled",
"held_reason": null,
"recipient": { "name": "Priya Raman" },
"arrives_between": {
"earliest": "2026-08-10",
"latest": "2026-08-14"
},
"reference": "crm-deal-8812",
"note": "One credit was charged. Arrival is a window, not a promised day.",
"url": "https://scribblecards.com/app/orders/SC-NH4DERB"
}statusisscheduledwhen the card is going to be made, orheldwhen something needs a person. A held card is not charged, andheld_reasonsays why in plain words.arrives_betweenis a window, never a promised date. It spans four to five days and always ends on the arrive-by date —arrive_onif you sent one, otherwise the soonest date we can honor. We aim to be early rather than late, because once a card is with USPS the last mile is not ours to control.idis the card code. It looks likeSC-NH4DERB: the prefixSC-, six payload characters and a check character, in an alphabet with no I, L, O or U so nothing can be misread aloud.
#GET/api/v1/cards
List the cards this organization has sent, newest first.
Requires scope cards:read
Query parameters
| Parameter | Type | Description |
|---|---|---|
limit | integer | Default 50, maximum 200. Anything larger is clamped. |
status | string | Filter by production status. See the status table below. |
reference | string | Find cards by the reference you sent on the POST — your own invoice number, CRM record id or ticket id. |
starting_after | string | The card code of the last item on the previous page. Use next_cursor from the response rather than building this yourself. |
curl "https://scribblecards.com/api/v1/cards?limit=2" \
-H "Authorization: Bearer sk_live_..."{
"object": "list",
"has_more": true,
"data": [
{
"id": "SC-2APXR1Z",
"object": "card",
"status": "in_production",
"recipient": { "name": "Tomas Nowak", "city": "Boston", "state": "MA" },
"arrives_between": { "earliest": "2026-08-10", "latest": "2026-08-14" },
"mailed_at": null,
"created_at": "2026-08-05T06:48:57.080629+00:00"
},
{
"id": "SC-5DKF1SW",
"object": "card",
"status": "written",
"recipient": { "name": "Aisha Bello", "city": "Austin", "state": "TX" },
"arrives_between": { "earliest": "2026-08-10", "latest": "2026-08-14" },
"mailed_at": null,
"created_at": "2026-08-05T06:19:04.555985+00:00"
}
]
}has_moreis true when the page came back full, andnext_cursorcarries the id to pass asstarting_afterfor the next page. It isnullon the last page, so a client can loop on that alone.- Pagination is by cursor rather than by page number on purpose. These lists are newest-first and rows are added at the newest end, so offset paging would show you the same card twice and skip another — which for a nightly sync means a missing card nobody notices.
referenceis returned on every row, and?reference=filters by it, so you can find a card by your own identifier without storing ours.
#GET/api/v1/cards/{card_code}
One card: status, per-step production progress, arrival window.
Requires scope cards:read
curl https://scribblecards.com/api/v1/cards/SC-NH4DERB \
-H "Authorization: Bearer sk_live_..."{
"id": "SC-NH4DERB",
"object": "card",
"status": "pending",
"held_reason": null,
"recipient": {
"name": "Priya Raman",
"line1": "1 Fifth Avenue",
"line2": "Apt 12B",
"city": "New York",
"state": "NY",
"zip": "10003",
"country": "US"
},
"handwriting": "miriam",
"arrives_between": {
"earliest": "2026-08-10",
"latest": "2026-08-14"
},
"steps": [
{ "step": "card_stock_prepared", "status": "pending", "completed_at": null },
{ "step": "card_written", "status": "pending", "completed_at": null },
{ "step": "envelope_addressed", "status": "pending", "completed_at": null },
{ "step": "packed", "status": "pending", "completed_at": null },
{ "step": "stamped", "status": "pending", "completed_at": null },
{ "step": "posted", "status": "pending", "completed_at": null }
],
"mailed_at": null,
"created_at": "2026-08-05T09:37:33.549357+00:00",
"url": "https://scribblecards.com/app/orders/SC-NH4DERB"
}- Card codes are case-insensitive on lookup and are normalized, so
sc-nh4derbresolves to the same card. - A card belonging to another organization returns
404, not403. Existence is not leaked across tenants. stepsalways contains all six entries in production order. A step that has not started ispending; a retried step reports its current state rather than the failure it recovered from.
#DELETE/api/v1/cards/{card_code}
Stop a card before it is made, and get the credit back.
Requires scope cards:send
curl -X DELETE https://scribblecards.com/api/v1/cards/SC-NH4DERB \
-H "Authorization: Bearer sk_live_..."{
"id": "SC-NH4DERB",
"object": "card",
"status": "cancelled",
"cancelled": true,
"credits_refunded": 1,
"note": "Canceled before anything was made. The credit is back on your balance."
}- Works while the card is
pending,queuedorheld. Those are the states where nothing physical exists yet. - Canceling an already-canceled card is a
200, not an error. Retrying a cancel is safe and refunds nothing twice. - Once the card has reached a machine you get
409 too_late_to_cancel, and the message says where it actually is so you know whether to call us.
#Card statuses
| Status | Meaning |
|---|---|
pending | Created and scheduled. Nothing physical has happened yet. |
queued | Released to the floor for a production run. |
in_production | Somewhere in the six steps. |
written | The pen has finished the card and the envelope. |
packed | In its envelope, stamped, in a tray. |
posted | Handed to USPS. mailed_at is set. |
blocked | Held. Needs a person. held_reason says why. Not charged. |
cancelled | Stopped before production. |
failed | Could not be produced. |
#Common questions
- How do I know when a card has been mailed?
- Subscribe to the
card.mailedwebhook — it fires when the card is handed to the carrier, and it is the event most systems act on. Polling this endpoint and watchingmailed_atalso works, but poll only the cards you care about, on a sensible interval. - Can I cancel a card sent through the API?
- Yes —
DELETE /api/v1/cards/{card_code}, and the credit goes back. It works while the card is stillpending,queuedorheld. Once a pen is on the paper you get a409explaining where the card actually is: there is no operation that un-writes a card, and we would rather say so than return a cheerful 200 you would act on. - Can I send the same card to several recipients in one request?
- No. One request, one card. For a batch, either loop within the rate limit or build a campaign.
- Is there a test mode?
- No sandbox and no test keys. Develop against an account with a deliberately small balance.
Last checked against the product on . Something wrong or missing? Tell us.
