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_REVOKED-Returned 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.
CREDIT_LIMIT_EXCEEDED402Account is billed on net terms and this order would push it past its credit limit. Distinct from PAYMENT_FAILED: there is no card involved, so retrying cannot succeed.Settle an outstanding invoice from the firm portal under Billing, or ask your account manager to raise the limit, then resubmit with the same idempotencyKey. Call get_capabilities() to see billing.creditLimitCents up front.
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