API documentation

Sandbox

Sandbox keys (vc_sandbox_…) run a deterministic wall-clock state machine. No real translators touch the order, no Stripe charge fires, but every other code path — auth, rate limiting, certificate issuance, JWS receipts, webhooks — is identical to live. Perfect for end-to-end agent dev.

Get a sandbox key (60 seconds)

No waitlist, no sales call, no payment method required.

  1. Open /onboarding. Sign in with your work email (magic-link auth — no password).
  2. Name your firm. One firm per account.
  3. On the next screen (/portal/api), click Mint key → pick env sandbox → copy the token. This is the only time the raw token is shown.

Live keys (vc_live_…) unlock the moment you add a default payment method on the same page. Everything else (webhook URL, spend caps, label) configures identically across both environments.

Lifecycle (what the sandbox does)

Status is a pure function of (createdAt, productLine, now). No background worker, no pipeline kick — status() advances the row atomically on each poll.

Time since submitfull_translationreview_and_certify
0–5 screatedcreated
5–15 spaidpaid
15–60 sprocessingreviewing_draft
60–90 sin_reviewreviewing_draft
90–120 sin_reviewready ✓
120 s+ready ✓ready

That's ~2 minutes to ready for full translation, ~90 seconds for Review-and-Certify. Both fire webhooks at each transition — exactly one per state change, even when many polls race.

The 'first call to status' advances the state machine
We update the DB row + fire the webhook on the first status()call that observes a transition. If you're testing webhooks against sandbox, poll status periodically (every 15s is fine) and your receiver will see the full sequence of events naturally.

Smoke test (copy-paste, ~3 minutes)

The shortest path to “I've exercised every endpoint.” Substitute your sandbox token.

# 0. Ground on enums
curl -s https://verdacert.com/api/v1/capabilities \
  -H "Authorization: Bearer vc_sandbox_…" | jq .

# 1. Quote (free, no order)
QUOTE=$(curl -s -X POST https://verdacert.com/api/v1/quote \
  -H "Authorization: Bearer vc_sandbox_…" \
  -H "Content-Type: application/json" \
  -d '{"sourceLanguage":"fa","useCase":"uscis","pageCount":2,"speedTier":"standard"}')
echo "$QUOTE" | jq .
QUOTE_ID=$(echo "$QUOTE" | jq -r .quoteId)

# 2. Submit (returns jobId immediately)
JOB=$(curl -s -X POST https://verdacert.com/api/v1/submit \
  -H "Authorization: Bearer vc_sandbox_…" \
  -H "Content-Type: application/json" \
  -d "{
    \"quoteId\":        \"$QUOTE_ID\",
    \"idempotencyKey\": \"smoke-test-$(date +%s)\",
    \"echoedInput\": {
      \"sourceLanguage\":\"fa\",\"useCase\":\"uscis\",
      \"pageCount\":2,\"speedTier\":\"standard\"
    },
    \"documents\":[{\"url\":\"https://example.com/birth.pdf\"}]
  }")
echo "$JOB" | jq .
JOB_ID=$(echo "$JOB" | jq -r .jobId)

# 3. Poll until ready (sandbox: ~2 min)
while true; do
  STATUS=$(curl -s "https://verdacert.com/api/v1/status/$JOB_ID" \
    -H "Authorization: Bearer vc_sandbox_…" | jq -r .status)
  echo "status: $STATUS"
  [ "$STATUS" = "ready" ] && break
  sleep 20
done

# 4. Fetch certified result
RESULT=$(curl -s "https://verdacert.com/api/v1/result/$JOB_ID" \
  -H "Authorization: Bearer vc_sandbox_…")
echo "$RESULT" | jq .
CERT_ID=$(echo "$RESULT" | jq -r .certification.certificateId)

# 5. Verify the certificate (public, no auth)
curl -s "https://verdacert.com/api/v1/verify/$CERT_ID" | jq .

Total wall-clock: ~3 minutes. If anything fails, the response carries an x-vc-request-id header and a structured error body — see /docs/errors.

What sandbox differs in

Everything else (including JWS receipts) is byte-identical to live.

BehaviorSandboxLive
BillingNever chargesStripe off-session charge on submit
Translation workNone — state machine driven by wall clockReal AI pipeline + human reviewer queue
Artifact URL/sandbox/demo-certified-translation.pdfSigned R2 URL to the actual certified PDF
JWS receiptReal Ed25519 JWS with isSandbox=true claimReal Ed25519 JWS with isSandbox=false claim
JWKS endpointSame /.well-known/jwks.jsonSame /.well-known/jwks.json
verify endpointReturns acceptanceGuarantee=null, isSandbox=trueReturns acceptanceGuarantee={uscis:true}, isSandbox=false
Webhook signingSame per-key secret + HMAC-SHA256 schemeSame per-key secret + HMAC-SHA256 scheme
Rate limit300 req/min per key300 req/min per key
Spend capsNot enforced (no billing)Enforced — SPEND_LIMIT_EXCEEDED on overflow
RefundsNo Stripe round-trip; lifecycle (status + webhook + audit) still firesReal Stripe refund + lifecycle

When you're ready to promote to live

Sandbox should fully validate your integration before you flip the key.

  • ✅ Smoke test completes against sandbox without error handling pop.
  • ✅ Webhook receiver verifies signatures and dedups on event.id.
  • ✅ Your retry strategy handles retryable: true errors with backoff but never retries 4xx validation failures.
  • ✅ You've verified at least one sandbox JWS against JWKS to confirm your verification code works.
  • ✅ You're comfortable with the per-page rate sheet — see /pricing.

Then: /portal/api → add a default payment method → mint a live key. Swap the env var. Done.

Where to next

Get instant quotePricing