REST API reference
Complete v1 reference. Every endpoint below also lives in the OpenAPI 3.1 spec — feed that to Stainless, Speakeasy, or Postman for typed SDKs in any language. The MCP server and AI SDK tools share this surface; one source of truth, three transports.
Conventions
Stable across every endpoint.
| Topic | Detail |
|---|---|
| Base URL | https://verdacert.com/api/v1 |
| Auth header | Authorization: Bearer vc_<env>_<token> |
| Content type | Request: application/json. Response: application/json; charset=utf-8. |
| Request id | Every response has x-vc-request-id. Log it alongside your own ids. |
| Idempotency | submit requires idempotencyKey in the body. refund is Stripe-idempotent server-side. Reads are naturally idempotent. |
| Rate limit | 300 req / 60s rolling per key. Exceed → HTTP 429 with Retry-After header and RATE_LIMITED code. |
| Time format | All timestamps are ISO 8601 strings (UTC). Money in integer cents. |
| Error envelope | Every non-2xx response is { error: { code, message, recoveryHint, retryable, retryAfterSeconds?, field? } }. See errors. |
x-vc-request-id is set on every response (success and error). Retry-After is set on 429 responses. Cache-Control is set on the static spec / JWKS endpoints (1h).GET /capabilities
Returns the live source-of-truth enums: supported languages, document types, speed tiers (and SLAs), add-ons and their live availability, product lines, and the USCIS acceptance guarantee URL. Free — no per-call charge — but auth-gated to keep crawlers off.
Request
curl https://verdacert.com/api/v1/capabilities \
-H "Authorization: Bearer vc_sandbox_…"Response (200)
{
"supportedSourceLanguages": [
"ar", "fa", "ur", "tr", "ps", "prs", "ti", "am",
"ku", "so", "he", "az", "om", "bn", "pa", "hi",
"tg", "uz", "ha", "sw", "other"
],
"supportedUseCases": [
"uscis", "court", "university", "medical", "employer",
"embassy", "apostille_outbound", "other"
],
"speedTiers": [
{ "tier": "standard", "slaHours": 48, "specialtyLanguageMultiplier": 1.25 },
{ "tier": "express", "slaHours": 24, "specialtyLanguageMultiplier": 1.25 },
{ "tier": "rush", "slaHours": 14, "specialtyLanguageMultiplier": 1.25 }
],
"documentTypes": [
{ "type": "birth_certificate", "complexity": "common" },
{ "type": "marriage_certificate", "complexity": "common" },
{ "type": "court_document", "complexity": "complex" }
],
"productLines": ["full_translation", "review_and_certify"],
"addons": [
{ "key": "notarization", "priceCents": 2495, "available": false },
{ "key": "hardcopyDomestic", "priceCents": 1495, "available": true },
{ "key": "hardcopyInternational", "priceCents": 3495, "available": true },
{ "key": "apostille", "priceCents": 8995, "available": false },
{ "key": "additionalCopies", "priceCents": 995, "available": true }
],
"acceptanceGuarantee": {
"uscis": true,
"refundPolicyUrl": "https://verdacert.com/quality#acceptance-guarantee"
}
}Errors
MISSING_AUTH (401), INVALID_AUTH (401), REVOKED_KEY (401), RATE_LIMITED (429).
POST /quote
Binding price + ETA. Free; no order is created. Returns a quoteId valid for 24 hours and an echoedInput that you must pass back to submit.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
sourceLanguage | string | yes | ISO 639-1/2 code from capabilities.supportedSourceLanguages. |
targetLanguage | string | no | Defaults to en. |
useCase | enum | yes | One of uscis | court | university | medical | employer | embassy | apostille_outbound | other. |
documentType | enum | no | Affects complexity surcharge — see capabilities.documentTypes. |
pageCount | integer | yes | 1–500. |
speedTier | enum | yes | standard | express | rush. SLA from capabilities. |
productLine | enum | no | full_translation (default) or review_and_certify. R&C requires a draft on submit; pricing is ~50%. |
addons | object | no | Booleans: notarization, hardcopyDomestic, hardcopyInternational, apostille. Integer additionalCopies(0–20). Server rejects any that aren't live in capabilities. |
Example
curl -X POST https://verdacert.com/api/v1/quote \
-H "Authorization: Bearer vc_sandbox_…" \
-H "Content-Type: application/json" \
-d '{
"sourceLanguage": "fa",
"useCase": "uscis",
"pageCount": 3,
"speedTier": "standard",
"addons": { "notarization": true }
}'Response (200)
{
"quoteId": "q_aabbccdd_1716480000000_x9k3",
"totalCents": 9620,
"currency": "USD",
"promisedDeliveryAt": "2026-05-24T18:00:00.000Z",
"expiresAt": "2026-05-23T18:00:00.000Z",
"breakdown": {
"pages": 3,
"perPageBaseCents": 1900,
"perPageComplexitySurchargeCents": 0,
"languageMultiplier": 1.25,
"translationSubtotalCents": 7125,
"addonsCents": 2495,
"addonsBreakdown": [
{ "label": "Notarization", "cents": 2495 }
],
"subtotalCents": 9620,
"promisedDeliveryHours": 48,
"complexity": "specialty",
"speedTier": "standard",
"productLine": "full_translation"
},
"echoedInput": {
"sourceLanguage": "fa",
"useCase": "uscis",
"pageCount": 3,
"speedTier": "standard",
"addons": { "notarization": true }
}
}Errors
VALIDATION_ERROR (400), UNSUPPORTED_LANGUAGE (400), ADDON_UNAVAILABLE (400), MISSING_AUTH / INVALID_AUTH / REVOKED_KEY (401), RATE_LIMITED (429).
POST /submit
Creates a job. On a live key, the call charges your default payment method off-session (Stripe Payment Intent), then enqueues the order. On a sandbox key, the order enters the synthetic state machine — no charge.
Idempotent. Same (apiKey, idempotencyKey) always returns the same job. Use this to retry safely.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
quoteId | string | yes | From the matching quote() call. 24h TTL. |
idempotencyKey | string | yes | 8–128 chars. Yours to choose; reusing returns the original job. See /docs/idempotency. |
echoedInput | object | yes | Exact input from the matching quote() call. Server re-prices off this to bind the quote. |
documents | array<object> | yes | 1–50 entries. Each { url, sha256?, pageHint? }. URLs must be publicly reachable (signed S3 / R2 URLs are fine), < 25MB, PDF/JPEG/PNG. |
draftTranslation | object | if R&C | Required when productLine === "review_and_certify". { url, sha256? }. |
endUser | object | no | Optional. { externalId, email?, consentToken? }. Lets your end-user be tied to the order for refunds and per-user attribution. |
webhookUrl | string | no | Per-call override of the URL stored on the API key. Useful for per-tenant routing. |
metadata | object<string,string> | no | Pass-through key/value pairs. Surfaced in admin views for support. |
Example
curl -X POST https://verdacert.com/api/v1/submit \
-H "Authorization: Bearer vc_sandbox_…" \
-H "Content-Type: application/json" \
-d '{
"quoteId": "q_aabbccdd_1716480000000_x9k3",
"idempotencyKey": "order-demo-0001",
"echoedInput": {
"sourceLanguage": "fa",
"useCase": "uscis",
"pageCount": 3,
"speedTier": "standard",
"addons": { "notarization": true }
},
"documents": [
{ "url": "https://example.com/birth-cert.pdf" }
],
"endUser": { "externalId": "user_42" },
"metadata": { "caseRef": "I-130-2026-001" }
}'Response (200)
{
"jobId": "ord_8a3c1b…",
"orderNumber": "VC-2026-000142",
"status": "paid",
"estimatedCompletionAt": "2026-05-24T18:00:00.000Z"
}checkoutUrl may also appear during the v1 beta for some live submissions (you forward it to your end-user to complete payment). Sandbox responses omit this field.
Errors
VALIDATION_ERROR, INVALID_QUOTE, QUOTE_EXPIRED, QUOTE_MISMATCH, DRAFT_REQUIRED_FOR_REVIEW_AND_CERTIFY, UNSUPPORTED_LANGUAGE, DOCUMENT_TOO_LARGE, DOCUMENT_UNREADABLE, IDEMPOTENCY_CONFLICT, NO_PAYMENT_METHOD, PAYMENT_FAILED, PAYMENT_REQUIRES_ACTION, SPEND_LIMIT_EXCEEDED. See /docs/errors.
GET /status/{jobId}
Poll the state of an order. Safe at any frequency, but please back off — recommended start is 30s with exponential growth. Better yet, subscribe to webhooks and skip polling entirely.
Response (200)
{
"jobId": "ord_8a3c1b…",
"orderNumber": "VC-2026-000142",
"status": "in_review",
"statusDescription": "Reviewer is verifying the translation.",
"progressPercent": 75,
"estimatedCompletionAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-22T18:01:15.000Z",
"events": [
{ "at": "2026-05-22T17:59:00.000Z", "type": "order.created" }
]
}status values
| status | progressPercent | Meaning |
|---|---|---|
| created | 5 | Order accepted; not yet paid. |
| paid | 15 | Payment confirmed; queued for translation. |
| processing | 45 | AI draft pipeline running. |
| reviewing_draft | 70 | R&C only: human-reviewing the agent draft. |
| in_review | 75–80 | Human reviewer verifying the translation. |
| ready | 95–100 | Certified translation ready. Fetch via /result. |
| delivered | 100 | Customer has accessed the artifact. |
| refunded | 100 | Order refunded (full). |
| failed | 100 | Order failed. Contact support with x-vc-request-id. |
| cancelled | 100 | Order cancelled before fulfillment. |
Errors
JOB_NOT_FOUND (404), plus the standard auth + rate-limit set.
GET /result/{jobId}
Returns the certified artifact + certification metadata + a compact JWS receipt. Valid only when status === "ready" or "delivered"; otherwise returns JOB_NOT_READY.
Response (200)
{
"jobId": "ord_8a3c1b…",
"orderNumber": "VC-2026-000142",
"artifacts": [
{
"kind": "certified_pdf",
"url": "https://verdacert.com/r2/orders/ord_8a3c1b/certified.pdf?…",
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4…",
"pageCount": 3,
"expiresAt": "2026-05-22T18:15:00.000Z"
}
],
"certification": {
"certificateId": "vcrt_01h…",
"translatorName": "Roya Khan",
"translatorCredentials": "ATA-certified, FA→EN",
"issuedAt": "2026-05-22T18:00:00.000Z",
"publicVerifyUrl": "https://verdacert.com/verify/vcrt_01h…",
"jws": "eyJhbGciOiJFZERTQSIsImtpZCI6Imt1MSJ9…"
},
"acceptanceGuarantee": {
"uscis": true,
"refundIfRejectedUrl": "https://verdacert.com/quality#acceptance-guarantee"
},
"referral": {
"revShareBps": 1000,
"payoutCents": 1185,
"availableAt": "2026-06-21T00:00:00.000Z"
}
}artifact URLs are short-lived — they expire in ~15 minutes. Re-call /result for a fresh URL; the underlying file (and the certificate JWS) are stable.
Errors
JOB_NOT_FOUND (404), JOB_NOT_READY (409), plus the standard set.
POST /refund/{jobId}
Refund an order placed by the calling key. Full refund by default; pass amountCentsfor partial. 30-day window from order creation; outside that, contact support. Idempotency-keyed Stripe-side so retries don't double-refund.
Request body (optional)
{
"amountCents": 5000,
"reason": "requested_by_customer"
}reason is one of duplicate | fraudulent | requested_by_customer | other (default requested_by_customer). Forwards to Stripe's refund reason on live orders.
Response (200)
{
"jobId": "ord_8a3c1b…",
"orderNumber": "VC-2026-000142",
"refundedCents": 5000,
"status": "paid",
"refundId": "re_3O…"
}status only flips to "refunded" on a full refund. Partial refunds keep the order in its existing state. refundId is the Stripe refund id (null in sandbox).
Errors
JOB_NOT_FOUND (404), VALIDATION_ERROR (400 — e.g. amount exceeds remaining refundable, already fully refunded, outside 30-day window), plus the standard set.
GET /verify/{certificateId}
Public — no auth required. Returns the certificate metadata + JWS, with both DB revocation status and live signature validation merged into valid. Use this when you've been handed a certificate id by a third party (e.g. a recipient verifying an artifact) and want a quick yes/no with cryptographic backing.
Response (200)
{
"valid": true,
"certificateId": "vcrt_01h…",
"issuedAt": "2026-05-22T18:00:00.000Z",
"revokedAt": null,
"revokedReason": null,
"translator": {
"name": "Roya Khan",
"credentials": "ATA-certified, FA→EN"
},
"documentHash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4…",
"isSandbox": false,
"acceptanceGuarantee": { "uscis": true },
"jws": "eyJhbGciOiJFZERTQSIsImtpZCI6Imt1MSJ9…"
}validis the AND of (a) the row isn't revoked and (b) the stored JWS verifies against the active JWKS. A revoked-but-cryptographically-valid cert returns valid: false; so does an unrevoked-but-tampered cert.
Verifying offline
You can skip the round-trip entirely: fetch /.well-known/jwks.json once, cache for ~1h, then verify any JWS using jose / python-jose / equivalent. See /docs/certificates for a walkthrough with code in TypeScript and Python.
Errors
CERTIFICATE_NOT_FOUND (404). No 401 — this endpoint never authenticates.
GET /.well-known/jwks.json
Public JWKS for Verdacert's Ed25519 signing keys. No auth. Cacheable for ~1h. Use to verify any JWS receipt (live or sandbox) without contacting us.
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"kid": "ku1",
"x": "…base64url public-key bytes…"
}
]
}Keys are rotated periodically (lazily — old key remains for a grace window). The kid claim in the JWS header tells you which key signed it.
GET /api/v1/openapi.yaml
Machine-readable OpenAPI 3.1 spec for the v1 surface. Feed it to any code-gen tool (Stainless, Speakeasy, OpenAPI Generator) to produce a typed SDK in your language of choice. Updated alongside the API; cache 1h.
curl -O https://verdacert.com/api/v1/openapi.yaml