# Schemas

Every OpenAPI schema used in request/response bodies of v1 — see [openapi.json](/docs/v1/openapi.json) for the full field-level definitions.

## V1Error {#schema-v1error}

Shared error body for every auth/registry/signing/origin/pool error (SHIELD_KEY_*, SCOPE_DENIED, ORIGIN_NOT_ALLOWED, POOL_MISMATCH, SHIELD_REGISTRY_*, PAYMENT_ENV_MISMATCH, CAPTURED_PROOF_UNAVAILABLE). `message`/`docs` ALWAYS present. Unlike `LegacyControllerError` below — the per-endpoint PayPal/Stripe error shape — which never carries `message`/`docs`.

## LegacyControllerError {#schema-legacycontrollererror}

Per-endpoint PayPal/Stripe error body — the SAME shape the legacy (non-v1) API has always returned for these routes. Only `issue` is guaranteed; `message`/`docs` NEVER present (unlike `V1Error`). Some responses carry no `data` at all — see each response's own description for the exact body.

## LegacyStringError {#schema-legacystringerror}

Bare `{error:string}` body with no `data` at all — unlike `LegacyGateError` (same shape) and `V1Error`/`LegacyControllerError` (which carry `data`). Used for shared config/internal errors: "Shield misconfigured" (Stripe pk/sk key mode mismatch), "Internal server error" (unknown error), "Shield unavailable" (shield has no secret configured on a PayPal route).

## StripeErrorPassthrough {#schema-stripeerrorpassthrough}

500 body on `/v1/stripe/create-intent` and `/v1/stripe/complete` when the underlying Stripe API call throws. `issue` is OPEN — it passes through Stripe's raw `error.code` (e.g. `card_declined`, `parameter_invalid_integer`, `amount_too_small`) when Stripe sets one, falling back to the literal string `STRIPE_ERROR` only when it doesn't. NOT a member of the closed `V1Issue` enum — do not validate `issue` against that enum for this response.

## LegacyGateError {#schema-legacygateerror}

Bare body from the Stripe pk-resolution gate and the PayPal hostname gate — `error` here is a STRING, not `true`, and there is no `data`/`issue` at all. Covers Stripe MISSING_PK/UNKNOWN_PK/PK_POOL_MISMATCH and PayPal's unknown-host/no-secret gate; the issue names in each response's description are documentation only, never present in the actual body.

## V1Issue {#schema-v1issue}

Every issue code a v1 response can carry. The first group (SHIELD_KEY_*, SCOPE_DENIED, ORIGIN_NOT_ALLOWED, POOL_MISMATCH, SHIELD_REGISTRY_*, SHIELD_ENV_MISCONFIGURED, PAYMENT_ENV_MISMATCH, SIGNING_UNAVAILABLE, LEGACY_API_DISABLED, CAPTURED_PROOF_UNAVAILABLE) is the shared v1 auth/registry/signing table (the shared V1_ISSUE_STATUS table — contract-tested to match 1:1). The remaining codes are per-endpoint validation/business issues returned by the underlying PayPal/Stripe controllers (the same codes the legacy API returns for the same endpoint). EXCEPTION: `STRIPE_ERROR` is listed here as the literal fallback value, but the Stripe create-intent/complete 500 response is NOT closed against this enum — see `StripeErrorPassthrough`, where `issue` is documented as Stripe's own open `error.code`.

## SignedEnvelope {#schema-signedenvelope}

v1 success body on `/v1/capture_order` and `/v1/stripe/complete` (the shared v1 envelope builder). Carries only Ed25519 proof — never the legacy `signature`/`signed_fields`/`payload_signature` fields. Verify `sig` against the exact bytes of `signed_payload` (a JSON string, not an object — do not re-serialize it) using the public key for `kid` looked up from `/v1/keys`.

## PaypalCaptureV1Payload {#schema-paypalcapturev1payload}

Parsed shape of `signed_payload` for `/v1/capture_order` (the `extra` fields from the PayPal capture signed-payload builder plus the shared v1 envelope builder). Every field below is signed — none can be altered without invalidating `sig`.

## StripeAddress {#schema-stripeaddress}

Postal address sub-shape inside `Contact`. UNSIGNED (display only). Mirrors Stripe's raw address object; any field may be `null` when Stripe didn't collect it.

## Contact {#schema-contact}

Buyer contact/shipping info returned alongside a Stripe v1 envelope. UNSIGNED — NOT part of the verified proof, do not use it for fraud/compliance decisions. `name`/`address` prefer SHIPPING details, falling back to billing; `email`/`phone` prefer BILLING (email falls back to `receipt_email`). On a v1 replay, `wallet_type` may fall back to the client-supplied request value — treat that as a client hint, not a trusted value, on replay calls.

## StripeCompleteV1Payload {#schema-stripecompletev1payload}

Parsed shape of `signed_payload` for `/v1/stripe/complete` (flat fields plus every `meta_*` key). Every `properties` entry below plus every `meta_*` key is SIGNED (tamper-proofed); verifying `sig` against `signed_payload` proves no field was altered after the shield produced this envelope.
SECURITY BOUNDARY (what a verifier MUST assert): `partner_id` + `key_id` (∈ the key ids you authorize) + `env` + `invoice_id` + `amount_received` + `id` (anti-replay — reject if already settled) + `status === 'succeeded'` + `iat` within [-600s, +300s] of now. `store`, `store_domain`, `reference_id`, and every `meta_*` key are all CLIENT-DECLARED at create-time (supplied by the SDK/caller; the server only flattens and signs them for tamper-proofing, never independently verifies their content) — INFORMATIONAL/audit/display ONLY, never use them for the binding decisions above.

## Jwks {#schema-jwks}

Set of Ed25519 OKP JWKs per RFC 8037, served by `/v1/keys`.
