{
  "openapi": "3.1.0",
  "info": {
    "title": "VerifyMe Public API",
    "version": "1.0.0",
    "summary": "Create hosted KYC / onboarding requests, receive signed webhooks, fetch results.",
    "description": "VerifyMe hosts the whole applicant journey on a link. Your server creates an **onboarding request**, sends the applicant to the returned `onboardingUrl`, and learns the outcome through **signed webhooks** (preferred) or by polling.\n\n**Authentication.** Send `Authorization: Bearer <api key>` (`vm_test_xxxxxxxx_<secret>` in sandbox, `vm_live_...` in production; a key of the other environment is rejected), or exchange the key prefix + secret for a one-hour OAuth token via `POST /v1/oauth/token` (client_credentials) and send that as the bearer. Every key carries scopes.\n\n**Idempotency.** `POST /v1/onboarding-requests` requires an `Idempotency-Key` header (8-120 chars `[A-Za-z0-9_.:-]`). Same key + same body replays the original response (`Idempotent-Replayed: true`); same key + different body returns `409 IDEMPOTENCY_KEY_REUSED`. Keys are scoped per tenant and endpoint and kept for the configured TTL (default 24h).\n\n**Errors.** Every error uses one envelope: `{\"error\": {\"code\", \"message\", \"details?\", \"requestId?\", \"traceId\"}}`. Quote `traceId` to support.\n\n**Rate limits.** Per API key, per minute (default 600). Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; `429` carries `Retry-After`.\n\n**Data minimisation.** `result` returns masked identifiers (PAN, Aadhaar, account) and masked contact details. Documents are never inlined: you get a short-lived (120s) signed URL.\n\n**Sandbox vs production.** Sandbox uses mock providers and echoes OTP codes to the applicant UI. Production never does. Pricing shown by `/v1/usage` is indicative.",
    "contact": {
      "name": "VerifyMe support"
    }
  },
  "servers": [
    {
      "url": "https://api.example.com",
      "description": "Your deployment (set BASE_URL)"
    },
    {
      "url": "http://localhost:8080",
      "description": "Local sandbox"
    }
  ],
  "tags": [
    {
      "name": "Auth"
    },
    {
      "name": "Onboarding requests"
    },
    {
      "name": "Flows"
    },
    {
      "name": "Webhook endpoints"
    },
    {
      "name": "Usage"
    },
    {
      "name": "Utility"
    },
    {
      "name": "Verification",
      "description": "Standalone verification APIs, charged per call."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "oauth2": [
        "onboarding.read"
      ]
    }
  ],
  "paths": {
    "/v1/oauth/token": {
      "post": {
        "tags": [
          "Auth"
        ],
        "operationId": "createOAuthToken",
        "summary": "Exchange API key credentials for a bearer token",
        "security": [],
        "description": "OAuth2 client_credentials. `client_id` is the key prefix (e.g. `vm_test_AbCdEfGh`), `client_secret` is the secret part of the key. The token inherits the key's scopes and IP allow-list and lives 3600 seconds. Body is `application/x-www-form-urlencoded`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/OAuthTokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token issued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OAuthTokenResponse"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "no-store"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/ping": {
      "get": {
        "tags": [
          "Utility"
        ],
        "operationId": "ping",
        "summary": "Check credentials",
        "description": "Needs no particular scope. Returns the environment and the scopes granted to the credential.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ping"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/onboarding-requests": {
      "post": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "createOnboardingRequest",
        "summary": "Create an onboarding request",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.create"
            ]
          }
        ],
        "description": "Creates the request, resolves the live version of the flow (the tenant default unless `flowKey` is given), and returns the single-use-link `onboardingUrl`. The link token is shown only here and on `reissue-link`; only its hash is stored. Meters one `ONBOARDING_REQUEST` on creation. `onboarding.created` is emitted.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_.:-]{8,120}$"
            },
            "example": "order-81723-kyc"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOnboardingRequest"
              },
              "example": {
                "externalReference": "CUST-10042",
                "applicantType": "merchant",
                "mobile": "9876543210",
                "email": "asha@example.com",
                "returnUrl": "https://app.example.com/kyc/done",
                "metadata": {
                  "orderId": "81723"
                },
                "expiresInHours": 72,
                "locale": "en"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestCreated"
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "description": "Present (and `true`) when this is a replay of an earlier call.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "true"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "402": {
            "description": "VERIFYME_INSUFFICIENT_BALANCE: the prepaid wallet cannot cover this request. Top up, then retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "listOnboardingRequests",
        "summary": "List onboarding requests",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.read"
            ]
          }
        ],
        "description": "Newest first. `externalReference` is a substring match; it also matches an exact `requestId` or a mobile number (matched via a keyed hash, so the number is never stored searchable in clear).",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Comma-separated list of statuses.",
            "schema": {
              "type": "string"
            },
            "example": "SUBMITTED,REVIEWING"
          },
          {
            "name": "externalReference",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Created at or after (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Created at or before (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/onboarding-requests/{requestId}": {
      "get": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "getOnboardingRequest",
        "summary": "Get status and progress",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.read"
            ]
          }
        ],
        "description": "Another tenant's request id is indistinguishable from a missing one (404).",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Request id",
            "schema": {
              "type": "string"
            },
            "example": "req_01H..."
          }
        ],
        "responses": {
          "200": {
            "description": "Status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/onboarding-requests/{requestId}/result": {
      "get": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "getOnboardingResult",
        "summary": "Get the verification result",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.result"
            ]
          }
        ],
        "description": "Available once the applicant has submitted (`SUBMITTED`, `REVIEWING`, `REVERTED`, `APPROVED`, `REJECTED`); before that, `409 RESULT_NOT_READY`. Identifiers are masked. Every read is audited. Use `fields` to ask for a subset (data minimisation).",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Request id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "description": "Comma-separated subset of `verified` keys, e.g. `fullName,pan`.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingResult"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "no-store"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/onboarding-requests/{requestId}/documents/{documentId}": {
      "get": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "getDocumentAccess",
        "summary": "Get a short-lived signed URL for a document",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.result"
            ]
          }
        ],
        "description": "Documents are stored encrypted. The URL is valid 120 seconds and is HMAC-signed over key, expiry, content type and disposition. Only documents with a clean malware scan are served. Every call is audited.",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Request id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "documentId",
            "in": "path",
            "required": true,
            "description": "Document id from the result",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signed URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DocumentAccess"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/onboarding-requests/{requestId}/cancel": {
      "post": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "cancelOnboardingRequest",
        "summary": "Cancel a request",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.cancel"
            ]
          }
        ],
        "description": "Idempotent for already-cancelled requests. Revokes applicant sessions and the link stops working. Terminal states (`APPROVED`, `REJECTED`, `EXPIRED`) return `409 VERIFYME_INVALID_STATE`. Emits `onboarding.cancelled`.",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Request id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Updated status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OnboardingRequestStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/onboarding-requests/{requestId}/reissue-link": {
      "post": {
        "tags": [
          "Onboarding requests"
        ],
        "operationId": "reissueOnboardingLink",
        "summary": "Issue a fresh link (suspected leak / lost link)",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.create"
            ]
          }
        ],
        "description": "Rotates the link token and revokes all applicant sessions; evidence already captured is kept. Not allowed in `SUBMITTED`, `REVIEWING` or terminal states (`409 VERIFYME_INVALID_STATE`). The expiry is not extended. Raises a medium security event.",
        "parameters": [
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "description": "Request id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReissuedLink"
                }
              }
            },
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "example": "no-store"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/flows": {
      "get": {
        "tags": [
          "Flows"
        ],
        "operationId": "listFlows",
        "summary": "List live flows",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "onboarding.read"
            ]
          }
        ],
        "description": "Published flows of the tenant, to choose a `flowKey` for request creation. Flows are authored in the portal (draft, validate, publish).",
        "responses": {
          "200": {
            "description": "Flows",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints": {
      "post": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "createWebhookEndpoint",
        "summary": "Register a webhook endpoint",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "webhook.manage"
            ]
          }
        ],
        "description": "The signing secret (`whsec_...`) is returned **once**. In production the URL must be https; in all modes it must not resolve to a private, loopback, link-local or metadata address (re-checked at every delivery). Up to 10 endpoints per tenant.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookEndpoint"
              },
              "example": {
                "url": "https://api.acme.example/hooks/verifyme",
                "events": [
                  "onboarding.submitted",
                  "onboarding.approved",
                  "onboarding.rejected"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "get": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "listWebhookEndpoints",
        "summary": "List endpoints",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "webhook.manage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints/{id}": {
      "patch": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "updateWebhookEndpoint",
        "summary": "Update URL, events or status",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "webhook.manage"
            ]
          }
        ],
        "description": "Setting `status` to `ACTIVE` resets the failure counter (use it to re-enable an endpoint that was auto-disabled after 20 consecutive failed deliveries).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Endpoint id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookEndpoint"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete an endpoint",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "webhook.manage"
            ]
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Endpoint id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-endpoints/{id}/test": {
      "post": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "testWebhookEndpoint",
        "summary": "Send a `webhook.test` event",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "webhook.manage"
            ]
          }
        ],
        "description": "Queues a signed `webhook.test` delivery to this endpoint only. Check the outcome with `GET /v1/webhook-events`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Endpoint id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryQueued"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/webhook-events": {
      "get": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "listWebhookDeliveries",
        "summary": "Delivery log",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "webhook.manage"
            ]
          }
        ],
        "description": "Recent deliveries (max 50) with attempt counts, HTTP status and next retry, plus the list of event types.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "PENDING",
                "SUCCESS",
                "DEAD"
              ]
            }
          },
          {
            "name": "endpointId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deliveries",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookDeliveryList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": [
          "Usage"
        ],
        "operationId": "getUsage",
        "summary": "Usage and estimated charges for a billing period",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "billing.read"
            ]
          }
        ],
        "description": "Amounts are in paise (INR) and shown as net, GST and total. Items are `FLOW.<flowKey>` (a journey start) and `API.<CODE>` (an API call, or an extra attempt inside a journey). Each ledger line snapshots price and GST at the time, so later price changes never rewrite history.",
        "parameters": [
          {
            "name": "period",
            "in": "query",
            "description": "Billing period `YYYY-MM`. Defaults to the current month.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/v1/wallet": {
      "get": {
        "tags": [
          "Billing"
        ],
        "operationId": "getWallet",
        "summary": "Prepaid wallet balance",
        "description": "Current wallet mode and balance. A prepaid workspace with too little balance cannot create onboarding requests: `POST /v1/onboarding-requests` answers `402 VERIFYME_INSUFFICIENT_BALANCE`.",
        "responses": {
          "200": {
            "description": "Wallet",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Wallet"
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/verify/pan": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "verifyPan",
        "summary": "Verify a PAN",
        "description": "Charged per call at your price book price plus GST. Nothing is stored.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "panMasked": {
                      "type": "string"
                    },
                    "holderName": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "aadhaarLinked": {
                      "type": "boolean"
                    },
                    "nameMatch": {
                      "type": "object",
                      "properties": {
                        "score": {
                          "type": "integer"
                        },
                        "matched": {
                          "type": "boolean"
                        },
                        "threshold": {
                          "type": "integer"
                        }
                      }
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "pan"
                ],
                "properties": {
                  "pan": {
                    "type": "string",
                    "example": "ABCPE1234F"
                  },
                  "name": {
                    "type": "string",
                    "description": "Optional name to compare with the PAN holder."
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "8 to 120 characters. A retried call with the same key is not charged again.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/verify/bank-account": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "verifyBankAccount",
        "summary": "Verify a bank account",
        "description": "Charged per call at your price book price plus GST.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean"
                    },
                    "accountMasked": {
                      "type": "string"
                    },
                    "ifsc": {
                      "type": "string"
                    },
                    "holderName": {
                      "type": "string"
                    },
                    "bankName": {
                      "type": "string"
                    },
                    "nameMatch": {
                      "type": "object"
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "accountNumber",
                  "ifsc"
                ],
                "properties": {
                  "accountNumber": {
                    "type": "string"
                  },
                  "ifsc": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "8 to 120 characters. A retried call with the same key is not charged again.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/verify/face-match": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "verifyFaceMatch",
        "summary": "Match a selfie to a photo",
        "description": "Base64 JPEG/PNG, up to 2 MB each.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matched": {
                      "type": "boolean"
                    },
                    "score": {
                      "type": "number"
                    },
                    "threshold": {
                      "type": "number"
                    },
                    "confidence": {
                      "type": "number"
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "selfieBase64",
                  "referenceBase64"
                ],
                "properties": {
                  "selfieBase64": {
                    "type": "string"
                  },
                  "referenceBase64": {
                    "type": "string"
                  },
                  "threshold": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 99,
                    "default": 60
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "8 to 120 characters. A retried call with the same key is not charged again.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/verify/liveness": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "verifyLiveness",
        "summary": "Check liveness",
        "description": "3 to 6 base64 frames.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "live": {
                      "type": "boolean"
                    },
                    "score": {
                      "type": "number"
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "frames"
                ],
                "properties": {
                  "frames": {
                    "type": "array",
                    "minItems": 3,
                    "maxItems": 6,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "8 to 120 characters. A retried call with the same key is not charged again.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/lookup/pincode/{pincode}": {
      "get": {
        "tags": [
          "Verification"
        ],
        "operationId": "lookupPincode",
        "summary": "Look up a pincode",
        "description": "City and state for a six-digit pincode.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "found": {
                      "type": "boolean"
                    },
                    "pincode": {
                      "type": "string"
                    },
                    "city": {
                      "type": "string"
                    },
                    "state": {
                      "type": "string"
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "parameters": [
          {
            "name": "pincode",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[1-9][0-9]{5}$"
            }
          }
        ]
      }
    },
    "/v1/otp/mobile/send": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "sendMobileOtp",
        "summary": "Send a mobile OTP",
        "description": "Valid 5 minutes, 3 tries. Sandbox keys also receive `sandboxCode`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "otpId": {
                      "type": "string"
                    },
                    "sent": {
                      "type": "boolean"
                    },
                    "target": {
                      "type": "string"
                    },
                    "expiresInSec": {
                      "type": "integer"
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "mobile"
                ],
                "properties": {
                  "mobile": {
                    "type": "string",
                    "example": "9876543210"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "8 to 120 characters. A retried call with the same key is not charged again.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/otp/email/send": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "sendEmailOtp",
        "summary": "Send an e-mail OTP",
        "description": "Valid 5 minutes, 3 tries.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "otpId": {
                      "type": "string"
                    },
                    "sent": {
                      "type": "boolean"
                    },
                    "target": {
                      "type": "string"
                    },
                    "expiresInSec": {
                      "type": "integer"
                    },
                    "charge": {
                      "$ref": "#/components/schemas/Charge"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "8 to 120 characters. A retried call with the same key is not charged again.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/otp/mobile/verify": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "verifyMobileOtp",
        "summary": "Verify a mobile OTP",
        "description": "Free. A code works once.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "verified": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string",
                      "description": "INCORRECT_CODE, EXPIRED_OR_UNKNOWN or ATTEMPTS_EXCEEDED"
                    },
                    "attemptsLeft": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "otpId",
                  "otp"
                ],
                "properties": {
                  "otpId": {
                    "type": "string"
                  },
                  "otp": {
                    "type": "string",
                    "example": "123456"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/otp/email/verify": {
      "post": {
        "tags": [
          "Verification"
        ],
        "operationId": "verifyEmailOtp",
        "summary": "Verify a email OTP",
        "description": "Free. A code works once.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "verify.use"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "verified": {
                      "type": "boolean"
                    },
                    "reason": {
                      "type": "string",
                      "description": "INCORRECT_CODE, EXPIRED_OR_UNKNOWN or ATTEMPTS_EXCEEDED"
                    },
                    "attemptsLeft": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "otpId",
                  "otp"
                ],
                "properties": {
                  "otpId": {
                    "type": "string"
                  },
                  "otp": {
                    "type": "string",
                    "example": "123456"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/pricing": {
      "get": {
        "tags": [
          "Usage"
        ],
        "operationId": "getPricing",
        "summary": "Your prices",
        "description": "Your price book: every journey and API with net, GST and total (paise). `priced: false` means not enabled.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "billing.read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "priceBook": {
                      "type": "object"
                    },
                    "gstRatePercent": {
                      "type": "number"
                    },
                    "sacCode": {
                      "type": "string"
                    },
                    "placeOfSupply": {
                      "type": "string"
                    },
                    "intraState": {
                      "type": "boolean"
                    },
                    "includedAttemptsPerStep": {
                      "type": "integer"
                    },
                    "flows": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "apis": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthenticated"
          },
          "402": {
            "description": "Prepaid wallet cannot cover the price plus GST (`VERIFYME_INSUFFICIENT_BALANCE`)."
          },
          "403": {
            "description": "Missing scope, or the API is not priced in your price book (`API_NOT_ENABLED`)."
          },
          "422": {
            "description": "Validation error."
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "`PROVIDER_UNAVAILABLE`: the partner failed. You are not charged. See `Retry-After`."
          }
        }
      }
    }
  },
  "webhooks": {
    "verifyme-event": {
      "post": {
        "tags": [
          "Webhook endpoints"
        ],
        "operationId": "receiveVerifyMeEvent",
        "summary": "Event delivered to your endpoint",
        "description": "VerifyMe POSTs one JSON event per delivery. Respond with any `2xx` within **8 seconds**; redirects are **not** followed.\n\n**Verify the signature** before trusting the body. `X-VerifyMe-Signature` is `v1=<hex>` where `<hex> = HMAC-SHA256(secret, \"<timestamp>.<raw body>\")`; during a secret rotation overlap the header contains two comma-separated `v1=` values and either may match. Compare in constant time and reject timestamps older than 5 minutes (replay window). Use the **raw** body bytes, not re-serialised JSON.\n\n**Retries.** Non-2xx, timeout and network errors are retried with exponential backoff (30s, 2m, 10m, 30m, 2h, 6h, 12h; each with +/-20% jitter), at most 8 attempts, then the delivery is `DEAD`. `3xx` and `4xx` other than 408, 409 and 429 are treated as permanent and not retried. After 20 consecutive failures the endpoint is disabled. Delivery is **at least once**: dedupe on `eventId` (`X-VerifyMe-Event-Id`). Ordering is not guaranteed.\n\n**Event types.** `onboarding.created`, `onboarding.started`, `onboarding.step.completed`, `onboarding.submitted`, `onboarding.reverted`, `onboarding.approved`, `onboarding.rejected`, `onboarding.expired`, `onboarding.cancelled`, `verification.started`, `verification.completed`, `verification.failed`, `verification.pending`, `document.uploaded`, `document.rejected`, `document.expired`, `webhook.test`.",
        "parameters": [
          {
            "name": "X-VerifyMe-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd"
          },
          {
            "name": "X-VerifyMe-Timestamp",
            "in": "header",
            "required": true,
            "description": "Unix seconds used in the signature.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-VerifyMe-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-VerifyMe-Delivery",
            "in": "header",
            "required": true,
            "description": "Delivery id (changes per replay).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-VerifyMe-Attempt",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "example": {
                "eventId": "evt_01H8...",
                "eventType": "onboarding.submitted",
                "schemaVersion": "1",
                "tenantId": "tnt_01H...",
                "requestId": "req_01H...",
                "externalReference": "CUST-10042",
                "occurredAt": "2026-10-01T09:30:00.000Z",
                "environment": "test",
                "data": {
                  "status": "SUBMITTED"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged. Any 2xx counts as delivered."
          },
          "410": {
            "description": "Permanent failure: not retried."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key (vm_test_/vm_live_) or OAuth access token (vm_at_)",
        "description": "Send `Authorization: Bearer <token>`. Keys carry scopes: onboarding.create, onboarding.read, onboarding.result, onboarding.cancel, webhook.manage, billing.read. Keys may have an IP allow-list."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "Client credentials. `client_id` = key prefix, `client_secret` = secret part of the key.",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "/v1/oauth/token",
            "scopes": {
              "onboarding.create": "Create requests and reissue links",
              "onboarding.read": "Read request status and flows",
              "onboarding.result": "Read results and document links",
              "onboarding.cancel": "Cancel requests",
              "webhook.manage": "Manage webhook endpoints and deliveries",
              "billing.read": "Read usage",
              "verify.use": "Use the verification APIs (charged per call)"
            }
          }
        }
      }
    },
    "responses": {
      "Validation": {
        "description": "Validation failed (`VERIFYME_VALIDATION_ERROR`, `VERIFYME_INVALID_JSON`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "VERIFYME_VALIDATION_ERROR",
                "message": "One or more fields are invalid.",
                "details": [
                  {
                    "field": "mobile",
                    "code": "INVALID_MOBILE"
                  }
                ],
                "traceId": "trc_01H..."
              }
            }
          }
        }
      },
      "Unauthenticated": {
        "description": "Missing, malformed, revoked or wrong-environment credentials (`VERIFYME_UNAUTHENTICATED`). Repeated failures raise a security event.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Key lacks the scope (`VERIFYME_INSUFFICIENT_SCOPE`), IP not allow-listed, or tenant suspended (`VERIFYME_TENANT_SUSPENDED`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Unknown resource, or one that belongs to another tenant (deliberately indistinguishable).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "State conflict: `IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_IN_PROGRESS`, `DUPLICATE_EXTERNAL_REFERENCE` (when the tenant blocks duplicates), `RESULT_NOT_READY`, `VERIFYME_INVALID_STATE`, `VERIFYME_STATE_CONFLICT`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "Body too large (`VERIFYME_PAYLOAD_TOO_LARGE`, limit 256 KB).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unprocessable": {
        "description": "Well-formed but refused: `FLOW_NOT_FOUND`, `ENDPOINT_LIMIT`, `PRIVATE_ADDRESS_NOT_ALLOWED`, `HTTPS_REQUIRED`, `HOST_NOT_RESOLVABLE`, `UNKNOWN_EVENT`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests (`VERIFYME_RATE_LIMITED`).",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "traceId"
            ],
            "properties": {
              "code": {
                "type": "string",
                "example": "VERIFYME_VALIDATION_ERROR"
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "field": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string"
                    }
                  },
                  "additionalProperties": true
                }
              },
              "requestId": {
                "type": "string"
              },
              "traceId": {
                "type": "string",
                "example": "trc_01H8X..."
              }
            }
          }
        }
      },
      "OAuthTokenRequest": {
        "type": "object",
        "required": [
          "grant_type",
          "client_id",
          "client_secret"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "enum": [
              "client_credentials"
            ]
          },
          "client_id": {
            "type": "string",
            "description": "Key prefix, e.g. `vm_test_AbCdEfGh`."
          },
          "client_secret": {
            "type": "string",
            "description": "Secret part of the key."
          }
        }
      },
      "OAuthTokenResponse": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "scope"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "example": "vm_at_..."
          },
          "token_type": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expires_in": {
            "type": "integer",
            "example": 3600
          },
          "scope": {
            "type": "string",
            "description": "Space-separated scopes."
          }
        }
      },
      "Ping": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "environment": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          },
          "tenantId": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "RequestStatus": {
        "type": "string",
        "enum": [
          "CREATED",
          "LINK_ISSUED",
          "IN_PROGRESS",
          "SUBMITTED",
          "REVIEWING",
          "REVERTED",
          "APPROVED",
          "REJECTED",
          "EXPIRED",
          "CANCELLED"
        ],
        "description": "CREATED/LINK_ISSUED -> IN_PROGRESS -> SUBMITTED -> REVIEWING -> APPROVED | REJECTED | REVERTED (back to the applicant). EXPIRED and CANCELLED are terminal; SUBMITTED/REVIEWING never expire."
      },
      "CreateOnboardingRequest": {
        "type": "object",
        "required": [
          "externalReference"
        ],
        "additionalProperties": false,
        "properties": {
          "externalReference": {
            "type": "string",
            "maxLength": 200,
            "description": "Your id for the customer/order. Echoed in results and webhooks."
          },
          "applicantType": {
            "type": "string",
            "maxLength": 60,
            "pattern": "^[A-Za-z0-9_.-]+$",
            "default": "applicant"
          },
          "mobile": {
            "type": "string",
            "maxLength": 20,
            "description": "Indian mobile, 10 digits (+91 / leading 0 tolerated). Pre-fills the OTP step; stored encrypted."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "flowKey": {
            "type": "string",
            "maxLength": 80,
            "description": "Key of a published flow. Default: tenant default flow."
          },
          "returnUrl": {
            "type": "string",
            "maxLength": 500,
            "description": "Where the applicant goes after finishing. Must be https (or a registered app scheme) and, when the tenant has an allow-list, on it."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Up to ~4 KB of your own JSON. Not returned in webhooks; keep PII out."
          },
          "expiresInHours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 720,
            "description": "Link lifetime. Default from tenant settings."
          },
          "locale": {
            "type": "string",
            "enum": [
              "en",
              "hi",
              "ta",
              "te",
              "kn",
              "mr",
              "bn",
              "gu"
            ]
          }
        }
      },
      "OnboardingRequestCreated": {
        "type": "object",
        "required": [
          "requestId",
          "externalReference",
          "status",
          "onboardingUrl",
          "expiresAt"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "externalReference": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "LINK_ISSUED"
            ]
          },
          "onboardingUrl": {
            "type": "string",
            "format": "uri",
            "description": "Send the applicant here. Contains a bearer-style token: treat as a secret."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OnboardingRequestSummary": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string"
          },
          "externalReference": {
            "type": "string"
          },
          "applicantType": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "reviewStatus": {
            "type": "string"
          },
          "mobileMasked": {
            "type": [
              "string",
              "null"
            ],
            "example": "98XXXXXX10"
          },
          "flagCount": {
            "type": "integer"
          },
          "createdOn": {
            "type": "string",
            "format": "date-time"
          },
          "submittedOn": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expiresOn": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OnboardingRequestList": {
        "type": "object",
        "required": [
          "total",
          "items"
        ],
        "properties": {
          "total": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OnboardingRequestSummary"
            }
          }
        }
      },
      "OnboardingRequestStatus": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string"
          },
          "externalReference": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "progress": {
            "type": "object",
            "properties": {
              "completed": {
                "type": "integer",
                "description": "Required steps completed or skipped."
              },
              "total": {
                "type": "integer",
                "description": "Required steps."
              },
              "stepsCompleted": {
                "type": "integer"
              },
              "stepsTotal": {
                "type": "integer"
              }
            }
          },
          "review": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "example": "NONE"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "submittedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "OnboardingResult": {
        "type": "object",
        "properties": {
          "requestId": {
            "type": "string"
          },
          "externalReference": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/RequestStatus"
          },
          "decision": {
            "type": [
              "object",
              "null"
            ],
            "description": "Null until a reviewer approves or rejects.",
            "properties": {
              "outcome": {
                "type": "string",
                "enum": [
                  "APPROVED",
                  "REJECTED"
                ]
              },
              "reasonCode": {
                "type": "string"
              },
              "decidedAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "verified": {
            "type": "object",
            "description": "Verified attributes. Identifiers are masked. Keys can be filtered with `fields`.",
            "properties": {
              "fullName": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dateOfBirth": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "gender": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "address": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": true
              },
              "mobile": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "98XXXXXX10"
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "a***@example.com"
              },
              "pan": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "ABXXXXXX4F"
              },
              "aadhaar": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "XXXX XXXX 1234"
              },
              "bankAccount": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "XXXXXXXX9012"
              },
              "bankAccountHolder": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "faceMatchScore": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "faceMatchPassed": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "livenessPassed": {
                "type": [
                  "boolean",
                  "null"
                ]
              }
            },
            "additionalProperties": true
          },
          "flags": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "step": {
                  "type": "string"
                },
                "severity": {
                  "type": "string"
                }
              }
            },
            "description": "Risk / consistency flags raised while verifying (e.g. name mismatch between PAN and Aadhaar)."
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "key": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "PENDING",
                    "IN_PROGRESS",
                    "PROVIDER_PENDING",
                    "FAILED_RETRYABLE",
                    "COMPLETED",
                    "FAILED",
                    "SKIPPED",
                    "REVERTED"
                  ]
                },
                "attempts": {
                  "type": "integer"
                },
                "completedAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          },
          "documents": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "documentId": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "contentType": {
                  "type": "string"
                },
                "sizeBytes": {
                  "type": "integer"
                },
                "scanStatus": {
                  "type": "string",
                  "example": "CLEAN"
                },
                "uploadedAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "submittedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "completedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DocumentAccess": {
        "type": "object",
        "required": [
          "documentId",
          "url",
          "expiresInSec"
        ],
        "properties": {
          "documentId": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Signed, expiring, no-store."
          },
          "expiresInSec": {
            "type": "integer",
            "example": 120
          }
        }
      },
      "ReissuedLink": {
        "type": "object",
        "required": [
          "requestId",
          "onboardingUrl",
          "expiresAt"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "onboardingUrl": {
            "type": "string",
            "format": "uri"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "FlowList": {
        "type": "object",
        "properties": {
          "flows": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "flowKey": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "version": {
                  "type": "integer"
                },
                "steps": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "key": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      },
                      "required": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "CreateWebhookEndpoint": {
        "type": "object",
        "required": [
          "url"
        ],
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 500
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "*",
                "onboarding.created",
                "onboarding.started",
                "onboarding.step.completed",
                "onboarding.submitted",
                "onboarding.reverted",
                "onboarding.approved",
                "onboarding.rejected",
                "onboarding.expired",
                "onboarding.cancelled",
                "verification.started",
                "verification.completed",
                "verification.failed",
                "verification.pending",
                "document.uploaded",
                "document.rejected",
                "document.expired",
                "webhook.test"
              ]
            },
            "description": "Defaults to `[\"*\"]`."
          }
        }
      },
      "UpdateWebhookEndpoint": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 500
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "*",
                "onboarding.created",
                "onboarding.started",
                "onboarding.step.completed",
                "onboarding.submitted",
                "onboarding.reverted",
                "onboarding.approved",
                "onboarding.rejected",
                "onboarding.expired",
                "onboarding.cancelled",
                "verification.started",
                "verification.completed",
                "verification.failed",
                "verification.pending",
                "document.uploaded",
                "document.rejected",
                "document.expired",
                "webhook.test"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "ACTIVE",
              "DISABLED"
            ]
          }
        }
      },
      "WebhookEndpoint": {
        "type": "object",
        "properties": {
          "endpointId": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "ACTIVE",
              "DISABLED"
            ]
          },
          "consecutiveFailures": {
            "type": "integer"
          },
          "secretRotating": {
            "type": "boolean",
            "description": "True while the previous secret is still accepted (rotation overlap)."
          },
          "createdOn": {
            "type": "string",
            "format": "date-time"
          },
          "updatedOn": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpointCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEndpoint"
          },
          {
            "type": "object",
            "required": [
              "secret"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "description": "Signing secret. Shown once."
              },
              "note": {
                "type": "string"
              }
            }
          }
        ]
      },
      "WebhookEndpointList": {
        "type": "object",
        "properties": {
          "endpoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            }
          }
        }
      },
      "DeliveryQueued": {
        "type": "object",
        "properties": {
          "deliveryId": {
            "type": "string"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "deliveryId": {
            "type": "string"
          },
          "eventId": {
            "type": "string"
          },
          "eventType": {
            "type": "string"
          },
          "requestId": {
            "type": [
              "string",
              "null"
            ]
          },
          "endpointId": {
            "type": "string"
          },
          "attempt": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "enum": [
              "PENDING",
              "SUCCESS",
              "DEAD"
            ]
          },
          "statusCode": {
            "type": [
              "integer",
              "null"
            ]
          },
          "durationMs": {
            "type": [
              "integer",
              "null"
            ]
          },
          "nextRetryOn": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "deliveredOn": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdOn": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookDeliveryList": {
        "type": "object",
        "properties": {
          "eventTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "deliveries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookDelivery"
            }
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "eventId",
          "eventType",
          "schemaVersion",
          "tenantId",
          "occurredAt",
          "environment",
          "data"
        ],
        "properties": {
          "eventId": {
            "type": "string",
            "description": "Stable across retries and replays: use it to dedupe."
          },
          "eventType": {
            "type": "string",
            "enum": [
              "onboarding.created",
              "onboarding.started",
              "onboarding.step.completed",
              "onboarding.submitted",
              "onboarding.reverted",
              "onboarding.approved",
              "onboarding.rejected",
              "onboarding.expired",
              "onboarding.cancelled",
              "verification.started",
              "verification.completed",
              "verification.failed",
              "verification.pending",
              "document.uploaded",
              "document.rejected",
              "document.expired",
              "webhook.test"
            ]
          },
          "schemaVersion": {
            "type": "string",
            "example": "1"
          },
          "tenantId": {
            "type": "string"
          },
          "requestId": {
            "type": [
              "string",
              "null"
            ]
          },
          "externalReference": {
            "type": [
              "string",
              "null"
            ]
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "environment": {
            "type": "string",
            "enum": [
              "test",
              "live"
            ]
          },
          "data": {
            "type": "object",
            "description": "Event-specific, minimal (no PII). Always carries `status` for onboarding.* events; step, verification and document events carry the step key and outcome. Fetch the result via the API for details.",
            "additionalProperties": true
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "billingPeriod": {
            "type": "string",
            "example": "2026-10"
          },
          "plan": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          },
          "byMeter": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "meter": {
                  "type": "string",
                  "enum": [
                    "ONBOARDING_REQUEST",
                    "KYC_SUBMISSION",
                    "AADHAAR_VERIFICATION",
                    "PAN_VERIFICATION",
                    "BANK_VERIFICATION",
                    "FACE_MATCH",
                    "VIDEO_LIVENESS",
                    "DOCUMENT_PROCESSING"
                  ]
                },
                "quantity": {
                  "type": "integer"
                },
                "unitPricePaise": {
                  "type": "integer"
                },
                "amountPaise": {
                  "type": "integer"
                }
              }
            }
          },
          "usagePaise": {
            "type": "integer"
          },
          "includedCreditsAppliedPaise": {
            "type": "integer"
          },
          "overagePaise": {
            "type": "integer"
          },
          "subscriptionPaise": {
            "type": "integer"
          },
          "estimatedSubtotalPaise": {
            "type": "integer",
            "description": "Before GST. Indicative only."
          },
          "currency": {
            "type": "string",
            "enum": [
              "INR"
            ]
          },
          "daily": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "string",
                  "format": "date"
                },
                "quantity": {
                  "type": "integer"
                },
                "amountPaise": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "Wallet": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "PREPAID",
              "POSTPAID"
            ]
          },
          "balancePaise": {
            "type": "integer"
          },
          "currency": {
            "type": "string",
            "example": "INR"
          },
          "lowBalanceThresholdPaise": {
            "type": "integer"
          }
        }
      },
      "Charge": {
        "type": "object",
        "description": "What this call cost. Amounts in paise.",
        "properties": {
          "item": {
            "type": "string",
            "example": "API.PAN_VERIFICATION"
          },
          "charged": {
            "type": "boolean",
            "description": "False when the result was not billable."
          },
          "netPaise": {
            "type": "integer"
          },
          "gstPaise": {
            "type": "integer"
          },
          "totalPaise": {
            "type": "integer",
            "description": "net + GST, the amount debited."
          },
          "gstRatePercent": {
            "type": "number"
          },
          "currency": {
            "type": "string",
            "example": "INR"
          }
        }
      }
    }
  }
}