API documentation

Error reference

Every Verdacert error returns the same envelope, with a stable machine-readable code, a human message, and a recoveryHint tuned for an LLM consumer. code values are part of the API contract and will not change.

Error envelope

Every non-2xx response (REST, MCP, and the AI SDK tool surface) has this shape.

{
  "error": {
    "code":              "QUOTE_EXPIRED",
    "message":           "Quote has expired (quotes are valid for 24 hours).",
    "recoveryHint":      "Call quote() again with the same inputs to get a fresh quoteId.",
    "retryable":         false,
    "retryAfterSeconds": 60,
    "field":             "documents"
  }
}
FieldTypeNotes
codestringStable machine-readable code. Branch on this.
messagestringHuman-readable; safe to show to operators or surface in agent UX.
recoveryHintstringWritten for an LLM consumer: a one-liner the model can act on.
retryablebooleantrue ⇒ same call can succeed on retry (e.g. RATE_LIMITED, INTERNAL_ERROR). false ⇒ change inputs first.
retryAfterSecondsinteger (optional)Suggested back-off when retryable.
fieldstring (optional)For VALIDATION_ERROR: the offending field path (e.g. documents.0.url).
Programmatic retry strategy
A safe default: retry when retryable === true with capped exponential backoff, starting from retryAfterSeconds if present (else ~1s). Never retry a VALIDATION_ERRORor other 4xx without changing the input — you'll just rate-limit yourself.

Auth & rate limits

Apply to every endpoint. RATE_LIMITED is the only retryable one.

CodeHTTPWhenRecovery
MISSING_AUTH401Authorization header absent.Include Authorization: Bearer vc_….
INVALID_AUTH401Token doesn't match any active key.Confirm the token isn't URL-encoded or truncated. Sandbox keys start vc_sandbox_; live keys start vc_live_.
REVOKED_KEY401Token was revoked in the portal.Mint a fresh key in /portal/api.
RATE_LIMITED429More than 300 requests in the last 60s on this key.Back off for the seconds returned in retryAfterSeconds (and the standard Retry-After header). Reduce concurrency; batch documents into a single submit.

Input validation

4xx — your inputs need to change before a retry will succeed.

CodeHTTPWhenRecovery
VALIDATION_ERROR400Generic input validation failure.Inspect error.field and the human message. Use capabilities to ground enum values.
UNSUPPORTED_LANGUAGE400sourceLanguage isn't in the supported list.Call capabilities to fetch the current supportedSourceLanguages.
UNSUPPORTED_USE_CASE400useCase isn't in the supported list.Call capabilities; pick an allowed useCase.
ADDON_UNAVAILABLE400Add-on is currently gated off (e.g. apostille while we're scaling that pipeline).Check capabilities.addons[].available before quoting.
DRAFT_REQUIRED_FOR_REVIEW_AND_CERTIFY400Submitted with productLine: review_and_certify but no draftTranslation.Supply the agent-generated draft URL, or switch to full_translation.
REVIEW_AND_CERTIFY_NOT_YET_AVAILABLE400R&C requested but not yet GA on your tenant.Use full_translation for now; R&C ships in the next release.
DOCUMENT_TOO_LARGE413Any document exceeds 25MB.Split or compress the file before submitting.
DOCUMENT_UNREADABLE400A document URL couldn't be fetched or parsed.Ensure each documents[].urlis reachable from the public internet, returns PDF/JPEG/PNG, and is < 25MB. Signed S3 / R2 URLs work; URLs behind login don't.

Quote flow

Specific to quote and submit. The quote ⇄ submit binding is what makes stateless quoting honest.

CodeHTTPWhenRecovery
INVALID_QUOTE400quoteId doesn't parse / isn't recognized.Call quote again to get a fresh id, then submit.
QUOTE_EXPIRED410quoteId is older than the 24h TTL.Re-call quote with the same inputs.
QUOTE_MISMATCH400quoteId belongs to a different API key.Quotes are scoped to the key that minted them. Call quote with the current key first.

Order & certificate lifecycle

Returned by submit, status, result, refund, verify.

CodeHTTPWhenRecovery
JOB_NOT_FOUND404jobId doesn't exist OR belongs to a different key.Verify the jobId returned by submit. Jobs are scoped to the key that created them.
JOB_NOT_READY409result called before the job reached ready / delivered.Poll status until ready, or subscribe to the order.ready webhook.
JOB_ALREADY_DELIVERED409Operation invalid for already-delivered orders.Verify your assumptions on order state; contact support for special cases.
IDEMPOTENCY_CONFLICT409A previous submit used this idempotencyKey with different inputs.Pick a fresh idempotencyKey for the new submission, or call status with the original jobId.
CERTIFICATE_NOT_FOUND404verify() with an unknown certificateId.Confirm the certificateId from getResult().
CERTIFICATE_REVOKEDReturned implicitly via valid: false + revokedAt on verify().Not retryable — the certificate has been revoked. Re-issue.

Billing & payments

Live keys only — sandbox never charges. Idempotency on submit makes these safe to retry once the underlying issue is fixed.

CodeHTTPWhenRecovery
NO_PAYMENT_METHOD402Live submit with no default payment method on file.Have an operator complete the SetupIntent flow in the portal. Then retry submit with the same idempotencyKey.
PAYMENT_FAILED402Stripe declined the off-session charge.Account holder updates their card via the SetupIntent flow; retry with the same idempotencyKey.
PAYMENT_REQUIRES_ACTION402Card requires 3DS — agent can't complete this off-session.Surface Stripe's next_actionto the end-user, have them complete 3DS, then retry. Off-session 3DS cannot be performed by the agent on the end-user's behalf.
SPEND_LIMIT_EXCEEDED402Per-day or per-month spend cap on this key was hit.Raise the cap in the portal, or wait for the rolling window to reset.

Server-side (5xx)

Our fault.

CodeHTTPWhenRecovery
INTERNAL_ERROR500We didn't anticipate this. The team has been paged.Retry with exponential backoff. If persistent, share the x-vc-request-id with support@verdacert.com.
UPSTREAM_UNAVAILABLE503A dependent service is degraded (Stripe, AI provider, R2).Retry with backoff. Watch verdacert.com/status for live status.

Where to next

Get instant quotePricing