{
  "openapi": "3.1.0",
  "info": {
    "title": "Verification API",
    "version": "2.0.0",
    "summary": "Hosted verification sessions backing the Valyd SDK — two products: the Unique Human API (non-account hosted checks: live + unique human) and Reusable Verification (a connected user’s workflow session, all checks, reusable proofs).",
    "description": "REST API backing the Valyd SDK (`/api/v2`). The SDK is the integration surface. Verification always runs inside a **hosted session** on Valyd’s verification page — there are no direct per-check endpoints. Two products:\n\n- **Unique Human API** — API-key only, no user account. Create a session (no user token) for a workflow containing the live-human and/or face-uniqueness checks, redirect the person to the returned `url`, then read the verdict from `GET /api/v2/session/{id}/decision`. Nothing is saved to an account.\n- **Reusable Verification** — the user connects with Valyd (standard OIDC); you create a verification session with their `valyd_access_token` running a configured workflow (ID/KYC, face match, age, professional licence, location, live-human checks). Passed proofs save to the user’s Valyd ID for reuse; PII stays encrypted with Valyd — you receive proofs, never raw identity data. The redirect-back `?status=` is a hint only — pull the authoritative result from the decision or a signed webhook.\n\nSuccessful responses are `{ success: true, data }` — there is no `error` key on success. Error responses are `{ success: false, data, error }` where `data` is `[]` (an empty array) or a populated object, never `null`. Authenticate with `X-API-Key: <App API key>` (a Bearer token with the same key is also accepted). API keys are created by a human in the Developer Portal — not via API.\n\nAll `/api/v2` routes are rate limited per client IP at ~150 requests/minute. A 429 body is `{ success: false, data: [], error: { code: \"rate_limited\", message } }` and carries no `Retry-After` header; successful responses include `X-RateLimit-Limit` / `X-RateLimit-Remaining`. POST endpoints accept an optional `Idempotency-Key` request header so a retry cannot double-run or double-charge.",
    "contact": {
      "name": "Valyd Developer Docs",
      "url": "https://docs.valyd.vip/verify/intro.md"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://docs.valyd.vip"
    }
  },
  "servers": [
    {
      "url": "https://idp.valyd.vip",
      "description": "Verification APIs host"
    }
  ],
  "tags": [
    {
      "name": "Sessions",
      "description": "Verification sessions and their decisions — create a session for a workflow, redirect the user to the hosted verification page, then read the authoritative decision."
    },
    {
      "name": "Workflows",
      "description": "Feature bundles for verification sessions, managed via a full REST resource (`WorkflowController`). Create a workflow from a list of `features`, then pass its `id` as `workflow_id` when creating a session."
    },
    {
      "name": "Credentials",
      "description": "Professional licence discovery for configuring a credential check in a workflow. **Always discover first:** GET /credential/states → pick a state → GET /credential/states/{state}/providers → take that provider’s `provider_code` + `credential_name`. Never hard-code a provider_code or guess a licence type — the board decides what it issues."
    },
    {
      "name": "Identity",
      "description": "Read back a previously-verified (\"verify once, reuse\") identity for a returning user, scoped to your project, and manage the face-uniqueness registry (GDPR unlink) — without starting a new session."
    }
  ],
  "security": [
    {
      "apiKeyAuth": []
    },
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/v2/session": {
      "post": {
        "tags": [
          "Sessions"
        ],
        "summary": "Create a verification session",
        "description": "Creates a hosted session and returns a Valyd-hosted `url` to redirect the user to. `workflow_id` is the UUID of a workflow (from the Developer Portal or the Workflows API) that selects the bundle of features to run. If you pass a `callback`, it must EXACTLY match an active, approved HTTPS webhook registered for the project, or you get 422 `callback_not_allowed`. Pass `valyd_access_token` to bind the session to a connected user's account (Reusable Verification).",
        "operationId": "createSession",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSessionRequest"
              },
              "example": {
                "workflow_id": "3f1c9e2a-6b4d-4a1e-9c2f-7d8e0a1b2c3d",
                "redirect_url": "https://app.example.com/verify/callback",
                "callback": "https://api.example.com/webhooks/valyd",
                "vendor_data": "user_123",
                "ttl_seconds": 900,
                "metadata": {
                  "plan": "pro"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "session_id": "ses_abc123",
                    "status": "NOT_STARTED",
                    "url": "https://idp.valyd.vip/s/abc123",
                    "session_token": "stk_xyz",
                    "features": [
                      "id_verification",
                      "liveness",
                      "face_match",
                      "credential"
                    ],
                    "redirect_url": "https://app.example.com/verify/callback",
                    "expires_at": "2026-06-11T12:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "402": {
            "$ref": "#/components/responses/BillingRequired"
          },
          "404": {
            "$ref": "#/components/responses/WorkflowNotFound"
          },
          "422": {
            "$ref": "#/components/responses/CallbackNotAllowed"
          }
        }
      },
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "List sessions",
        "description": "Returns session summary objects newest-first (by `created_at`). This endpoint is NOT paginated: there is no cursor, page, or offset parameter and no total/next metadata. Use `limit` to cap the number returned (default 50; there is no server-enforced maximum). Narrow results with the `status` and `vendor_data` equality filters. `data` is an object with a `sessions` array.",
        "operationId": "listSessions",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/SessionStatus"
            },
            "description": "Filter by session status."
          },
          {
            "name": "vendor_data",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by your correlation ref (SDK arg `vendorData`)."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Max sessions to return (default 50; no cursor/offset/page pagination is available and no server-enforced maximum).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "An object with a `sessions` array of session summaries, newest-first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "sessions": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/SessionSummary"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          }
        }
      }
    },
    "/api/v2/session/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SessionId"
        }
      ],
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "Retrieve a session",
        "description": "Fetch a session's current state (status, workflow, vendor_data, timestamps). Poll this while the user works through a hosted flow. For the OUTCOME of each check — and the identity — use GET /session/{id}/decision instead.",
        "operationId": "getSession",
        "responses": {
          "200": {
            "description": "Session summary object.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSummaryEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/session/{id}/decision": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SessionId"
        }
      ],
      "get": {
        "tags": [
          "Sessions"
        ],
        "summary": "Get full decision and per-check data",
        "description": "Returns the authoritative final decision plus the per-check breakdown. Call this after a terminal webhook to retrieve all extracted data.",
        "operationId": "getSessionDecision",
        "responses": {
          "200": {
            "description": "Decision and per-check data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DecisionEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "session_id": "ses_abc123",
                    "status": "APPROVED",
                    "vendor_data": "user_123",
                    "valyd_id": null,
                    "origin": "fresh",
                    "decision": "APPROVED",
                    "checks": [
                      {
                        "type": "id_verification",
                        "status": "passed",
                        "score": 0.97,
                        "data": {},
                        "error": null
                      },
                      {
                        "type": "liveness",
                        "status": "passed",
                        "score": 1,
                        "data": {
                          "live_score": 1
                        },
                        "error": null
                      },
                      {
                        "type": "face_match",
                        "status": "passed",
                        "score": 0.97,
                        "data": {
                          "similarity": 0.97,
                          "threshold": 0.95
                        },
                        "error": null
                      },
                      {
                        "type": "credential",
                        "status": "passed",
                        "score": 1,
                        "data": {
                          "match": true,
                          "license": {
                            "status": "active",
                            "expires_at": "2027-01-01"
                          }
                        },
                        "error": null
                      }
                    ],
                    "verifications": {
                      "id_verification": {
                        "status": "passed"
                      },
                      "liveness": {
                        "status": "passed"
                      }
                    },
                    "identity": null,
                    "decided_at": "2026-06-11T12:05:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/session/{id}/status": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SessionId"
        }
      ],
      "patch": {
        "tags": [
          "Sessions"
        ],
        "summary": "Decide an IN_REVIEW session",
        "description": "The manual-decision endpoint for a session that is currently `IN_REVIEW`. It only decides an IN_REVIEW session — calling it on any other state returns 409 `review_not_pending`. Setting `APPROVED` additionally requires that id_verification, liveness and face_match have all passed, otherwise you get 409 `required_face_checks_incomplete`. The decision is terminal and the session stops accepting further checks. It is an audited action.",
        "operationId": "updateSessionStatus",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "APPROVED",
                      "DECLINED"
                    ],
                    "description": "REQUIRED. The terminal decision to force (e.g. APPROVED / DECLINED)."
                  }
                }
              },
              "example": {
                "status": "APPROVED"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated session summary reflecting the decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionSummaryEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/ReviewConflict"
          }
        }
      }
    },
    "/api/v2/identity": {
      "get": {
        "tags": [
          "Identity"
        ],
        "summary": "Read a reusable verified identity",
        "description": "On-demand read of a previously-verified (\"verify once, reuse\") identity, scoped to the calling project, without starting a new session. Look the record up by `valyd_id` (alias `pollus_id` — same value) OR by your own `vendor_data` — one of the two is REQUIRED; supplying neither returns 400 `missing_parameter`. Only an active record (not revoked, not expired) is returned; otherwise 404 `not_found`. The `identity` object carries a shareable `pseudonym`, `age_bands`, verified `licenses`, a `verified_at` timestamp and a `stale` flag — raw legal name and date of birth are never returned.",
        "operationId": "getIdentity",
        "parameters": [
          {
            "name": "valyd_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The user's public Valyd id. Provide this OR vendor_data. Alias: pollus_id (same value)."
          },
          {
            "name": "pollus_id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Back-compat alias of valyd_id (same value)."
          },
          {
            "name": "vendor_data",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Your own correlation ref stored at verification time. Used when valyd_id is not supplied."
          }
        ],
        "responses": {
          "200": {
            "description": "The reusable verified identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdentityEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "identity": {
                      "valyd_id": "valyd_9f2c8a1b4d5e4a1e9c2f7d8e0a1b2c3d",
                      "pseudonym": "swift-harbor-2417",
                      "age_bands": {
                        "is_18_plus": true,
                        "is_21_plus": true
                      },
                      "licenses": [
                        {
                          "license_type": "Medical Doctor",
                          "status": "verified",
                          "registry_status": "Active",
                          "license_number": "A12345",
                          "state": "CA",
                          "issuing_authority": "Medical Board of California",
                          "expires_on": "2027-01-01"
                        }
                      ],
                      "verified_at": "2026-06-11T12:05:00+00:00",
                      "stale": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/MissingParameter"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/face-uniqueness/{valyd_uuid}": {
      "parameters": [
        {
          "name": "valyd_uuid",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "The gallery face id to forget."
        }
      ],
      "delete": {
        "tags": [
          "Identity"
        ],
        "summary": "Forget a face (GDPR unlink)",
        "description": "Removes THIS project's link to a gallery face id. The project's active enrollment is marked unlinked; if the id is a shadow identity with no remaining active enrollments, its template and vector are hard-deleted. Returns `{ valyd_uuid, unlinked: true, deleted }`, where `deleted` is true only when the underlying identity was fully removed. 404 `not_found` when the id is unknown or this project has no active enrollment for it. Requires the `face_uniqueness` feature.",
        "operationId": "faceUniquenessUnlink",
        "responses": {
          "200": {
            "description": "The project's link to the face id was removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FaceUnlinkEnvelope"
                },
                "example": {
                  "success": true,
                  "data": {
                    "valyd_uuid": "valyd_9f2c8a1b4d5e4a1e9c2f7d8e0a1b2c3d",
                    "unlinked": true,
                    "deleted": false
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "403": {
            "$ref": "#/components/responses/FeatureNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/credential/states": {
      "get": {
        "tags": [
          "Credentials"
        ],
        "summary": "List states",
        "description": "**Step 1 of licence verification.** Lists every state/jurisdiction that has at least one licence provider wired up.\nTake a `state_code` from here and pass it to /credential/states/{state}/providers to see what that board actually issues.\n\nSDK: `verify.credentials.states()` → `[{ stateName, stateCode, providerCount, mappingCount }]`.",
        "operationId": "listCredentialStates",
        "responses": {
          "200": {
            "description": "States.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "states": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "state_name": {
                                "type": "string"
                              },
                              "state_code": {
                                "type": "string"
                              }
                            }
                          }
                        }
                      }
                    },
                    "error": {
                      "type": [
                        "object",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "states": [
                      {
                        "state_name": "California",
                        "state_code": "CA"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          }
        }
      }
    },
    "/api/v2/credential/states/{state}/providers": {
      "parameters": [
        {
          "name": "state",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "2-letter state code (e.g. CA)."
        }
      ],
      "get": {
        "tags": [
          "Credentials"
        ],
        "summary": "List providers for a state",
        "description": "**Step 2 — this is where `provider_code` comes from.** Lists what the state board issues. Show these as your \"licence type\" dropdown, then pass the SELECTED entry straight through to /credential-verification:\n\n- `provider_code` → send as `provider_code` (pins the exact board, no guessing)\n- `credential_name` → send as `license_type` (what the board calls it, e.g. \"Medical Doctor\")\n- `required_fields` → the inputs you must collect for this provider (e.g. `license_no`, `remote_code`)\n\nKey the dropdown on `credential_code`, NOT `provider_code`: one provider serves many credentials, so provider_code is not unique per row.\n\nSDK: `verify.credentials.providers(stateCode)` → `[{ providerCode, providerDisplayName, credentialCode, credentialName, stateCode, remoteCode, requiredFields }]`.",
        "operationId": "listStateProviders",
        "responses": {
          "200": {
            "description": "Providers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "providers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "provider_code": {
                                "type": "string"
                              },
                              "provider_display_name": {
                                "type": "string"
                              },
                              "credential_name": {
                                "type": "string"
                              },
                              "state_code": {
                                "type": "string"
                              },
                              "required_fields": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "error": {
                      "type": [
                        "object",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "providers": [
                      {
                        "provider_code": "MD",
                        "provider_display_name": "Medical Board of California",
                        "credential_name": "Physician & Surgeon",
                        "required_fields": [
                          "license_number"
                        ]
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v2/workflows": {
      "post": {
        "tags": [
          "Workflows"
        ],
        "summary": "Create a workflow",
        "description": "Bundles a set of `features` into a reusable workflow. Pass the returned workflow `id` as `workflow_id` when creating a hosted session. Adding `age` without `id_verification` auto-injects `id_verification`.",
        "operationId": "createWorkflow",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkflowCreateRequest"
              },
              "example": {
                "name": "KYC + License",
                "features": [
                  "id_verification",
                  "liveness",
                  "face_match",
                  "credential"
                ],
                "settings": {},
                "is_active": true
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Workflow created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          }
        }
      },
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "List workflows",
        "operationId": "listWorkflows",
        "description": "List your workflows — the reusable feature bundles a hosted session runs. Returns `data: { workflows: [...] }`; pass a workflow `id` as `workflow_id` when creating a session.",
        "responses": {
          "200": {
            "description": "Workflow list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "workflows": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Workflow"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          }
        }
      }
    },
    "/api/v2/workflows/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "Retrieve a workflow",
        "operationId": "getWorkflow",
        "description": "Fetch one workflow: its features and configuration. Returns `data: { workflow: {...} }`.",
        "responses": {
          "200": {
            "description": "Workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowEnvelope"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Workflows"
        ],
        "summary": "Update a workflow",
        "operationId": "updateWorkflow",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkflowUpdateRequest"
              },
              "example": {
                "name": "KYC + License (v2)",
                "is_active": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated workflow.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkflowEnvelope"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "description": "Update a workflow's features or settings. Applies to sessions created AFTER the change — sessions already running keep the pipeline they started with."
      },
      "delete": {
        "tags": [
          "Workflows"
        ],
        "summary": "Delete a workflow",
        "operationId": "deleteWorkflow",
        "responses": {
          "200": {
            "description": "Workflow deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/InvalidApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "description": "Delete a workflow. Sessions already created with it keep working; new sessions can no longer reference it."
      }
    }
  },
  "webhooks": {
    "verificationEvent": {
      "post": {
        "operationId": "verificationEvent",
        "summary": "Verification event (terminal session state)",
        "description": "Valyd POSTs this to your app-level or per-session `callback` URL when a session reaches a terminal state. Verify the signature before trusting it: the `X-Valyd-Signature` header is the lowercase hex `HMAC-SHA256` of `\"{timestamp}.{rawBody}\"` keyed by your webhook signing secret. Reject timestamps older than 300s and dedupe on `X-Valyd-Event-Id`. The webhook is a notification only — call the decision endpoint for full data.",
        "parameters": [
          {
            "name": "X-Valyd-Timestamp",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "Unix seconds when the event was signed."
          },
          {
            "name": "X-Valyd-Event-Id",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "Unique event id; equals body `event_id`. Use for idempotency."
          },
          {
            "name": "X-Valyd-Signature",
            "in": "header",
            "schema": {
              "type": "string"
            },
            "description": "Lowercase hex HMAC-SHA256 signature."
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              },
              "example": {
                "event_id": "evt_123",
                "type": "verification.approved",
                "session_id": "ses_abc",
                "status": "APPROVED",
                "vendor_data": "user_123",
                "decision": "approved",
                "occurred_at": "2026-06-11T12:05:00Z"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Return any 2xx quickly to acknowledge. Non-2xx is retried with exponential backoff."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Your App API key from the Developer Portal."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same App API key may be sent as a Bearer token."
      }
    },
    "parameters": {
      "SessionId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Session id (e.g. ses_abc123)."
      }
    },
    "schemas": {
      "LicenseBadge": {
        "type": "object",
        "description": "A normalised verified-licence badge. Empty fields are dropped, so any field may be absent.",
        "properties": {
          "license_type": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "Our verification outcome (e.g. \"verified\")."
          },
          "registry_status": {
            "type": "string",
            "description": "The board's own status text (e.g. \"Active\")."
          },
          "license_number": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "first_name": {
            "type": "string"
          },
          "middle_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "npi": {
            "type": "string"
          },
          "issuing_authority": {
            "type": "string"
          },
          "issued_on": {
            "type": "string"
          },
          "expires_on": {
            "type": "string"
          },
          "provider_url": {
            "type": "string"
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IdentityRecord": {
        "type": "object",
        "description": "A reusable verified identity as returned to integrators — pseudonym + proofs only, never raw legal name or date of birth.",
        "properties": {
          "valyd_id": {
            "type": "string",
            "description": "The user's public Valyd id (the user's uuid)."
          },
          "pseudonym": {
            "type": "string",
            "nullable": true,
            "description": "Stable, shareable pseudonymous handle resolved from the identity core; null if the user has none."
          },
          "age_bands": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Stored age-band proofs (e.g. { \"is_18_plus\": true, \"is_21_plus\": true })."
          },
          "licenses": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LicenseBadge"
            },
            "description": "Verified professional-licence badges."
          },
          "verified_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the identity was verified / last refreshed (ISO 8601)."
          },
          "stale": {
            "type": "boolean",
            "description": "true when the cached identity-core source changed and the integrator should re-verify."
          }
        }
      },
      "IdentityEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "identity": {
                "$ref": "#/components/schemas/IdentityRecord"
              }
            }
          }
        }
      },
      "CaptureEnv": {
        "type": "object",
        "description": "Optional client-declared capture environment. When `virtual_suspect` is true a virtual camera (OBS / ManyCam / etc.) is assumed and the check fails closed with signal `virtual_camera`. Client-declared: a tampered client can omit or falsify it, so it only catches the honest virtual-camera case.",
        "properties": {
          "virtual_suspect": {
            "type": "boolean",
            "description": "The client's judgement that the camera stream came from a virtual / injected device."
          },
          "camera_label": {
            "type": "string",
            "description": "The device label the client observed (truncated to 120 chars in the failure record)."
          }
        }
      },
      "AntispoofCheck": {
        "type": "object",
        "description": "The anti-spoof check result. `score` carries the same 0-100 human_score as `data.human_score`.",
        "properties": {
          "type": {
            "type": "string",
            "example": "antispoof"
          },
          "status": {
            "$ref": "#/components/schemas/CheckStatus"
          },
          "score": {
            "type": "number",
            "nullable": true,
            "description": "The 0-100 human_score."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "human_score": {
                "type": "number",
                "description": "Vendor-neutral liveness confidence, 0-100."
              },
              "assurance": {
                "type": "string",
                "enum": [
                  "upload",
                  "burst"
                ],
                "description": "\"upload\" for a single image, \"burst\" for a 3-8 frame sequence."
              },
              "frames_received": {
                "type": "integer"
              },
              "frames_analyzed": {
                "type": "integer"
              },
              "frames_genuine": {
                "type": "integer"
              },
              "frames_spoof": {
                "type": "integer"
              },
              "frames_no_face": {
                "type": "integer"
              },
              "duplicate_frames": {
                "type": "integer"
              },
              "challenge": {
                "type": "string",
                "description": "The commanded gesture, present when a challenge_id was supplied."
              },
              "challenge_performed": {
                "type": "boolean",
                "description": "Whether the burst showed the commanded gesture (present only when a challenge was required)."
              },
              "signal": {
                "type": "string",
                "description": "Machine-readable failure reason on a failed check (e.g. spoof_detected, no_face, virtual_camera)."
              },
              "identity": {
                "type": "object",
                "additionalProperties": true,
                "description": "Present ONLY on POST /antispoof/identity, and only when liveness passed: the resolved gallery identity — { valyd_uuid, registered } on success, or { error } if the gallery lookup failed.",
                "properties": {
                  "valyd_uuid": {
                    "type": "string",
                    "description": "Stable global face id."
                  },
                  "registered": {
                    "type": "string",
                    "enum": [
                      "new",
                      "existing"
                    ],
                    "description": "Whether the face was newly enrolled or already known."
                  },
                  "error": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable failure reason, or null on pass."
          }
        }
      },
      "AntispoofEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "description": "Synchronous anti-spoof response. `data.check` carries the anti-spoof result.",
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "session_id": {
                "type": "string"
              },
              "status": {
                "$ref": "#/components/schemas/CheckStatus"
              },
              "check": {
                "$ref": "#/components/schemas/AntispoofCheck"
              }
            }
          }
        }
      },
      "LivenessChallengeEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "challenge_id": {
                "type": "string",
                "format": "uuid",
                "description": "Opaque single-use id to echo back on the run as challenge_id."
              },
              "challenge": {
                "type": "string",
                "enum": [
                  "turn_left",
                  "turn_right",
                  "open_mouth",
                  "nod",
                  "come_closer"
                ],
                "description": "The gesture the burst must show."
              },
              "expires_in": {
                "type": "integer",
                "description": "Seconds until the challenge expires (60)."
              }
            }
          }
        }
      },
      "FaceUniquenessCheck": {
        "type": "object",
        "description": "The face-uniqueness check result. `score` is always null — no similarity score is exposed.",
        "properties": {
          "type": {
            "type": "string",
            "example": "face_uniqueness"
          },
          "status": {
            "$ref": "#/components/schemas/CheckStatus"
          },
          "score": {
            "type": "number",
            "nullable": true,
            "description": "Always null."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "properties": {
              "valyd_uuid": {
                "type": "string",
                "description": "Stable global face id — the same value for this face across every integrator."
              },
              "registered": {
                "type": "string",
                "enum": [
                  "new",
                  "existing"
                ],
                "description": "\"new\" if the face was just enrolled, \"existing\" if already in the gallery."
              },
              "binding": {
                "type": "string",
                "enum": [
                  "binding"
                ],
                "description": "\"binding\" — the enrollment was written to the live gallery."
              },
              "code": {
                "type": "string",
                "description": "Machine-readable failure code on a failed check (e.g. no_face, uniqueness_failed)."
              }
            }
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable failure reason, or null on pass."
          }
        }
      },
      "FaceUniquenessEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "description": "Synchronous face-uniqueness response. `data.check` carries the result.",
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "session_id": {
                "type": "string"
              },
              "status": {
                "$ref": "#/components/schemas/CheckStatus"
              },
              "check": {
                "$ref": "#/components/schemas/FaceUniquenessCheck"
              }
            }
          }
        }
      },
      "FaceUnlinkEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "valyd_uuid": {
                "type": "string"
              },
              "unlinked": {
                "type": "boolean"
              },
              "deleted": {
                "type": "boolean",
                "description": "true only when the underlying (shadow) identity had no other active links and was hard-deleted."
              }
            }
          }
        }
      },
      "SessionStatus": {
        "type": "string",
        "enum": [
          "NOT_STARTED",
          "IN_PROGRESS",
          "IN_REVIEW",
          "APPROVED",
          "DECLINED",
          "ABANDONED",
          "EXPIRED"
        ],
        "description": "APPROVED, DECLINED, ABANDONED, and EXPIRED are terminal (a webhook is sent)."
      },
      "CheckStatus": {
        "type": "string",
        "enum": [
          "pending",
          "running",
          "passed",
          "failed",
          "review"
        ],
        "description": "Lifecycle: pending → running → passed | failed | review. statuses.md lists only the three terminal values; hosted.md adds pending/running."
      },
      "CheckType": {
        "type": "string",
        "enum": [
          "id_verification",
          "liveness",
          "face_match",
          "age",
          "credential"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "data": {
            "description": "Empty array `[]` on most errors, or a populated object for some codes (e.g. `review_not_pending` returns `{ status }`). Never null.",
            "oneOf": [
              {
                "type": "array"
              },
              {
                "type": "object"
              }
            ]
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "validation_error",
                  "API_KEY_MISSING",
                  "invalid_api_key",
                  "not_found",
                  "workflow_not_found",
                  "callback_not_allowed",
                  "valyd_login_required",
                  "review_not_pending",
                  "required_face_checks_incomplete",
                  "invalid_image",
                  "missing_parameter",
                  "feature_not_allowed",
                  "too_many_frames",
                  "invalid_frames",
                  "frames_required",
                  "challenge_required",
                  "challenge_expired",
                  "account_dob_unavailable",
                  "rate_limited",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "CreateSessionRequest": {
        "type": "object",
        "required": [
          "workflow_id"
        ],
        "properties": {
          "workflow_id": {
            "type": "string",
            "format": "uuid",
            "description": "REQUIRED. The UUID of the workflow (feature bundle) to run, from the Developer Portal or the Workflows API."
          },
          "valyd_access_token": {
            "type": "string",
            "maxLength": 4096,
            "description": "Optional. Binds the session to a connected user's account (Reusable Verification). sso/reuse workflows require it — a missing token on those returns 401 `valyd_login_required`."
          },
          "redirect_url": {
            "type": "string",
            "format": "uri",
            "description": "Where Valyd redirects the user's browser after the flow."
          },
          "callback": {
            "type": "string",
            "format": "uri",
            "description": "Per-session webhook URL."
          },
          "vendor_data": {
            "type": "string",
            "description": "Your internal correlation ref; echoed on webhooks and the decision."
          },
          "ttl_seconds": {
            "type": "integer",
            "description": "Session time-to-live in seconds."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Arbitrary key/values."
          }
        }
      },
      "Session": {
        "type": "object",
        "properties": {
          "session_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/SessionStatus"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Hosted capture URL to redirect the user to."
          },
          "session_token": {
            "type": "string"
          },
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CheckType"
            }
          },
          "redirect_url": {
            "type": "string",
            "format": "uri"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SessionEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "description": "Success envelope — success responses carry only `success` + `data` (no `error` key).",
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Session"
          }
        }
      },
      "SessionSummary": {
        "type": "object",
        "description": "The session summary shape returned by list, get, decision-status and update-status. It has NO `url` or `session_token`.",
        "properties": {
          "session_id": {
            "type": "string"
          },
          "workflow_id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/SessionStatus"
          },
          "mode": {
            "type": "string"
          },
          "vendor_data": {
            "type": "string",
            "nullable": true
          },
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CheckType"
            }
          },
          "decision": {
            "type": "string",
            "nullable": true,
            "description": "Terminal decision status string, or null while not decided."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SessionSummaryEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/SessionSummary"
          }
        }
      },
      "Check": {
        "type": "object",
        "properties": {
          "type": {
            "$ref": "#/components/schemas/CheckType"
          },
          "status": {
            "$ref": "#/components/schemas/CheckStatus"
          },
          "score": {
            "type": "number",
            "description": "Per-check confidence."
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "Per-check details; shape varies by check type."
          },
          "error": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Decision": {
        "type": "object",
        "properties": {
          "session_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/SessionStatus"
          },
          "vendor_data": {
            "type": "string",
            "nullable": true
          },
          "valyd_id": {
            "type": "string",
            "nullable": true,
            "description": "Linked Valyd account id, or null for a fresh (non-Valyd) session."
          },
          "origin": {
            "type": "string",
            "enum": [
              "managed",
              "fresh"
            ],
            "description": "`managed` for a Valyd-linked session, `fresh` otherwise."
          },
          "decision": {
            "type": "string",
            "description": "The final decision as a single status string (e.g. \"APPROVED\"), normalised from the session decision."
          },
          "checks": {
            "type": "array",
            "description": "Per-check results (SDK array shape).",
            "items": {
              "$ref": "#/components/schemas/Check"
            }
          },
          "verifications": {
            "type": "object",
            "additionalProperties": true,
            "description": "Per-check outcomes keyed by check type."
          },
          "identity": {
            "type": "object",
            "nullable": true,
            "additionalProperties": true,
            "description": "Verified profile + licenses for a Valyd-linked session; null otherwise."
          },
          "decided_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "DecisionEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Decision"
          }
        }
      },
      "Workflow": {
        "type": "object",
        "description": "A reusable feature bundle for hosted sessions.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Enabled features (from `config('verify.features')`, e.g. id_verification, liveness, face_match, age, credential, location, antispoof, face_uniqueness)."
          },
          "settings": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "is_active": {
            "type": "boolean"
          }
        }
      },
      "WorkflowCreateRequest": {
        "type": "object",
        "required": [
          "name",
          "features"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "REQUIRED."
          },
          "features": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "REQUIRED. At least one feature. Each value must be one of `config('verify.features')` (id_verification, liveness, face_match, age, credential, location, antispoof, face_uniqueness, …). Adding `age` without `id_verification` auto-injects id_verification."
          },
          "settings": {
            "type": "object",
            "additionalProperties": true,
            "description": "Optional per-workflow settings."
          },
          "is_active": {
            "type": "boolean",
            "description": "Optional. Defaults to active."
          }
        }
      },
      "WorkflowUpdateRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "features": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "settings": {
            "type": "object",
            "additionalProperties": true
          },
          "is_active": {
            "type": "boolean"
          }
        }
      },
      "WorkflowEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "data": {
            "type": "object",
            "properties": {
              "workflow": {
                "$ref": "#/components/schemas/Workflow"
              }
            }
          }
        }
      },
      "WebhookPayload": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Equals the X-Valyd-Event-Id header; dedupe key."
          },
          "type": {
            "type": "string",
            "enum": [
              "verification.approved",
              "verification.declined",
              "verification.in_review",
              "verification.abandoned",
              "verification.expired"
            ]
          },
          "session_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/SessionStatus"
          },
          "vendor_data": {
            "type": "string"
          },
          "decision": {
            "description": "A short status string (e.g. \"approved\"), consistent with the decision API which now returns `decision` as a single status string. [owner: confirm the exact webhook payload against the queue-worker dispatcher (idp-queue.service) before relying on an object form.]",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "$ref": "#/components/schemas/Decision"
              }
            ]
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "MissingParameter": {
        "description": "A required query parameter was not supplied.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "missing_parameter",
                "message": "Provide valyd_id or vendor_data."
              }
            }
          }
        }
      },
      "FeatureNotAllowed": {
        "description": "The feature this endpoint needs is not enabled for the project.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "feature_not_allowed",
                "message": "Feature 'antispoof' is not enabled for this project."
              }
            }
          }
        }
      },
      "LivenessBadRequest": {
        "description": "The submitted capture was rejected before scoring. Codes: `invalid_image` (no image/frames), `too_many_frames` (more than 8 frames), `invalid_frames` (exactly 2 frames on face-uniqueness), `frames_required` (a strict-liveness project needs a 3-8 frame burst), `challenge_required` / `challenge_expired` (the challenge_id was missing, already used, or expired).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "invalid_image",
                "message": "Provide `image` (file or base64) or `frames[]` (3-8 burst stills)."
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Malformed body or missing required field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "validation_error",
                "message": "Missing required field: workflow_id"
              }
            }
          }
        }
      },
      "InvalidApiKey": {
        "description": "Missing or invalid API key. A completely missing key uses code `API_KEY_MISSING` ('API key is required. Provide X-API-Key or Authorization: Bearer.'); an invalid key uses `invalid_api_key`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "API_KEY_MISSING",
                "message": "API key is required. Provide X-API-Key or Authorization: Bearer."
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Session or resource does not exist.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "not_found",
                "message": "Session not found"
              }
            }
          }
        }
      },
      "WorkflowNotFound": {
        "description": "The referenced workflow_id does not exist for this project.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "workflow_not_found",
                "message": "Workflow not found"
              }
            }
          }
        }
      },
      "CallbackNotAllowed": {
        "description": "The `callback` URL does not exactly match an active, approved HTTPS webhook registered for the project.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "callback_not_allowed",
                "message": "Callback URL is not an approved webhook for this project."
              }
            }
          }
        }
      },
      "BillingRequired": {
        "description": "Billing could not cover the requested session/checks.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "internal_error",
                "message": "Payment required."
              }
            }
          }
        }
      },
      "ReviewConflict": {
        "description": "The session is not in a state that can be decided. `review_not_pending` (session is not IN_REVIEW) returns `data: { status }`; `required_face_checks_incomplete` is returned when approving before id_verification, liveness and face_match have all passed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": {
                "status": "APPROVED"
              },
              "error": {
                "code": "review_not_pending",
                "message": "Session is not pending review."
              }
            }
          }
        }
      },
      "InvalidImage": {
        "description": "A required image was missing or could not be read.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "invalid_image",
                "message": "Could not read image"
              }
            }
          }
        }
      },
      "AccountDobUnavailable": {
        "description": "Account mode was requested but the resolved Valyd account has no stored date of birth.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "account_dob_unavailable",
                "message": "Account has no stored date of birth."
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. All `/api/v2` routes are throttled per client IP at ~150 requests/minute. The 429 body carries NO `Retry-After` or `X-RateLimit-*` headers.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "data": [],
              "error": {
                "code": "rate_limited",
                "message": "Too many requests"
              }
            }
          }
        }
      }
    }
  }
}
