Skip to content

Production checklist ​

Work through this before your first real-volume send. Every item exists because getting it wrong costs printed paper.

Credentials ​

  • [ ] Key stored in a secret manager or environment variable — never in source control, client-side code or a browser.
  • [ ] Separate keys per workload (sending, reporting), so a leak is contained and one workload cannot exhaust another's per-key rate limit. Note all keys share the account ceiling, so extra keys isolate budgets rather than adding throughput.
  • [ ] Only the permissions each key needs. In particular, only the sending service holds campaigns:create, and nothing holds qr-codes:delete unless you genuinely need it.
  • [ ] A documented revoke-and-rotate procedure (web app → API page).
  • [ ] Expiry noted in your calendar if the key was created with one — an expired key returns 401 and no amount of retrying helps.

Idempotency ​

  • [ ] Idempotency-Key sent on every POST /api/v1/campaigns.
  • [ ] The key is generated once per send intent and persisted before the request, so a crash-and-restart reuses it.
  • [ ] Retries reuse the same key; a new key is never generated inside a retry loop.
  • [ ] Your code branches on 201 vs 200 to know whether it created or replayed.
  • [ ] No key is ever reused for a logically different send — the payload is not compared, so a reused key silently ignores the new body.

See Idempotency.

Content ​

  • [ ] Templates authored and visually approved in the web app.
  • [ ] Template ids pinned in configuration, not resolved by name at send time.
  • [ ] No dependence on raw editor content — it is unsupported and will be rejected for API keys in a future release.
  • [ ] For PDF sends: A4, correct page count, embedded fonts, and the top 77 mm of page 1 kept clear of artwork. The API does not check any of this. See PDF campaigns.
  • [ ] At least one real proof letter posted to your own address per layout, and inspected on paper.

Recipient data ​

  • [ ] Addresses deduplicated before sending — nothing dedupes them for you, and two identical rows mean two letters.
  • [ ] import.imported (or the import endpoint's imported) compared against the count you expected, with an alert on the difference.
  • [ ] skipped logged and understood. { "imported": 0, "skipped": 500 } is a 200 response that mailed nothing.
  • [ ] One stored record spot-checked after an import, to catch a swapped columnIndex that the counts alone would hide.
  • [ ] Column mappings validated against the list's schema before importing into an existing list.
  • [ ] Missing-value policies set deliberately — the default is skip, which drops rows silently.

Error handling ​

  • [ ] Branch on error.code, never on error.message.
  • [ ] Retry only rate_limited and service_unavailable; treat other 4xx as bugs in the request.
  • [ ] Retry-After honoured on 429 rather than guessed at.
  • [ ] Exponential backoff with jitter for everything else.
  • [ ] RateLimit-Remaining watched so you slow down before being throttled.
  • [ ] 401 and 403 routed to an operator alert, not into the retry loop.
  • [ ] details accessed defensively — many 400s are message-only: body.error.details?.fieldErrors ?? {}.
  • [ ] A 500 on campaign create retried only with the original Idempotency-Key.

See Errors.

Polling ​

  • [ ] Polling starts at ~2 s and backs off, capped around 30 s. Not a tight loop.
  • [ ] An overall timeout that raises "check on this", not "it failed".
  • [ ] failed never triggers an automatic resend — some mail may already have printed.
  • [ ] Dashboards use GET /api/v1/campaigns/status-counts rather than polling every campaign.

See Polling & failures.

Rate limits ​

  • [ ] Bulk reads use limit=100, not the default 20.
  • [ ] Bulk recipient loading uses a file import, not a loop of single adds.
  • [ ] Large sweeps are paced below ~90 requests/minute.
  • [ ] 429 handled everywhere, including inside polling loops.

Data lifecycle ​

  • [ ] You know that a list is deleted after 90 days without activity, and that reading does not count as activity.
  • [ ] Audiences you care about are held in your system as the source of truth.
  • [ ] You accept that campaigns of a deleted list survive with listId: null.
  • [ ] You know there is no update or delete for recipients or lists over the API.

See Lists & recipients.

QR codes ​

  • [ ] Codes attached in the campaign create call, not afterwards.
  • [ ] id persisted at create time — it is the only handle for retargeting later, and no screen in the web app shows it.
  • [ ] slug persisted too; trackingUrl is not returned again.
  • [ ] The redirect verified with a manual request before mailing.
  • [ ] trackingEnabled: true if you want the campaign under Analys in the web app; landing-page analytics still needed if your own systems must hold the numbers (no scan API yet).
  • [ ] Deleting a QR code understood as permanent breakage of printed mail.

Telephony add-on ​

  • [ ] Letters using the add-on carry the Telefonnummer field where the number should print — the web app warns, but does not refuse, without it.
  • [ ] The forwarding number is one that answers during office hours; unanswered calls are logged as missed and the caller hears nothing else.

Configuration ​

  • [ ] Base URL configurable per environment — no hardcoded localhost address from local development left in your code.
  • [ ] Health check wired to GET /api so you can tell a bad URL from a bad key.
  • [ ] Request timeouts set. Campaign create with a large multipart body is slow; allow for it.

Before the first big send ​

  • [ ] A send to exactly one recipient — your own address — completed and the letter inspected on paper.
  • [ ] The whole failure path exercised deliberately: a bad row (422), a wrong id (404), a missing permission (403).
  • [ ] An operator alert path that a human actually reads for failed campaigns.
  • [ ] Volume and cost confirmed. There is no cancel and no unsend once a campaign reaches processing.

The last check is the one that matters

POST /api/v1/campaigns is irreversible and billable. Confirm the recipient count you are about to mail before the call, and log it with the campaign id. A mistyped filter that turns 250 letters into 25,000 is discovered too late.

Next ​