API documentation

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.

TopicDetail
Base URLhttps://verdacert.com/api/v1
Auth headerAuthorization: Bearer vc_<env>_<token>
Content typeRequest: application/json. Response: application/json; charset=utf-8.
Request idEvery response has x-vc-request-id. Log it alongside your own ids.
Idempotencysubmit requires idempotencyKey in the body. refund is Stripe-idempotent server-side. Reads are naturally idempotent.
Rate limit300 req / 60s rolling per key. Exceed → HTTP 429 with Retry-After header and RATE_LIMITED code.
Time formatAll timestamps are ISO 8601 strings (UTC). Money in integer cents.
Error envelopeEvery non-2xx response is { error: { code, message, recoveryHint, retryable, retryAfterSeconds?, field? } }. See errors.
Headers we set on the response
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

FieldTypeRequiredNotes
sourceLanguagestringyesISO 639-1/2 code from capabilities.supportedSourceLanguages.
targetLanguagestringnoDefaults to en.
useCaseenumyesOne of uscis | court | university | medical | employer | embassy | apostille_outbound | other.
documentTypeenumnoAffects complexity surcharge — see capabilities.documentTypes.
pageCountintegeryes1–500.
speedTierenumyesstandard | express | rush. SLA from capabilities.
productLineenumnofull_translation (default) or review_and_certify. R&C requires a draft on submit; pricing is ~50%.
addonsobjectnoBooleans: 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

FieldTypeRequiredNotes
quoteIdstringyesFrom the matching quote() call. 24h TTL.
idempotencyKeystringyes8–128 chars. Yours to choose; reusing returns the original job. See /docs/idempotency.
echoedInputobjectyesExact input from the matching quote() call. Server re-prices off this to bind the quote.
documentsarray<object>yes1–50 entries. Each { url, sha256?, pageHint? }. URLs must be publicly reachable (signed S3 / R2 URLs are fine), < 25MB, PDF/JPEG/PNG.
draftTranslationobjectif R&CRequired when productLine === "review_and_certify". { url, sha256? }.
endUserobjectnoOptional. { externalId, email?, consentToken? }. Lets your end-user be tied to the order for refunds and per-user attribution.
webhookUrlstringnoPer-call override of the URL stored on the API key. Useful for per-tenant routing.
metadataobject<string,string>noPass-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

statusprogressPercentMeaning
created5Order accepted; not yet paid.
paid15Payment confirmed; queued for translation.
processing45AI draft pipeline running.
reviewing_draft70R&C only: human-reviewing the agent draft.
in_review75–80Human reviewer verifying the translation.
ready95–100Certified translation ready. Fetch via /result.
delivered100Customer has accessed the artifact.
refunded100Order refunded (full).
failed100Order failed. Contact support with x-vc-request-id.
cancelled100Order 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

Where to next

Get instant quotePricing