API documentation

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.

The four-line summary
Sandbox-tested. Idempotency keys are stable across retries. Webhook signatures verified in constant time. Spend cap set below the “wake me up” threshold.

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 retryAfterSeconds when 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-id before 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 to payload.documentSha256. Tampered PDF ⇒ mismatch.
  • Surface publicVerifyUrl alongside 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 /result for 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-id on 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_LIMITED spikes, PAYMENT_FAILED spikes, 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 submit returns NO_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_action to the human.
  • Recover from PAYMENT_FAILED by retrying with the same idempotencyKey after 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 submit with the same key + same body, confirm no duplicate order; with same key + different body, confirm IDEMPOTENCY_CONFLICT.

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-id from 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.

Where to next

Get instant quotePricing