Production checklist
Run through this before you flip a key from sandbox to live. Each item is a thing we've seen integrators forget — and regret. Skim now, action later.
Keys & secrets
Get these right once and the rest of production becomes routine.
- Distinct keys per environment(dev, staging, prod). Revoking one shouldn't kill the others.
- Stored in your secret manager — Vercel Project Env Vars, Doppler, 1Password Secrets, AWS Secrets Manager — never in source control.
- Never
NEXT_PUBLIC_. Keys are server-side only. - Spend caps set on every live key. Even a high cap is better than no cap — runaway loops are real.
- Webhook signing secret stored alongside the API key. Receivers verify HMAC against this.
- Rotation plan documented. Mint new key, roll out, then revoke old — in that order.
Idempotency & retries
The single most common cause of duplicate live charges is a misuse of idempotencyKey.
- Reproducible
idempotencyKey. Hash of inputs (e.g.sha256(userId + caseId + docHash)) or your order_intent row id. Not a fresh UUID generated inside a retry loop. - Retry only when
retryable: true. See /docs/errors. Retrying a 4xx validation error just wastes your rate budget. - Capped exponential backoff with jitter. Honor
retryAfterSecondswhen present. - Maximum attempt count. Five is generous; after that, page a human or move to a dead-letter queue.
// What "reproducible" looks like in practice
const idempotencyKey =
"order_" +
createHash("sha256")
.update(`${userId}.${caseId}.${primaryDocSha256}`)
.digest("hex")
.slice(0, 24);
// Hand the SAME key to every retry attempt.
for (let attempt = 1; attempt <= 5; attempt++) {
try { return await submit({ idempotencyKey, ... }); }
catch (e) {
if (!e.body?.retryable) throw e;
await sleep(Math.min(30_000, 500 * 2 ** (attempt - 1)) + jitter());
}
}Webhooks
If you're polling status() in a tight loop, you're leaving money (and rate budget) on the table.
- Webhook receiver deployed on a public URL. Behind a corporate VPN? Use a public bridge.
- Signature verified in constant time (
timingSafeEqual,hmac.compare_digest,hmac.Equal). Multi-language examples in /docs/webhooks. - Timestamp freshness check (reject events older than ~5 minutes for replay protection — retries are handled by event-id dedup, not by timestamp).
- Dedup on
x-verdacert-event-idbefore doing work. At-least-once delivery means duplicates are possible. - Ack 2xx fast (< 5s), process async. Holding the connection while doing real work earns timeout retries.
- Receiver alerts on abandoned deliveries. Surface the agent portal's “recent deliveries” counts to your own dashboard.
Certificates & JWS
If you surface the certified PDF to your own end-users, make sure they can verify it.
- JWKS cached on your side (1h or less). Refresh on signature failure before declaring a cert invalid — we rotate keys with a grace window.
- Document SHA-256 check if you re-host the PDF: compute
sha256(file)and compare topayload.documentSha256. Tampered PDF ⇒ mismatch. - Surface
publicVerifyUrlalongside any download — recipients should always have a one-click way to confirm authenticity. - Don't cache an artifact URLlonger than ~15 min — they're short-lived signed URLs. Re-fetch
/resultfor a fresh URL; the cert id and JWS are stable.
Observability
The day something goes wrong, what you logged decides whether you fix it in 2 minutes or 2 hours.
- Log
x-vc-request-idon every request, success or failure. Attach to your own trace context. This is the first thing support asks for. - Log
error.code(not just HTTP status). Stable; queryable; categorizable. - Metric: error rate by code over a rolling 5-minute window. Alert on
RATE_LIMITEDspikes,PAYMENT_FAILEDspikes,INTERNAL_ERROR(us) at all. - Metric: time-to-readyper order (submit → order.ready webhook). Watch the P95 — surfaces slow-pipeline incidents on our side that don't trip our alerts.
Payments
Live keys charge real cards on submit. Three failure modes; plan for all of them.
- Default payment method on file. Otherwise every
submitreturnsNO_PAYMENT_METHOD. - Stripe 3DS surface pathwired up. Some cards require Strong Customer Authentication that an agent can't complete off-session — you must surface
next_actionto the human. - Recover from
PAYMENT_FAILEDby retrying with the sameidempotencyKeyafter the card is updated.
Pre-launch testing
Don't skip these. The full live pipeline is harder to mock than to actually run.
- Sandbox smoke test passing — capabilities → quote → submit → status → result → verify. See /docs/sandbox.
- One real live orderend-to-end before scaling. Refund it after so you've also exercised the refund path.
- Failure-injection tests: simulate
RATE_LIMITED,VALIDATION_ERROR, webhook 5xx, and confirm your retry / fallback paths behave. - Idempotency replay test: re-call
submitwith the same key + same body, confirm no duplicate order; with same key + different body, confirmIDEMPOTENCY_CONFLICT.
Legal & policy
One-time housekeeping that's easy to forget.
- Refund policy surfaced to your end-users. Link to our policy and your own (the two interact — your refund window can be shorter than ours but not longer).
- End-user consent for sending documents to a third party (us) where applicable. Pass
endUser.externalIdonsubmitfor attribution. - Data retention expectations. We retain source documents and certificates for the published retention window.
Incident response
Plan once, breathe easy later.
- Bookmark verdacert.com/status on your team's incident dashboard.
- Direct line to us: hello@verdacert.com. Email the
x-vc-request-idfrom a failing call. - Fallback plan for sustained outages: queue submissions to a durable store; retry once the
order.createdevent flows again. Avoid customer-facing “sorry, try later” if you can help it.
