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.
- Open /onboarding. Sign in with your work email (magic-link auth — no password).
- Name your firm. One firm per account.
- 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 submit | full_translation | review_and_certify |
|---|---|---|
| 0–5 s | created | created |
| 5–15 s | paid | paid |
| 15–60 s | processing | reviewing_draft |
| 60–90 s | in_review | reviewing_draft |
| 90–120 s | in_review | ready ✓ |
| 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.
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.
| Behavior | Sandbox | Live |
|---|---|---|
| Billing | Never charges | Stripe off-session charge on submit |
| Translation work | None — state machine driven by wall clock | Real AI pipeline + human reviewer queue |
| Artifact URL | /sandbox/demo-certified-translation.pdf | Signed R2 URL to the actual certified PDF |
| JWS receipt | Real Ed25519 JWS with isSandbox=true claim | Real Ed25519 JWS with isSandbox=false claim |
| JWKS endpoint | Same /.well-known/jwks.json | Same /.well-known/jwks.json |
| verify endpoint | Returns acceptanceGuarantee=null, isSandbox=true | Returns acceptanceGuarantee={uscis:true}, isSandbox=false |
| Webhook signing | Same per-key secret + HMAC-SHA256 scheme | Same per-key secret + HMAC-SHA256 scheme |
| Rate limit | 300 req/min per key | 300 req/min per key |
| Spend caps | Not enforced (no billing) | Enforced — SPEND_LIMIT_EXCEEDED on overflow |
| Refunds | No Stripe round-trip; lifecycle (status + webhook + audit) still fires | Real 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: trueerrors 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.
