{
  "openapi": "3.0.3",
  "info": {
    "version": "1.3.0",
    "title": "UpPass API",
    "description": "UpPass verification services, callable directly from your backend. Create a\nverification for one applicant, hand them a link — a URL, a QR code, or an\nembedded `WebView` — and receive the full eKYC result on your webhook.\n\n> **Guides live at [docs.uppass.io](https://docs.uppass.io).** This reference is\n> the endpoint-by-endpoint contract. For the quickstart, delivery patterns,\n> decision logic and webhook receiver design, start with the guides.\n\n\n### Placeholders in this reference\n\n\nNothing below contains a real credential or identifier. Substitute your own:\n\n\n| Placeholder | Where to find it |\n|---|---|\n| `{api_token}` | Portal → your workspace → API Token |\n| `{form_slug}` / `YOUR_FORM_SLUG` | Portal → Flows → your flow → Settings |\n| `{slug}` / `YOUR_APPLICATION_SLUG` | Returned as `detail.slug` by [Create application](#tag/applications/createAppliedForm) |\n| `{lang}` | The locale you want the form rendered in |\n| `{workspace}` | Workspace selector in the Portal |\n\n\n`{braces}` appear in URL paths and prose; `YOUR_UPPER_CASE` appears inside example\npayloads, because braces are not legal URI characters. Both mean the same thing.\nFull table at\n[docs.uppass.io/get-started/values](https://docs.uppass.io/get-started/values/).\n\n# Before you integrate\n\nFour things catch most integrations. Each is documented in full where it applies;\nthey are collected here because reading the reference alone will not surface them\nin time.\n\n### Read results from `extra`, not from a status column\n\n`other_status.ekyc` is the only status key that is part of this contract; it\nreports the **document and liveness** checks, combined. Every other key in that\nobject is a status column configured in your own workspace, so its name and\nvalues are yours rather than ours.\n\nThe detail behind any verdict — face-comparison score, liveness attempts,\ndocument fields, screening hits — lives in `extra`.\n\n### Signed image URLs expire after 15 minutes\n\nThe `exp` on every image URL is 15 minutes after `event.created_at`, not after\nsubmission — so acknowledge the webhook fast, but fetch images on a priority path\ninside that window rather than behind a queue.\n[Resend webhook](#tag/webhooks/resendWebhook) mints fresh URLs if you miss it.\n\n### The `slug` opens an unfinished application\n\n[Get application info](#tag/applications/getAppliedFormInfo) is served **without\nauthentication** while an application is still open — this is what lets an\napplicant resume a form they abandoned. Once the application is submitted, the\nendpoint requires the token; once deleted, it is `404` for everyone. Deliver\n`form_url` to the applicant only, and keep it out of logs and analytics.\n\n### `401` and `403` are not what you expect\n\nA **missing** `Authorization` header returns `403`; `401` is reserved for a token\nthat is present but invalid. `GET` on a `POST`-only endpoint also returns `403`,\nnever `405`.\n\n# Authentication\n\nEvery endpoint except [Get application\ninfo](#tag/applications/getAppliedFormInfo) requires a bearer token from **UpPass Portal → your workspace →\nAPI Token** (left-hand menu):\n\n```http\nAuthorization: Bearer {api_token}\n```\n\nKeep it server-side. A token in client code lets anyone create applications against\nyour workspace, and the verifications they trigger consume your credit.\n\n# Errors\n\nMost errors use one envelope:\n\n```json\n{\n  \"error\": {\n    \"status_code\": 401,\n    \"message\": \"Unauthorized\",\n    \"detail\": \"No permission -- see authorization schemes\"\n  }\n}\n```\n\nOn `422`, `error.detail` is an object keyed by `question_key` rather than a\nstring. Two endpoints differ, and both are documented on the operation:\n[Submit](#tag/applications/submitAppliedForm)\nreturns a **flat** field map with no `error` wrapper, and\n[Resend webhook](#tag/webhooks/resendWebhook) returns a bare `detail` string on\n`404`.\n\nRetry `429` with backoff and `5xx` up to three times. Never retry `4xx`. Alert on\n`461` specifically: before creating an application UpPass checks that the workspace\nholds enough credit to complete the whole flow, and when it does not, every create\nfails until the workspace is topped up.\n",
    "contact": {
      "name": "UpPass Support",
      "url": "https://www.uppass.io"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://www.uppass.io/legal/terms-of-service"
    }
  },
  "servers": [
    {
      "url": "https://app.uppass.io",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiToken": []
    }
  ],
  "tags": [
    {
      "name": "Applications",
      "description": "Create an application for one applicant, read its state, and submit it.\n\n\nAn **application** (`applied_form` internally) is one applicant's pass\nthrough one flow. Its `slug` is your correlation key — store it against your\nown record.\n"
    },
    {
      "name": "Webhooks",
      "description": "Endpoints **you** implement and UpPass calls, plus the API to re-trigger a\ndelivery.\n\n\nThe webhook is the authoritative result channel. Polling\n[Get application result](#tag/applications/getAppliedFormResult) is for progress\ndisplay and recovery, not for driving business logic.\n"
    }
  ],
  "paths": {
    "/{lang}/api/forms/{form_slug}/create/": {
      "post": {
        "operationId": "createAppliedForm",
        "summary": "Create application",
        "tags": [
          "Applications"
        ],
        "description": "Creates one application (`applied_form`) for a single applicant and returns the\nunique URL to send them to.\n\n\nEvery applicant needs their own call. `form_url` is bound to one application —\nreusing it across people corrupts both records.\n\n\n## Choosing a delivery pattern\n\n\n| | Link / QR | API → link | WebView |\n|---|---|---|---|\n| This endpoint | Not used | Yes | Yes |\n| Where it opens | Hosted form | Hosted form | In your app |\n| Result via | Portal | Webhook + Portal | Webhook + Portal |\n\n\n## After a successful call\n\n\n1. Persist `detail.slug` against your own record — it is your correlation key.\n2. Send the applicant to `form_url` (mobile browser, or a QR code on desktop).\n3. Wait for the [webhook](#tag/Webhooks). Poll\n   [Get application result](#tag/applications/getAppliedFormResult) only for progress\n   indicators, never to drive business logic.\n\n\n> **Credit.** Creating an application does not consume credit by itself — credit\n> is consumed when the applicant runs the verification steps. UpPass does check,\n> before creating, that the workspace holds enough credit to complete the whole\n> flow; if it does not, this call returns `461`. Do not call it in a retry loop.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Lang"
          },
          {
            "$ref": "#/components/parameters/FormSlug"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFormRequest"
              },
              "examples": {
                "draft": {
                  "summary": "National ID (default)",
                  "value": {
                    "answers": {}
                  }
                },
                "passport": {
                  "summary": "Passport instead of National ID",
                  "value": {
                    "answers": {
                      "ekyc_document_type": "passport"
                    }
                  }
                },
                "prefilled": {
                  "summary": "With pre-filled answers",
                  "description": "Keys must be fields on the form, as named in the Builder. Anything\nelse is discarded silently.\n",
                  "value": {
                    "answers": {
                      "ekyc_document_type": "front_card",
                      "ekyc_document_country": "THA",
                      "name_prefix": "Mr.",
                      "gender": "M",
                      "nid": "1111111111119",
                      "date_of_birth": "1990-01-15",
                      "home_address_country": "THA"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Application created.\n\n\n`submitted_at` is `null` until the applicant submits.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateFormResponse"
                },
                "examples": {
                  "draft": {
                    "$ref": "#/components/examples/CreateFormResponseDraft"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "461": {
            "$ref": "#/components/responses/InsufficientCredit"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "source": "curl --request POST \\\n  'https://app.uppass.io/en/api/forms/{form_slug}/create/' \\\n  --header 'Authorization: Bearer '\"$UPPASS_API_TOKEN\" \\\n  --header 'Content-Type: application/json' \\\n  --data '{\n    \"answers\": {\n      \"ekyc_document_type\": \"passport\"\n    }\n  }'\n"
          },
          {
            "lang": "JavaScript",
            "source": "// Browser-side callers must NOT hold the API token. Call your own backend,\n// which calls UpPass and returns form_url. See the Node.js sample.\nconst res = await fetch('/kyc/start', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ transactionId: 'TXN-0001' }),\n});\nconst { formUrl } = await res.json();\n\n// The verification flow is mobile-web only. On desktop, show a QR code\n// rather than a redirect that dead-ends.\nif (/Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {\n  window.location.href = formUrl;\n} else {\n  renderQrCode(formUrl);\n}\n"
          },
          {
            "lang": "Node.js",
            "source": "const HOST = 'https://app.uppass.io';\nconst LANG = 'en';\nconst FORM = '{form_slug}';\n\nasync function createApplication({ documentType = 'passport' } = {}) {\n  const res = await fetch(`${HOST}/${LANG}/api/forms/${FORM}/create/`, {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${process.env.UPPASS_API_TOKEN}`,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ answers: { ekyc_document_type: documentType } }),\n  });\n\n  if (res.status === 461) throw new Error('Workspace credit exhausted — top up the workspace');\n  if (res.status === 422) throw new Error(`Validation failed: ${JSON.stringify((await res.json()).error.detail)}`);\n  if (!res.ok) throw new Error(`create failed: ${res.status}`);\n\n  // 201 Created — not 200.\n  const { detail, form_url } = await res.json();\n\n  // detail.slug is your correlation key. Store it against your own record.\n  return { slug: detail.slug, formUrl: form_url };\n}\n"
          },
          {
            "lang": "Python",
            "source": "import os\nimport requests\n\nHOST = \"https://app.uppass.io\"\nLANG = \"en\"\nFORM = \"{form_slug}\"\n\n\ndef create_application(document_type: str = \"passport\") -> dict:\n    res = requests.post(\n        f\"{HOST}/{LANG}/api/forms/{FORM}/create/\",\n        headers={\n            \"Authorization\": f\"Bearer {os.environ['UPPASS_API_TOKEN']}\",\n            \"Content-Type\": \"application/json\",\n        },\n        json={\"answers\": {\"ekyc_document_type\": document_type}},\n        timeout=30,\n    )\n\n    if res.status_code == 461:\n        raise RuntimeError(\"Workspace credit exhausted — top up before retrying\")\n    if res.status_code == 422:\n        raise ValueError(f\"Validation failed: {res.json()['error']['detail']}\")\n    res.raise_for_status()  # 201 Created on success, not 200\n\n    body = res.json()\n    # detail[\"slug\"] is the correlation key. Store it against your own record.\n    return {\"slug\": body[\"detail\"][\"slug\"], \"form_url\": body[\"form_url\"]}\n"
          },
          {
            "lang": "C#",
            "source": "using System.Net.Http.Json;\nusing System.Text.Json;\n\nvar host = \"https://app.uppass.io\";\nconst string lang = \"en\";\nconst string form = \"{form_slug}\";\n\nusing var http = new HttpClient();\nhttp.DefaultRequestHeaders.Authorization =\n    new System.Net.Http.Headers.AuthenticationHeaderValue(\n        \"Bearer\", Environment.GetEnvironmentVariable(\"UPPASS_API_TOKEN\"));\n\nvar res = await http.PostAsJsonAsync(\n    $\"{host}/{lang}/api/forms/{form}/create/\",\n    new { answers = new { ekyc_document_type = \"passport\" } });\n\n// 201 Created on success, not 200.\nif ((int)res.StatusCode == 461)\n    throw new InvalidOperationException(\"Workspace credit exhausted — top up before retrying\");\n\nvar body = await res.Content.ReadFromJsonAsync<JsonElement>();\nif ((int)res.StatusCode == 422)\n    throw new ArgumentException(body.GetProperty(\"error\").GetProperty(\"detail\").ToString());\n\nres.EnsureSuccessStatusCode();\n\n// detail.slug is the correlation key. Store it against your own record.\nvar slug = body.GetProperty(\"detail\").GetProperty(\"slug\").GetString();\nvar formUrl = body.GetProperty(\"form_url\").GetString();\n"
          }
        ]
      }
    },
    "/{lang}/api/forms/{form_slug}/applied-forms/{slug}/result/": {
      "get": {
        "operationId": "getAppliedFormResult",
        "summary": "Get application result",
        "tags": [
          "Applications"
        ],
        "description": "Current result for one application, in **exactly the shape the webhook\ndelivers** — `application`, `answers`, `extra` — minus the `event` object.\n\n\nUse this for any server-side read: progress indicators, reconciliation, or\nrecovering a result your receiver missed. It is a few kilobytes, versus ~57 kB\nfor [Get application info](#tag/applications/getAppliedFormInfo).\n\n\n> **The webhook remains the authoritative channel.** Poll this endpoint for\n> display and recovery, not to drive business logic — and never in a tight\n> loop.\n\n\nBefore reading any verdict, check `application.status === \"complete\"`. On an\napplication still in progress, `other_status.ekyc` can already read `pass`\nwhile nothing has actually been verified.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Lang"
          },
          {
            "$ref": "#/components/parameters/FormSlug"
          },
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Current state of the application.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppliedFormResult"
                },
                "examples": {
                  "inProgress": {
                    "summary": "Unsubmitted, nothing verified yet",
                    "description": "Note `ekyc: \"pass\"` on an application with `status: \"incomplete\"`\nand no captured artefacts — which is why `status` must be checked\nfirst.\n",
                    "value": {
                      "application": {
                        "id": 100001,
                        "no": "COK00100001",
                        "form": "YOUR_FORM_SLUG",
                        "slug": "YOUR_APPLICATION_SLUG",
                        "status": "incomplete",
                        "submitted_at": null,
                        "other_status": {
                          "ekyc": "pass"
                        }
                      },
                      "answers": {},
                      "extra": {}
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "source": "curl 'https://app.uppass.io/en/api/forms/{form_slug}/applied-forms/{slug}/result/' \\\n  --header 'Authorization: Bearer '\"$UPPASS_API_TOKEN\"\n"
          },
          {
            "lang": "Node.js",
            "source": "const HOST = 'https://app.uppass.io';\n\nasync function getResult(formSlug, slug) {\n  const res = await fetch(\n    `${HOST}/en/api/forms/${formSlug}/applied-forms/${slug}/result/`,\n    { headers: { Authorization: `Bearer ${process.env.UPPASS_API_TOKEN}` } },\n  );\n  if (!res.ok) throw new Error(`result failed: ${res.status}`);\n\n  // Same shape as the webhook payload, minus `event`.\n  // How to turn it into a decision — and in what order — is specified at\n  // https://docs.uppass.io/guides/reading-the-result/\n  return res.json();\n}\n"
          },
          {
            "lang": "Python",
            "source": "import os\n\nimport requests\n\nHOST = \"https://app.uppass.io\"\n\n\ndef get_result(form_slug: str, slug: str) -> dict:\n    res = requests.get(\n        f\"{HOST}/en/api/forms/{form_slug}/applied-forms/{slug}/result/\",\n        headers={\"Authorization\": f\"Bearer {os.environ['UPPASS_API_TOKEN']}\"},\n        timeout=30,\n    )\n    res.raise_for_status()\n\n    # Same shape as the webhook payload, minus `event`.\n    # The decision sequence is specified at\n    # https://docs.uppass.io/guides/reading-the-result/\n    return res.json()\n"
          }
        ]
      }
    },
    "/{lang}/api/forms/{form_slug}/applied-forms/{slug}/info/": {
      "get": {
        "operationId": "getAppliedFormInfo",
        "summary": "Get application info",
        "tags": [
          "Applications"
        ],
        "description": "Everything the hosted form needs to render itself — the full form schema, all UI\ntranslations, branding config and the consent text — plus the application's\ncurrent state under `data`.\n\n\nRoughly **57 kB per call**. This endpoint exists to serve the hosted form, which\nfetches its own schema with it. For the state of an application from your own\ncode, use [Get application result](#tag/applications/getAppliedFormResult) — a\nfew kilobytes, and the same shape as the webhook.\n\n\n## Authentication depends on the application's state\n\n\n| Application state | Without an `Authorization` header |\n|---|---|\n| Created, not yet submitted | `200` — the form must be able to load and resume |\n| Submitted | Requires the API token |\n| Deleted | `404` for everyone |\n\n\nSo a `slug` reaches an in-progress form and nothing else; results, images and\nscreening outcomes are never readable this way. While the application is open,\nwhoever holds the slug can read what you pre-filled into it — deliver `form_url`\nto the applicant only, and keep it out of logs, analytics and referrer-leaking\nredirects.\n",
        "security": [],
        "parameters": [
          {
            "$ref": "#/components/parameters/Lang"
          },
          {
            "$ref": "#/components/parameters/FormSlug"
          },
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Form definition and current state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppliedFormInfo"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "source": "# No Authorization header is required on this endpoint.\n# The slug alone grants read access — treat it as a secret.\ncurl 'https://app.uppass.io/en/api/forms/{form_slug}/applied-forms/{slug}/info/' \\\n  | jq '.data | {current_step, is_submitted, persisted: .data}'\n"
          }
        ]
      }
    },
    "/{lang}/api/forms/{form_slug}/applied-forms/{slug}/submit/": {
      "post": {
        "operationId": "submitAppliedForm",
        "summary": "Submit an application",
        "tags": [
          "Applications"
        ],
        "description": "Submits an existing, unsubmitted application server-side, validating every required field and\nstarting the Decision Flow bound to the `on_submit` trigger.\n\n\n> **On a flow with a document-scan or liveness step this always returns `422`.**\n> `ekyc_document` and `ekyc_liveness` are camera captures and cannot be supplied\n> over JSON, so an application created through the API can never satisfy them. Those\n> flows must be submitted by the applicant from the form.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Lang"
          },
          {
            "$ref": "#/components/parameters/FormSlug"
          },
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnswersRequest"
              },
              "example": {
                "answers": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submitted. The Decision Flow has started. Empty response body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The application was already submitted. Do not retry.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "status_code": 403,
                    "message": "Forbidden",
                    "detail": "Request forbidden -- authorization will not help"
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationErrorFlat"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "source": "# Returns 422 on any flow with a document-scan or liveness step —\n# camera artefacts cannot be supplied over JSON.\ncurl --request POST \\\n  'https://app.uppass.io/en/api/forms/{form_slug}/applied-forms/{slug}/submit/' \\\n  --header 'Authorization: Bearer '\"$UPPASS_API_TOKEN\" \\\n  --header 'Content-Type: application/json' \\\n  --data '{ \"answers\": {} }'\n"
          }
        ]
      }
    },
    "/{lang}/api/forms/{form_slug}/applied-forms/{slug}/hook-submit/": {
      "post": {
        "operationId": "resendWebhook",
        "summary": "Resend webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "Asks UpPass to re-deliver the submission webhook for one application — after\nyour receiver was down, or during integration testing.\n\n\n**This is also how you recover expired images.** Signed image URLs live 15\nminutes from `event.created_at`, and a re-delivery mints fresh ones.\n\n\n> `204` means **queued**, not delivered — and not even *deliverable*. The call\n> returns `204` for an application that was never submitted, in which case no\n> webhook will ever arrive. Confirm the application is `complete` with\n> [Get application result](#tag/applications/getAppliedFormResult) first.\n\n\nThe resent payload has the same shape as the original, with a **new `nounce`**\nand a **new `event.created_at`**. Deduplicate on `application.slug` +\n`application.submitted_at`, not on `nounce`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/Lang"
          },
          {
            "$ref": "#/components/parameters/FormSlug"
          },
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "204": {
            "description": "Re-delivery queued. No response body. The payload reaches your receiver\nshortly afterwards.\n"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundBare"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-codeSamples": [
          {
            "lang": "Shell",
            "source": "# 204 means queued, not delivered. Also returns 204 for an application\n# that was never submitted, in which case no webhook will arrive.\ncurl --request POST \\\n  'https://app.uppass.io/en/api/forms/{form_slug}/applied-forms/{slug}/hook-submit/' \\\n  --header 'Authorization: Bearer '\"$UPPASS_API_TOKEN\" \\\n  --write-out '%{http_code}\\n'\n"
          }
        ]
      }
    },
    "/your-webhook-path": {
      "servers": [
        {
          "url": "https://your-server.example",
          "description": "Your receiver. The host and path are illustrative — UpPass posts to\nwhatever HTTPS URL you configured in Portal → Connect → Add Webhook.\nThis is not an UpPass endpoint.\n"
        }
      ],
      "post": {
        "operationId": "webhookSubmission",
        "summary": "Submission webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "**You implement this endpoint. UpPass calls it.**\n\n\nConfigure the receiver in **UpPass Portal → Connect → Add Webhook**: your HTTPS\nURL, `Bearer` authorization, and the `submit_form` event at minimum.\n\n\n## Receiver requirements\n\n\n| Requirement | Detail |\n|---|---|\n| Method | `POST`, `application/json` |\n| Transport | HTTPS with a valid certificate |\n| Auth | Reject any request whose `Authorization` header is not the Bearer secret you configured |\n| Response | Return `2xx` within **30 seconds** — acknowledge first, process afterwards |\n| Retries | `5xx` from you is retried up to three times; `4xx` and timeouts are not — recover with [Resend webhook](#tag/webhooks/resendWebhook) |\n| Idempotency | Deduplicate on `application.slug` + `application.submitted_at` — **not** on `event.nounce` |\n| Images | Fetch every signed URL within 15 minutes and copy it to your own storage |\n| Size | Size-test against a sanctions-match payload; `extra.identity_aml` is unbounded |\n\n\n## Verifying the request\n\n\nEvery delivery carries `Authorization: Bearer {webhook_secret}`, where the\nsecret is the one you entered when adding the webhook in the Portal. Compare\nit against your stored value and reject a mismatch with `401`. Nothing else\nauthenticates the caller — there is no signature header.\n\n\n## Idempotency\n\n\n`event.nounce` is minted per delivery, so a resend or an automatic retry after\na `5xx` arrives with a new value and passes any nonce-only check. Key your\ndeduplication on `application.slug` + `application.submitted_at` instead.\n\n\n## Reading the result\n\n\nSee [Reading the result](https://docs.uppass.io/guides/reading-the-result/)\nfor which fields carry the verdict and why to read them from `extra`.\n",
        "security": [
          {
            "WebhookSecret": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookSubmission"
              },
              "examples": {
                "amlHit": {
                  "$ref": "#/components/examples/WebhookSubmissionAmlHit"
                },
                "clean": {
                  "$ref": "#/components/examples/WebhookSubmissionClean"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any `2xx` **within 30 seconds** to acknowledge. Anything else — a\n`4xx`, or no answer in time — is a failed delivery that is not retried;\nrecover it with [Resend webhook](#tag/webhooks/resendWebhook).\n"
          },
          "401": {
            "description": "Return this if the `Authorization` header does not match your configured\nBearer secret. UpPass treats it as a failed delivery and does not retry.\n"
          },
          "4XX": {
            "description": "Any other `4xx` is a failed delivery and is **not** retried — recover it\nwith [Resend webhook](#tag/webhooks/resendWebhook).\n"
          },
          "5XX": {
            "description": "A `5xx` from your receiver is **retried, up to three times**. Return it only\nfor a genuine transient failure on your side; a payload you cannot process\nshould be acknowledged with `2xx` and dealt with out of band.\n"
          }
        }
      }
    },
    "/your-lifecycle-webhook-path": {
      "servers": [
        {
          "url": "https://your-server.example",
          "description": "Your receiver. The host and path are illustrative — UpPass posts to\nwhatever HTTPS URL you configured in Portal → Connect → Add Webhook.\nThis is not an UpPass endpoint.\n"
        }
      ],
      "post": {
        "operationId": "webhookEvents",
        "summary": "Lifecycle webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "**You implement this endpoint. UpPass calls it.**\n\n\nLightweight lifecycle notifications, delivered to the receiver you configured\nin **Portal → Connect → Add Webhook**. They carry `event` and `application`\nonly — no `answers`, no `extra`, and **no `event.version`**. Enable the ones\nyou need by contacting support.\n\n\n| `type` | Fires when | Setup |\n|---|---|---|\n| `update_status` | A status on the application changed — for example an operator changed it in the Portal. | Contact support |\n| `drop_off` | The applicant started and did not finish before the form expired. | Contact support |\n\n\n`drop_off` is the funnel-recovery signal: the applicant started and never\nfinished, so it is the moment to send a fresh link rather than wait for a\nsubmission that will not arrive.\n\n\nBranch on `event.type` before reading anything else, and ignore types you do\nnot handle — new ones may be added.\n",
        "security": [
          {
            "WebhookSecret": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              },
              "examples": {
                "dropOff": {
                  "$ref": "#/components/examples/WebhookEventDropOff"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any `2xx` to acknowledge."
          },
          "401": {
            "description": "Return this if the `Authorization` header does not match your configured\nBearer secret.\n"
          },
          "4XX": {
            "description": "Any other `4xx` is a failed delivery and is not retried. Lifecycle events\nare not replayable — subscribe reliably rather than relying on recovery.\n"
          },
          "5XX": {
            "description": "A `5xx` from your receiver is retried up to three times. Answer within 30\nseconds; a timeout is not retried.\n"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "api_token",
        "description": "Bearer token generated in **API Token** in your workspace's left-hand menu in the UpPass Portal.\n\n\nSend it on every request as `Authorization: Bearer {api_token}`.\n\n\n> **Keep the token server-side.** A token shipped in browser or mobile-app code\n> lets anyone create applications against your workspace, and the verifications\n> they trigger consume your credit.\n\n\nTwo failure modes, which the API distinguishes:\n\n\n| Situation | Status |\n|---|---|\n| Header present, token invalid or revoked | `401 Unauthorized` |\n| Header absent entirely | `403 Forbidden` |\n"
      },
      "WebhookSecret": {
        "type": "http",
        "scheme": "bearer",
        "description": "The Bearer secret you configured in **Portal → Connect → Add Webhook**.\n\n\nUpPass sends it as `Authorization: Bearer {secret}` on every delivery.\n**Reject any inbound request whose header does not match** — this is the\nonly thing authenticating the caller.\n"
      }
    },
    "parameters": {
      "Lang": {
        "name": "lang",
        "in": "path",
        "required": true,
        "description": "UI language for the generated form, as a path prefix — for example `en` or `th`.\n\n\nThis is the **platform** locale list. Each form exposes its own subset via\n`schema_config.configs.locale.available_locales` on the\n[Get application info](#tag/applications/getAppliedFormInfo) response; a locale outside\nthat subset is accepted here but the form still renders in its default locale.\n\n\nAn unrecognised value is **not** rejected — the request is answered with a\n`302` redirect to a path prefixed with the platform default locale, which will\nnot resolve. Validate the locale before you build the URL.\n",
        "schema": {
          "type": "string",
          "default": "en",
          "enum": [
            "en",
            "th",
            "zh-hans",
            "zh-hant",
            "km",
            "my",
            "lo",
            "ms",
            "vi",
            "id",
            "ja",
            "hi",
            "ar",
            "ru",
            "de",
            "es",
            "fr"
          ],
          "example": "en"
        }
      },
      "FormSlug": {
        "name": "form_slug",
        "in": "path",
        "required": true,
        "description": "The flow to create an application against. Find it in\n**UpPass Portal → Flows → your flow → Settings**.\n",
        "schema": {
          "type": "string",
          "example": "YOUR_FORM_SLUG"
        }
      },
      "Slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "Application identifier returned as `detail.slug` by\n[Create application](#tag/applications/createAppliedForm).\n\n\n> **Treat the slug as a secret.** It is the only credential protecting\n> [Get application info](#tag/applications/getAppliedFormInfo), which is served\n> without a bearer token.\n",
        "schema": {
          "type": "string",
          "example": "YOUR_APPLICATION_SLUG"
        }
      }
    },
    "schemas": {
      "AnswerInput": {
        "type": "object",
        "title": "AnswerInput",
        "description": "Flat map of `question_key` → value. Values are strings, numbers or booleans —\nnever nested objects.\n",
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            }
          ]
        },
        "example": {
          "ekyc_document_type": "passport",
          "ekyc_document_country": "THA",
          "name_prefix": "Mr.",
          "gender": "M",
          "nid": "1111111111119",
          "date_of_birth": "1990-01-15",
          "consent_checkbox_1": true
        }
      },
      "CreateFormRequest": {
        "type": "object",
        "title": "CreateFormRequest",
        "description": "Body for creating an application. Optional: an empty body creates an application\nwith no pre-filled answers.\n",
        "properties": {
          "answers": {
            "description": "Pre-filled answers, keyed by the form's field name as shown in the Builder.\nAny key that is not a field on the form — and not declared under\n**Form → Builder → Input Parameter** — is **accepted and silently\ndiscarded**. That includes your own references such as `transaction_id` in\nthe example: they only come back in the webhook if you have declared them\nas input parameters first.\n\n\nValues you supply are validated for **format** (regex, checksum, type) at\ncreation. Only `required` checks are deferred to submission. See\n[Validation](https://docs.uppass.io/guides/validation/).\n",
            "allOf": [
              {
                "$ref": "#/components/schemas/AnswerInput"
              }
            ]
          }
        },
        "example": {
          "answers": {
            "ekyc_document_type": "passport",
            "transaction_id": "TXN-0001"
          }
        }
      },
      "CreateFormResponse": {
        "type": "object",
        "title": "CreateFormResponse",
        "description": "The created application. `form_url` is where the applicant goes; `detail.slug` is\nwhat you store against your own record.\n",
        "required": [
          "info",
          "detail",
          "form_url"
        ],
        "properties": {
          "form_url": {
            "type": "string",
            "format": "uri",
            "description": "The applicant's unique form URL. Redirect a mobile browser here, load it in\na `WebView` / `WKWebView`, or render it as a QR code for a desktop visitor.\n\n\n**Never reuse a `form_url` across applicants** — it is bound to one\napplication.\n",
            "example": "https://app.uppass.io/en/form/YOUR_FORM_SLUG/YOUR_APPLICATION_SLUG/"
          },
          "info": {
            "type": "string",
            "format": "uri",
            "description": "Absolute URL of the [Get application info](#tag/applications/getAppliedFormInfo)\nendpoint for this application.\n\n\nFor server-side polling prefer\n[Get application result](#tag/applications/getAppliedFormResult) — it returns the\nwebhook payload in ~3 kB rather than the full form schema in ~57 kB.\n",
            "example": "https://app.uppass.io/en/api/forms/YOUR_FORM_SLUG/applied-forms/YOUR_APPLICATION_SLUG/info/"
          },
          "detail": {
            "type": "object",
            "description": "Identifiers and current position for the application just created.",
            "required": [
              "form",
              "slug"
            ],
            "properties": {
              "form": {
                "type": "string",
                "description": "Echoes the `form_slug` from the path.",
                "example": "YOUR_FORM_SLUG"
              },
              "slug": {
                "type": "string",
                "description": "Application identifier. **Store it against your own record now** — it is\nyour correlation key, and it comes back in the webhook as\n`application.slug`.\n",
                "example": "YOUR_APPLICATION_SLUG"
              },
              "step": {
                "type": "string",
                "description": "First step of the flow. Flow-specific — read it from the response rather\nthan assuming a value.\n",
                "example": "personal"
              },
              "section": {
                "type": "string",
                "description": "First section within `step`.",
                "example": "personal"
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "`null` until the applicant submits.\n",
                "example": null
              }
            }
          }
        }
      },
      "ValidationErrors": {
        "type": "object",
        "title": "ValidationErrors",
        "description": "Field-level validation failures, keyed by `question_key`. Every value is an\narray of messages.\n",
        "additionalProperties": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "example": {
          "ekyc_document": [
            "The ekyc_document field is required."
          ],
          "ekyc_liveness": [
            "The field is required."
          ],
          "nid": [
            "The ID Card Number is invalid (checksum)"
          ]
        }
      },
      "Error": {
        "type": "object",
        "title": "Error",
        "description": "Standard error envelope. Returned by every endpoint **except** two documented\nexceptions — see [Error handling](https://docs.uppass.io/guides/errors/).\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "description": "Wrapper carrying the status code, its reason phrase, and the detail. Present on\nevery error except the two documented exceptions.\n",
            "required": [
              "status_code",
              "message",
              "detail"
            ],
            "properties": {
              "status_code": {
                "type": "integer",
                "description": "Repeats the HTTP status code.",
                "example": 401
              },
              "message": {
                "type": "string",
                "description": "Reason phrase for the status code.",
                "example": "Unauthorized"
              },
              "detail": {
                "description": "A human-readable string for transport errors, or — on `422` — an object\nmapping each rejected field to an array of messages.\n",
                "oneOf": [
                  {
                    "type": "string",
                    "example": "No permission -- see authorization schemes"
                  },
                  {
                    "$ref": "#/components/schemas/ValidationErrors"
                  }
                ]
              }
            }
          }
        }
      },
      "OtherStatus": {
        "type": "object",
        "title": "OtherStatus",
        "description": "Per-check outcomes for the application.\n\n\n### Only `ekyc` is part of the contract\n\n\n`ekyc` is always present and always means the same thing. **Every other key is a\nstatus column configured in your workspace's Decision Flow** — its name, its\nvalues and whether it appears at all are yours to define, and renaming a column\nin the Portal changes the key on the wire.\n\n\nSo: read `ekyc` by name, and treat anything else as workspace-specific. Never\nhard-code another key without a fallback, and never assume one is present.\n\n\n### `ekyc` alone is not a verdict\n\n\nBefore `application.status` is `complete` it carries no meaning — it may read\n`pending` or even `pass` on an application nothing has been captured for. Gate on\n`status === \"complete\"` first. For what the verification actually produced, read\n[`extra`](#tag/webhooks/webhookSubmission) rather than a status column.\n",
        "properties": {
          "ekyc": {
            "type": "string",
            "description": "Document and liveness outcome, combined, with the face-comparison score\nfolded in. Lower-case.\n\n\n| Value | Meaning |\n|---|---|\n| `pass` | Document scan and liveness both passed, and the face-comparison score is 70 or above (`face_compare.status` is `match`). |\n| `need_review` | Both passed, but the face-comparison score is 61–69 (`face_compare.status` is `need_review`). Route to a human. |\n| `fail` | Either check failed, or the face-comparison score is 60 or below (`face_compare.status` is `not_match`). |\n| `pending` | The checks have not completed. |\n\n\nBefore `application.status` is `complete` this value carries no meaning — it may\nread `pending` or even `pass`. Check `status` first.\n",
            "enum": [
              "pass",
              "need_review",
              "fail",
              "pending"
            ],
            "example": "pass"
          }
        },
        "additionalProperties": {
          "type": "string",
          "description": "A status column configured in your Decision Flow. Naming and values are\nworkspace-defined; `-` conventionally means the check has not run.\n"
        },
        "example": {
          "ekyc": "pass"
        }
      },
      "Application": {
        "type": "object",
        "title": "Application",
        "description": "Identifiers and outcomes for one applicant's pass through one flow.\n\n\n`slug` is the correlation key to store against your own record. `status` gates\nwhether any other verdict in the payload is meaningful.\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal application ID. Note that\n[Get application info](#tag/applications/getAppliedFormInfo) exposes this same\nvalue as `data.id` — **not** as `data.application.id`, which is a different\nnumber.\n",
            "example": 100001
          },
          "no": {
            "type": "string",
            "description": "Human-readable application number. Derived as `COK00` + zero-padded `id`, so\nit is not an independent identifier. Quote it to UpPass support.\n",
            "example": "COK00100001"
          },
          "form": {
            "type": "string",
            "description": "The `form_slug` this application belongs to.",
            "example": "YOUR_FORM_SLUG"
          },
          "slug": {
            "type": "string",
            "description": "Application slug — your correlation key.",
            "example": "YOUR_APPLICATION_SLUG"
          },
          "status": {
            "type": "string",
            "description": "Overall application status.",
            "enum": [
              "incomplete",
              "complete",
              "expired"
            ],
            "example": "complete"
          },
          "other_status": {
            "$ref": "#/components/schemas/OtherStatus"
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the applicant submitted. **This — not `event.created_at` — is the\nsubmission time.**\n",
            "example": "2026-08-26T09:11:17.574375+00:00"
          }
        }
      },
      "Answer": {
        "type": "object",
        "title": "Answer",
        "description": "One captured answer. Read the value at `.value`.",
        "required": [
          "value"
        ],
        "properties": {
          "value": {
            "description": "The captured value. Usually a string; `boolean` for checkboxes (for example\n`consent_checkbox_1`).\n",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "number"
              },
              {
                "type": "boolean"
              }
            ],
            "example": "1111111111119"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this field was first captured.",
            "example": "2026-08-11T06:10:06.363394+00:00"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this field was last written. Differs from `created_at` when the applicant\ncorrected a value or re-scanned the document.\n",
            "example": "2026-08-11T06:10:06.363419+00:00"
          }
        }
      },
      "AnswerMap": {
        "type": "object",
        "title": "AnswerMap",
        "description": "Every captured value, keyed by `question_key` — OCR output from the document,\nanswers the applicant typed, and any input parameters you passed at creation.\n\n\n### Treat this as a sparse map\n\n\n**The key set is not fixed.** It varies with the document type, how the form is built, and which optional steps the flow contains.\nRead every field defensively; do not destructure.\n\n\n### The keys\n\n\nPresence varies by document type and flow configuration — noted per key below.\nAnything not noted appears on both National ID and passport submissions.\n\n\n| `question_key` | Type | Meaning |\n|---|---|---|\n| `nid` | string | Thai national ID, check-digit validated. **National ID submissions only.** |\n| `document_number` | string | Number printed on the document. |\n| `ekyc_document_type` | enum | Document actually scanned: `front_card` (National ID) or `passport`. |\n| `ekyc_document_country` | string | ISO 3166-1 alpha-3 issuing country. |\n| `name_prefix` | string | Title. Not cross-validated against `gender`. |\n| `gender` | enum | `M` or `F`. |\n| `full_name_type` | enum | `parts` or `full`. |\n| `full_name_first_name` / `_last_name` | string | **Applicant-editable.** Local script. |\n| `full_name_en_first_name` / `_en_last_name` | string | **Read-only, from OCR.** Latin script. |\n| `date_of_birth` / `_issue` / `_expiry` | date | ISO dates. Check expiry against the submission date. |\n| `home_address_country` | string | ISO 3166-1 alpha-3. Always present. |\n| `home_address_address`, `_subdistrict`, `_district`, `_province`, `_zipcode` | string | Thai address parts. |\n| `home_address_address_1_common`, `_address_2_common`, `_city_common`, `_postal_code_common` | string | International address parts — lines 1 and 2, city, postal code. |\n| `home_address_full` | string | Derived single line. Absent on some submissions. |\n| `consent_version` / `consent_date` / `consent_checkbox_1` | mixed | Consent audit trail. `consent_date` uses a `Z` suffix, unlike every other timestamp. |\n\n\n### ⚠️ The screened name is not always the name on the document\n\n\n`full_name_first_name` / `full_name_last_name` are editable inputs;\n`full_name_en_first_name` / `full_name_en_last_name` are read-only fields\npopulated from OCR. **AML screening runs against the editable pair** — see\n`extra.identity_aml.*.input`.\n\n\nCompare the two pairs and treat any mismatch as a review signal: it means the\nname that was screened is not the name printed on the document.\n",
        "additionalProperties": {
          "$ref": "#/components/schemas/Answer"
        }
      },
      "SignedImageUrl": {
        "type": "string",
        "format": "uri",
        "title": "SignedImageUrl",
        "description": "Signed, short-lived URL for a captured image, on the `app.uppass.io` host.\n\n\n### ⚠️ These URLs live 15 minutes\n\n\nThe `exp` query parameter is a Unix timestamp set **15 minutes after\n`event.created_at`**. The clock starts at delivery, not at submission.\n\n\nThis constrains how you may process the webhook. Returning `2xx` immediately and\nqueueing the payload is correct for everything *except* the images: a queue that\nfalls more than 15 minutes behind loses them. **Fetch images on a priority path\ninside the window** and copy them to your own storage; never persist an UpPass\nURL as a permanent reference.\n\n\nIf you do miss the window, it is recoverable —\n[Resend webhook](#tag/webhooks/resendWebhook) mints fresh URLs.\n",
        "example": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738493"
      },
      "LivenessAttempt": {
        "type": "object",
        "title": "LivenessAttempt",
        "description": "One facial-liveness attempt, with the frames captured during it. More than one\nattempt on an application means the applicant retried.\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal attempt identifier.",
            "example": 100010
          },
          "attempt": {
            "type": "integer",
            "description": "1-based counter. More than one entry means the applicant retried.",
            "example": 1
          },
          "status": {
            "type": "string",
            "description": "Outcome of this attempt.",
            "enum": [
              "success",
              "fail"
            ],
            "example": "success"
          },
          "message": {
            "type": "string",
            "description": "Reserved for a failure reason. Empty on failed attempts as well as successful ones — do not rely on it for diagnostics.\n",
            "example": ""
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this attempt was captured.",
            "example": "2026-08-26T09:11:03.137966+00:00"
          },
          "images": {
            "type": "array",
            "description": "Frames captured during this attempt.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "$ref": "#/components/schemas/SignedImageUrl"
                },
                "action": {
                  "type": "string",
                  "description": "The liveness action captured.",
                  "example": "idle"
                }
              }
            }
          }
        }
      },
      "DocumentAttempt": {
        "type": "object",
        "title": "DocumentAttempt",
        "description": "One document-scan attempt. `type` reports the document actually presented, which\ncan differ from the one you requested at creation.\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal attempt identifier. Quote it to support when reporting a scan problem.",
            "example": 100011
          },
          "attempt": {
            "type": "integer",
            "description": "1-based counter.",
            "example": 1
          },
          "type": {
            "type": "string",
            "description": "Document scanned on this attempt.",
            "enum": [
              "front_card",
              "passport"
            ],
            "example": "passport"
          },
          "url": {
            "$ref": "#/components/schemas/SignedImageUrl"
          },
          "status": {
            "type": "string",
            "description": "Outcome of this scan attempt.",
            "enum": [
              "success",
              "fail"
            ],
            "example": "success"
          },
          "message": {
            "type": "string",
            "description": "Reserved for a failure reason. Empty on failed attempts as well as successful ones — use `status` instead.\n",
            "example": ""
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this attempt was captured.",
            "example": "2026-08-26T09:09:58.660347+00:00"
          }
        }
      },
      "EkycArtifacts": {
        "type": "object",
        "title": "EkycArtifacts",
        "description": "Document scan, facial liveness and face-comparison artefacts.",
        "properties": {
          "face_compare": {
            "type": "object",
            "description": "Similarity between the liveness selfie and the document portrait.",
            "properties": {
              "score": {
                "type": "number",
                "format": "float",
                "minimum": 0,
                "maximum": 100,
                "description": "Similarity score, 0–100.",
                "example": 85.72
              },
              "status": {
                "type": "string",
                "description": "The match verdict, set from `score`:\n\n\n| `score` | `status` |\n|---|---|\n| 70 or above | `match` |\n| 61–69 | `need_review` |\n| 60 or below | `not_match` |\n",
                "enum": [
                  "match",
                  "need_review",
                  "not_match"
                ],
                "example": "match"
              }
            }
          },
          "liveness": {
            "type": "object",
            "description": "Facial liveness check. `url` is the best frame across all attempts; `attempts`\ncarries one entry per try.\n",
            "properties": {
              "url": {
                "$ref": "#/components/schemas/SignedImageUrl"
              },
              "attempts": {
                "type": "array",
                "description": "One entry per attempt, in order. More than one entry means the applicant\nretried — worth recording even when the last attempt succeeded.\n",
                "items": {
                  "$ref": "#/components/schemas/LivenessAttempt"
                }
              }
            }
          },
          "identity_document": {
            "type": "object",
            "description": "The accepted document scan, the portrait cropped from it, and one entry per scan\nattempt.\n",
            "properties": {
              "type": {
                "type": "string",
                "description": "Document type accepted.",
                "enum": [
                  "front_card",
                  "passport"
                ],
                "example": "passport"
              },
              "url": {
                "$ref": "#/components/schemas/SignedImageUrl"
              },
              "face_image_url": {
                "$ref": "#/components/schemas/SignedImageUrl"
              },
              "attempts": {
                "type": "array",
                "description": "One entry per scan attempt, in order.",
                "items": {
                  "$ref": "#/components/schemas/DocumentAttempt"
                }
              }
            }
          }
        }
      },
      "AmlSearchInput": {
        "type": "object",
        "title": "AmlSearchInput",
        "description": "Exactly what was screened.\n\n\nCompare `first_name` / `last_name` against `answers.full_name_en_first_name` /\n`full_name_en_last_name`: these come from the **editable** name fields, so a\nmismatch means the screened name differs from the name on the document.\n",
        "properties": {
          "first_name": {
            "type": "string",
            "description": "Given name that was screened, taken from `answers.full_name_first_name` — the\napplicant-editable field, not the read-only OCR field.\n",
            "example": "Anucha"
          },
          "last_name": {
            "type": "string",
            "description": "Family name that was screened, taken from `answers.full_name_last_name`.\n",
            "example": "Example"
          },
          "date_of_birth": {
            "type": "string",
            "format": "date",
            "description": "Only the **year** is used as a search filter.",
            "example": "1970-01-01"
          }
        }
      },
      "AmlAggregateFlags": {
        "type": "object",
        "title": "AmlAggregateFlags",
        "description": "Verdict across every search in this screening pass. `is_any_*` is true when at\nleast one search matched; `is_all_*` when every search did.\n\n\n**For a single-applicant flow the two are equivalent — use `is_any_*`.**\n",
        "properties": {
          "is_any_found": {
            "type": "boolean",
            "description": "Any list match at all.",
            "example": true
          },
          "is_all_found": {
            "type": "boolean",
            "description": "True when *every* search matched. Equivalent to `is_any_found` on a single-applicant flow.",
            "example": true
          },
          "is_any_in_sanctions": {
            "type": "boolean",
            "description": "Matched a sanctions list.",
            "example": true
          },
          "is_all_in_sanctions": {
            "type": "boolean",
            "description": "True when every search matched a sanctions list.",
            "example": true
          },
          "is_any_in_pep": {
            "type": "boolean",
            "description": "Matched a politically-exposed-person list.",
            "example": true
          },
          "is_all_in_pep": {
            "type": "boolean",
            "description": "True when every search matched a PEP list.",
            "example": true
          },
          "is_any_in_warnings": {
            "type": "boolean",
            "description": "Matched a regulator or law-enforcement warning list.",
            "example": false
          },
          "is_all_in_warnings": {
            "type": "boolean",
            "description": "True when every search matched a warning list.",
            "example": false
          },
          "is_any_in_adverse_media": {
            "type": "boolean",
            "description": "Matched adverse media.",
            "example": false
          },
          "is_all_in_adverse_media": {
            "type": "boolean",
            "description": "True when every search matched adverse media.",
            "example": false
          },
          "is_any_in_fitness_probity": {
            "type": "boolean",
            "description": "Matched a fitness-and-probity list.",
            "example": false
          },
          "is_all_in_fitness_probity": {
            "type": "boolean",
            "description": "True when every search matched a fitness-and-probity register.",
            "example": false
          }
        }
      },
      "AmlSearchFlags": {
        "type": "object",
        "title": "AmlSearchFlags",
        "description": "Verdict for one search.",
        "properties": {
          "is_found": {
            "type": "boolean",
            "description": "Any list match at all for this search.",
            "example": true
          },
          "total_hits": {
            "type": "integer",
            "description": "Number of matching records.",
            "example": 9
          },
          "is_in_sanctions": {
            "type": "boolean",
            "description": "Matched a sanctions list — UN, OFAC, HM Treasury, EU or a national equivalent.",
            "example": true
          },
          "is_in_pep": {
            "type": "boolean",
            "description": "Matched a politically-exposed-person list.",
            "example": true
          },
          "is_in_warnings": {
            "type": "boolean",
            "description": "Matched a regulator or law-enforcement warning list.",
            "example": false
          },
          "is_in_adverse_media": {
            "type": "boolean",
            "description": "Matched an adverse-media record.",
            "example": false
          },
          "is_in_fitness_probity": {
            "type": "boolean",
            "description": "Matched a fitness-and-probity register.",
            "example": false
          }
        }
      },
      "AmlHit": {
        "type": "object",
        "title": "AmlHit",
        "description": "One matching record from a sanctions, PEP, warning or adverse-media list.",
        "properties": {
          "score": {
            "type": "number",
            "format": "float",
            "description": "Provider match score. Higher is a stronger match.",
            "example": 1.9
          },
          "match_types": {
            "type": "array",
            "description": "Why this record matched.",
            "items": {
              "type": "string"
            },
            "example": [
              "aka_exact",
              "year_of_birth"
            ]
          },
          "match_types_details": {
            "type": "array",
            "description": "Per-source breakdown of which query terms matched.",
            "items": {
              "type": "object"
            }
          },
          "doc": {
            "type": "object",
            "description": "The matched record.",
            "properties": {
              "id": {
                "type": "string",
                "description": "Provider's stable identifier for this record. Use it to deduplicate across searches.",
                "example": "EXAMPLEDOCID000"
              },
              "name": {
                "type": "string",
                "description": "Primary name on the matched record.",
                "example": "Anucha Example"
              },
              "entity_type": {
                "type": "string",
                "description": "Whether the record describes an individual or an organisation.",
                "enum": [
                  "person",
                  "organisation"
                ],
                "example": "person"
              },
              "types": {
                "type": "array",
                "description": "Categories this record belongs to — for example `sanction`, `pep`,\n`pep-class-1`, `warning`, `fitness-probity`, or one of the\n`adverse-media-*` families.\n",
                "items": {
                  "type": "string"
                },
                "example": [
                  "sanction"
                ]
              },
              "sources": {
                "type": "array",
                "description": "Source lists carrying this record.",
                "items": {
                  "type": "string"
                },
                "example": [
                  "un-consolidated",
                  "hm-treasury-list",
                  "dfat-australia-list"
                ]
              },
              "aka": {
                "type": "array",
                "description": "Known aliases. Can run to several hundred entries.",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "One alias."
                    }
                  }
                }
              },
              "fields": {
                "type": "array",
                "description": "Attributes per source — nationality, date of birth, passport numbers,\naddresses, designation acts, programme names.\n",
                "items": {
                  "type": "object"
                }
              },
              "media": {
                "type": "array",
                "description": "Adverse-media articles. Present on adverse-media matches only.",
                "items": {
                  "type": "object"
                }
              },
              "associates": {
                "type": "array",
                "description": "Related entities named by the source lists.",
                "items": {
                  "type": "object"
                }
              },
              "keywords": {
                "type": "array",
                "description": "Provider-assigned keywords for the record.",
                "items": {
                  "type": "string"
                }
              },
              "assets": {
                "type": "array",
                "description": "Provider-hosted documents and images for this record.",
                "items": {
                  "type": "object"
                }
              },
              "source_notes": {
                "type": "object",
                "description": "Provenance for each entry in `sources`."
              },
              "created_utc": {
                "type": "string",
                "format": "date-time",
                "description": "When the provider first created this record."
              },
              "last_updated_utc": {
                "type": "string",
                "format": "date-time",
                "description": "When the provider last updated the record. Useful for judging how current a\nmatch is.\n"
              }
            }
          }
        }
      },
      "ComplyAdvantageData": {
        "type": "object",
        "title": "ComplyAdvantageData",
        "description": "ComplyAdvantage search record, passed through as received.\n\n\n### ⚠️ Size\n\n\nThis object is unbounded and dominates the payload. A single sanctions match can\ncarry hundreds of alias entries and dozens of adverse-media article snippets\nacross a dozen source lists. **Size-test your receiver against a real\nsanctions-match payload** before go-live — body-size limits on reverse proxies\nand serverless gateways are a realistic failure mode here.\n\n\nPrefer `AmlSearchFlags` and `AmlAggregateFlags` for decisions; treat this object\nas evidence to archive.\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Provider search ID.",
            "example": 1000001
          },
          "ref": {
            "type": "string",
            "description": "Provider search reference.",
            "example": "1000001-example"
          },
          "search_term": {
            "type": "string",
            "description": "The name string the provider searched, as normalised by the provider.",
            "example": "Anucha Example"
          },
          "submitted_term": {
            "type": "string",
            "description": "The name string as submitted, before provider normalisation.",
            "example": "Anucha Example"
          },
          "match_status": {
            "type": "string",
            "description": "Provider triage state.",
            "enum": [
              "no_match",
              "potential_match",
              "false_positive",
              "true_positive",
              "unknown"
            ],
            "example": "potential_match"
          },
          "risk_level": {
            "type": "string",
            "description": "Provider risk banding. `unknown` on an automated search that no one has\nadjudicated — do not treat it as a risk assessment.\n",
            "example": "unknown"
          },
          "total_hits": {
            "type": "integer",
            "description": "Number of matching records in `hits`, before any adjudication.",
            "example": 9
          },
          "total_matches": {
            "type": "integer",
            "description": "Number of records the provider counts as matches. Equals `total_hits` on a fresh search.",
            "example": 9
          },
          "total_blacklist_hits": {
            "type": "integer",
            "description": "Matches against your own uploaded blacklist, if the workspace maintains one.",
            "example": 0
          },
          "filters": {
            "type": "object",
            "description": "Search parameters actually used. **`fuzziness: 0` with `exact_match: true`\nmeans no fuzzy matching**, and only the birth *year* is filtered on.\n",
            "properties": {
              "fuzziness": {
                "type": "number",
                "description": "Provider fuzzy-matching tolerance, 0–1. **`0` means exact matching only** — a\ndifferent transliteration of the same name will not match.\n",
                "example": 0
              },
              "exact_match": {
                "type": "boolean",
                "description": "Whether every query term had to match exactly.",
                "example": true
              },
              "birth_year": {
                "type": "integer",
                "description": "Year extracted from the applicant's date of birth. Only the year is filtered\non, so a same-name match in the same year surfaces even when day and month differ.\n",
                "example": 1970
              },
              "types": {
                "type": "array",
                "description": "List categories searched. Empty means all categories.",
                "items": {
                  "type": "string"
                }
              },
              "country_codes": {
                "type": "array",
                "description": "Country filter applied. Empty means no country restriction.",
                "items": {
                  "type": "string"
                }
              },
              "remove_deceased": {
                "type": "integer",
                "description": "`1` excludes records marked deceased; `0` keeps them.",
                "example": 0
              }
            }
          },
          "hits": {
            "type": "array",
            "description": "Matching records. Empty when `match_status` is `no_match`.",
            "items": {
              "$ref": "#/components/schemas/AmlHit"
            }
          },
          "blacklist_hits": {
            "type": "array",
            "description": "Matches against your own uploaded blacklist.",
            "items": {
              "type": "object"
            }
          },
          "tags": {
            "type": "array",
            "description": "Provider-side tags applied to this search.",
            "items": {
              "type": "object"
            }
          },
          "labels": {
            "type": "array",
            "description": "Provider-side labels applied to this search.",
            "items": {
              "type": "string"
            }
          },
          "limit": {
            "type": "integer",
            "description": "Provider result cap for this search.",
            "example": 500
          },
          "offset": {
            "type": "integer",
            "description": "Pagination offset used for this search.",
            "example": 0
          },
          "client_ref": {
            "type": "string",
            "nullable": true,
            "description": "Provider-side client reference. Not populated by UpPass.",
            "example": null
          },
          "assignee_id": {
            "type": "integer",
            "description": "Provider account identifiers. Internal to the screening provider.",
            "example": 100000
          },
          "searcher_id": {
            "type": "integer",
            "description": "Provider account that ran the search. Internal to the screening provider.",
            "example": 100000
          },
          "created_at": {
            "type": "string",
            "description": "Provider timestamp. Space-separated, **not** ISO-8601.",
            "example": "2026-08-26 09:11:20"
          },
          "updated_at": {
            "type": "string",
            "description": "When the provider last updated this search record. Space-separated, **not** ISO-8601.",
            "example": "2026-08-26 09:11:20"
          }
        }
      },
      "AmlProviderSearch": {
        "type": "object",
        "title": "AmlProviderSearch",
        "description": "One search against the provider, with its own verdict and the provider's raw\nresponse.\n",
        "properties": {
          "input": {
            "$ref": "#/components/schemas/AmlSearchInput"
          },
          "result": {
            "$ref": "#/components/schemas/AmlSearchFlags"
          },
          "output": {
            "type": "object",
            "description": "The provider's raw response, passed through unmodified.",
            "properties": {
              "code": {
                "type": "integer",
                "description": "Provider HTTP status.",
                "example": 200
              },
              "status": {
                "type": "string",
                "description": "Provider-side outcome of the search **request**, not of the screening. Values\nare the provider's own and not a closed set; `success` is the only one\nobserved. Branch on `result`, never on this.\n",
                "example": "success"
              },
              "content": {
                "type": "object",
                "description": "Wrapper the provider returns around its search record.",
                "properties": {
                  "data": {
                    "$ref": "#/components/schemas/ComplyAdvantageData"
                  }
                }
              }
            }
          }
        }
      },
      "AmlScreening": {
        "type": "object",
        "title": "AmlScreening",
        "description": "One screening pass for this application.\n\n\nRead the aggregate verdict at `result`. `output.comply_advantage[]` holds the\nindividual searches and the provider's raw response — archive those as evidence\nrather than branching on them.\n",
        "properties": {
          "input": {
            "$ref": "#/components/schemas/AmlSearchInput"
          },
          "result": {
            "$ref": "#/components/schemas/AmlAggregateFlags"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "When screening completed.",
            "example": "2026-08-26T09:11:20.644965+00:00"
          },
          "values_list": {
            "type": "array",
            "description": "Which aggregate flags the workspace's Decision Flow is configured to act on.\n",
            "items": {
              "type": "string"
            },
            "example": [
              "is_any_in_adverse_media",
              "is_any_in_warnings",
              "is_any_in_fitness_probity",
              "is_any_in_sanctions",
              "is_any_in_pep"
            ]
          },
          "output": {
            "type": "object",
            "description": "Per-search results. Nested under the same key name as the parent array.",
            "properties": {
              "comply_advantage": {
                "type": "array",
                "description": "One entry per search performed in this pass. Nested under the same key name\nas the parent array — see the note on `IdentityAml`.\n",
                "items": {
                  "$ref": "#/components/schemas/AmlProviderSearch"
                }
              }
            }
          }
        }
      },
      "IdentityAml": {
        "type": "object",
        "title": "IdentityAml",
        "description": "AML, sanctions, PEP and adverse-media screening results, keyed by provider.\n\n\nPresent only on flows with a screening step bound to their Decision Flow. The\nobject is **omitted** when no screening ran — which is not the same as a clean\nresult.\n\n\n`other_status.ekyc` covers the document and liveness checks only, and is\nindependent of anything reported here.\n\n\n### Reading it\n\n\n`comply_advantage` appears **twice, nested inside itself**. Two summary objects\nsave you from walking the raw provider response:\n\n\n| What you want | Path |\n|---|---|\n| Verdict across all searches | `comply_advantage[0].result` |\n| Verdict for one search | `comply_advantage[0].output.comply_advantage[0].result` |\n| Raw provider hits | `comply_advantage[0].output.comply_advantage[0].output.content.data.hits` |\n\n\nPrefer the two `result` objects. The raw path is seven levels deep and its shape\nis the provider's, not ours.\n\n\n### ⚠️ Screening is exact-match only\n\n\nSearches run with `fuzziness: 0` and `exact_match: true`, against name plus\nbirth **year**. A different transliteration of the same name will not match. If\nyour risk policy needs fuzzy screening, run it yourself on top.\n",
        "properties": {
          "comply_advantage": {
            "type": "array",
            "description": "One entry per screening pass performed for this application.",
            "items": {
              "$ref": "#/components/schemas/AmlScreening"
            }
          }
        }
      },
      "BankStatement": {
        "type": "object",
        "title": "BankStatement",
        "description": "Bank-statement analysis. Present only on flows containing a bank-statement step.\n",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Internal bank-statement analysis identifier."
          },
          "validation_pass": {
            "type": "boolean",
            "description": "Overall validation outcome."
          },
          "validation": {
            "type": "object",
            "description": "Per-check validation outcomes across all uploaded statements.",
            "properties": {
              "validation_transaction_min_month": {
                "type": "boolean",
                "description": "Whether the statements span the minimum number of months the flow requires."
              },
              "validation_transaction_latest_after": {
                "type": "boolean",
                "description": "Whether the most recent transaction is recent enough."
              },
              "validation_transaction_oldest_before": {
                "type": "boolean",
                "description": "Whether the oldest transaction reaches far enough back."
              }
            }
          },
          "validation_note": {
            "type": "object",
            "description": "Why a check in `validation` failed, keyed by the same names."
          },
          "margin": {
            "type": "object",
            "description": "Derived income and expenditure margins."
          },
          "casa_weighted_pct": {
            "type": "object",
            "nullable": true,
            "description": "Weighted current-and-savings-account share. `null` when not computed."
          },
          "documents": {
            "type": "array",
            "description": "One entry per uploaded statement.",
            "items": {
              "type": "object",
              "description": "One uploaded statement, with OCR metadata, per-document validation flags\nand a `transactions` array.\n"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the analysis was created."
          },
          "deleted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Set when the analysis was soft-deleted. `null` otherwise."
          }
        }
      },
      "WebhookExtra": {
        "type": "object",
        "title": "WebhookExtra",
        "description": "Verification artefacts. **Answers are not here** — they are the top-level\n`answers` object.\n",
        "properties": {
          "ekyc": {
            "$ref": "#/components/schemas/EkycArtifacts"
          },
          "identity_aml": {
            "$ref": "#/components/schemas/IdentityAml"
          },
          "bank_statement": {
            "$ref": "#/components/schemas/BankStatement"
          }
        }
      },
      "AppliedFormResult": {
        "type": "object",
        "title": "AppliedFormResult",
        "description": "The same shape the webhook delivers, minus `event`. Poll this when you need\nstate server-side.\n",
        "properties": {
          "application": {
            "description": "Identifiers and outcomes for the application.",
            "allOf": [
              {
                "$ref": "#/components/schemas/Application"
              }
            ]
          },
          "answers": {
            "$ref": "#/components/schemas/AnswerMap"
          },
          "extra": {
            "$ref": "#/components/schemas/WebhookExtra"
          }
        }
      },
      "AppliedFormInfo": {
        "type": "object",
        "title": "AppliedFormInfo",
        "description": "Everything the hosted form needs to render itself, plus current state.\n\n\nRoughly **57 kB**. This endpoint exists to serve the form UI; for server-side\nstate use [Get application result](#tag/applications/getAppliedFormResult) instead.\n",
        "properties": {
          "data": {
            "type": "object",
            "description": "Current state. The only part most integrations need.",
            "properties": {
              "id": {
                "type": "integer",
                "description": "**This** is the value that matches `application.id` in the webhook and in\n[Get application result](#tag/applications/getAppliedFormResult) — not\n`data.application.id` below, which is a different number.\n",
                "example": 100001
              },
              "slug": {
                "type": "string",
                "description": "Application slug, echoing the one in the path.",
                "example": "YOUR_APPLICATION_SLUG"
              },
              "form_slug": {
                "type": "string",
                "description": "The flow this application belongs to.",
                "example": "YOUR_FORM_SLUG"
              },
              "form_name": {
                "type": "string",
                "description": "Human-readable flow name, as set in the Portal.",
                "example": "eKYC"
              },
              "is_submitted": {
                "type": "boolean",
                "description": "Whether the applicant has submitted. Mirrors `submitted_at` being non-null.",
                "example": false
              },
              "is_blocked": {
                "type": "boolean",
                "description": "True when the application is barred from progressing — for example after hitting an attempt limit.",
                "example": false
              },
              "can_update": {
                "type": "boolean",
                "description": "Whether answers may still be written. False once submitted.",
                "example": true
              },
              "submitted_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the applicant submitted. `null` while in progress.",
                "example": null
              },
              "created_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the application was created."
              },
              "current_step": {
                "type": "string",
                "description": "Step the applicant is on.\n\n\n> Not guaranteed to be a key of `schema.steps` — resolve it with a\n> fallback rather than indexing directly.\n",
                "example": "personal"
              },
              "current_section": {
                "type": "string",
                "description": "Section within `current_step`.",
                "example": "personal"
              },
              "step_log": {
                "type": "array",
                "description": "Steps visited so far.",
                "items": {
                  "type": "object",
                  "properties": {
                    "step": {
                      "type": "string",
                      "description": "Step visited."
                    },
                    "section": {
                      "type": "string",
                      "description": "Section visited within that step."
                    }
                  }
                }
              },
              "data": {
                "description": "Answers persisted so far, as a flat map. Use it to confirm which of the\nkeys you pre-filled were actually accepted.\n",
                "allOf": [
                  {
                    "$ref": "#/components/schemas/AnswerInput"
                  }
                ]
              },
              "hitl": {
                "type": "object",
                "description": "Human-in-the-loop review state, when the flow uses one."
              },
              "application": {
                "type": "object",
                "description": "> **`id` here is not the application ID** used elsewhere — read\n> `data.id`. And `other_status` keys are suffixed `_status` on this\n> endpoint only (`ekyc_status`), unlike every other response.\n",
                "properties": {
                  "id": {
                    "type": "integer",
                    "description": "**Not** the application id used elsewhere — read `data.id` instead. This\nvalue does not match `application.id` in the webhook.\n",
                    "example": 100003
                  },
                  "slug": {
                    "type": "string",
                    "description": "Application slug."
                  },
                  "status": {
                    "type": "string",
                    "description": "Overall application status.",
                    "enum": [
                      "incomplete",
                      "complete",
                      "expired"
                    ]
                  },
                  "other_status": {
                    "type": "object",
                    "description": "Per-check outcomes. **On this endpoint only**, every key is suffixed\n`_status` — `ekyc` appears as `ekyc_status` — unlike every other\nresponse. Keys other than `ekyc` are status columns configured in\nyour own workspace.\n",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "example": {
                      "ekyc_status": "pass"
                    }
                  }
                }
              }
            }
          },
          "schema": {
            "type": "object",
            "description": "Rendered form definition — steps, sections and items. Read\n`schema.steps.*.sections.*.items` to discover the real `question_key` values\nyou can pre-fill.\n"
          },
          "schema_config": {
            "type": "object",
            "description": "Branding, navigation and locale configuration.\n`schema_config.configs.locale.available_locales` is the authoritative list of\nlocales this form supports.\n"
          },
          "trans": {
            "type": "object",
            "description": "Flattened UI translations, keyed by schema path."
          },
          "pages": {
            "type": "object",
            "description": "Landing and thank-you page slugs.",
            "properties": {
              "landing": {
                "type": "string",
                "description": "Slug of the flow's landing page."
              },
              "thanks": {
                "type": "string",
                "description": "Slug of the page shown after submission."
              }
            }
          },
          "actions": {
            "type": "array",
            "description": "Pending UI actions for the form runtime. Empty for most applications.",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "AnswersRequest": {
        "type": "object",
        "title": "AnswersRequest",
        "description": "Body for submitting an application server-side. `answers` carries any values\nyou want written at submission time; keys must be fields on the form as named\nin the Builder, or declared input parameters — anything else is accepted and\nsilently discarded. Supplied values are validated for format, and `required`\nchecks run because this call submits.\n",
        "properties": {
          "answers": {
            "description": "Answers to write at submission, keyed by field name.",
            "allOf": [
              {
                "$ref": "#/components/schemas/AnswerInput"
              }
            ]
          }
        },
        "example": {
          "answers": {
            "transaction_id": "TXN-0001"
          }
        }
      },
      "EventMeta": {
        "type": "object",
        "title": "EventMeta",
        "description": "Metadata about one webhook **delivery** — which event fired, a per-delivery nonce,\nthe payload schema version, and when the delivery was generated.\n\n\nNote that nothing here describes the applicant; `application` and `answers` do that.\n",
        "properties": {
          "type": {
            "type": "string",
            "description": "Which event triggered the delivery.\n\n\n| `type` | Fires when | Setup |\n|---|---|---|\n| `submit_form` | The applicant submitted the form. The only type that carries `answers` and `extra`. | Enabled by default |\n| `update_status` | A status on the application changed — for example an operator changed it in the Portal. | Contact support |\n| `drop_off` | The applicant started and did not finish before the form expired. | Contact support |\n\n\nOnly `submit_form` carries the full payload. Every other type is a lifecycle\nnotification: it carries `event` and `application`, and you should treat\n`answers`, `extra` and `event.version` as absent. Further lifecycle types\nexist and can be enabled by support — branch on `type` and ignore values\nyou do not handle.\n\n\n> **`submit_form`, not `on_submit`.** The `on_*` names are the internal event\n> and Decision Flow trigger names; they do not appear as an `event.type`.\n",
            "example": "submit_form"
          },
          "nounce": {
            "type": "string",
            "description": "Unique **per delivery**.\n\n\n> **This cannot deduplicate a re-delivery.** Because the value is minted per\n> delivery, asking UpPass to resend — or an automatic retry after your\n> receiver answered `5xx` — arrives with a *different* nounce and passes any\n> nounce-only check.\n>\n> Key idempotency on `application.slug` + `application.submitted_at`, and use\n> `nounce` only to collapse an immediate transport-level retry.\n",
            "example": "4UJne3ZqgzQvCeYe9WYh8lUtfyhJxo"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When this **delivery** was generated — not when the applicant submitted. On a\nfirst delivery the two are seconds apart; on a re-delivery they can be far\napart.\n\n\nFor the submission time read `application.submitted_at`. `created_at` does\nmatter for one thing: it starts the 15-minute clock on every signed image URL\nin the payload.\n",
            "example": "2026-08-26T09:46:31.960030+00:00"
          },
          "version": {
            "type": "integer",
            "description": "Payload schema version. Currently `2`.\n\n\nPresent on `submit_form` deliveries. **Absent from lifecycle events**\n(`drop_off` and friends) — do not require it.\n",
            "example": 2
          }
        }
      },
      "WebhookSubmission": {
        "type": "object",
        "title": "WebhookSubmission",
        "description": "Full result payload, delivered on `submit_form`.\n",
        "required": [
          "event",
          "application"
        ],
        "properties": {
          "event": {
            "description": "Metadata about this delivery. Always present.",
            "allOf": [
              {
                "$ref": "#/components/schemas/EventMeta"
              }
            ]
          },
          "application": {
            "description": "Identifiers and outcomes for the application. Always present.",
            "allOf": [
              {
                "$ref": "#/components/schemas/Application"
              }
            ]
          },
          "answers": {
            "$ref": "#/components/schemas/AnswerMap"
          },
          "extra": {
            "$ref": "#/components/schemas/WebhookExtra"
          }
        }
      },
      "WebhookEvent": {
        "type": "object",
        "title": "WebhookEvent",
        "description": "Lightweight lifecycle event. Carries `event` and `application` only — no\n`answers`, no `extra`, and no `event.version`.\n",
        "required": [
          "event",
          "application"
        ],
        "properties": {
          "event": {
            "description": "Metadata about this delivery. Carries no `version` on lifecycle events.",
            "allOf": [
              {
                "$ref": "#/components/schemas/EventMeta"
              }
            ]
          },
          "application": {
            "description": "Identifiers and status for the application the event concerns.",
            "allOf": [
              {
                "$ref": "#/components/schemas/Application"
              }
            ]
          }
        }
      }
    },
    "examples": {
      "CreateFormResponseDraft": {
        "summary": "Application created, not yet submitted",
        "description": "`submitted_at` is `null` — the applicant has not submitted yet.",
        "value": {
          "info": "https://app.uppass.io/en/api/forms/YOUR_FORM_SLUG/applied-forms/YOUR_APPLICATION_SLUG/info/",
          "detail": {
            "form": "YOUR_FORM_SLUG",
            "step": "personal",
            "section": "personal",
            "submitted_at": null,
            "slug": "YOUR_APPLICATION_SLUG"
          },
          "form_url": "https://app.uppass.io/en/form/YOUR_FORM_SLUG/YOUR_APPLICATION_SLUG/"
        }
      },
      "WebhookSubmissionAmlHit": {
        "summary": "Submission with screening matches",
        "description": "A complete `submit_form` delivery for an applicant whose screening step returned\nmatches. `extra.identity_aml` carries the provider results; `other_status.ekyc`\nreports the document and liveness checks only, and is unaffected by them.\n\n\nProvider match data is trimmed at the `__TRIMMED__` markers — a real payload is\nfar larger.\n",
        "value": {
          "event": {
            "nounce": "4UJne3ZqgzQvCeYe9WYh8lUtfyhJxo",
            "type": "submit_form",
            "version": 2,
            "created_at": "2026-08-26T09:46:31.960030+00:00"
          },
          "application": {
            "form": "YOUR_FORM_SLUG",
            "id": 100001,
            "no": "COK00100001",
            "slug": "YOUR_APPLICATION_SLUG",
            "submitted_at": "2026-08-26T09:11:17.574375+00:00",
            "status": "complete",
            "other_status": {
              "ekyc": "pass"
            }
          },
          "answers": {
            "consent_version": {
              "value": "",
              "created_at": "2026-08-26T09:09:41.656046+00:00",
              "updated_at": "2026-08-26T09:09:41.656068+00:00"
            },
            "consent_date": {
              "value": "2026-08-26T09:09:39.158Z",
              "created_at": "2026-08-26T09:09:41.656159+00:00",
              "updated_at": "2026-08-26T09:09:41.656165+00:00"
            },
            "consent_checkbox_1": {
              "value": true,
              "created_at": "2026-08-26T09:09:41.656215+00:00",
              "updated_at": "2026-08-26T09:09:41.656220+00:00"
            },
            "ekyc_document_type": {
              "value": "passport",
              "created_at": "2026-08-26T09:09:50.028006+00:00",
              "updated_at": "2026-08-26T09:09:50.028031+00:00"
            },
            "document_number": {
              "value": "AB0000002",
              "created_at": "2026-08-26T09:09:58.371451+00:00",
              "updated_at": "2026-08-26T09:09:58.371470+00:00"
            },
            "name_prefix": {
              "value": "Mr.",
              "created_at": "2026-08-26T09:09:58.401239+00:00",
              "updated_at": "2026-08-26T09:09:58.401255+00:00"
            },
            "full_name_type": {
              "value": "parts",
              "created_at": "2026-08-26T09:09:58.469133+00:00",
              "updated_at": "2026-08-26T09:09:58.469150+00:00"
            },
            "full_name_first_name": {
              "value": "Anucha",
              "created_at": "2026-08-26T09:09:58.422820+00:00",
              "updated_at": "2026-08-26T09:09:58.422836+00:00"
            },
            "full_name_last_name": {
              "value": "Example",
              "created_at": "2026-08-26T09:09:58.446340+00:00",
              "updated_at": "2026-08-26T09:09:58.446361+00:00"
            },
            "full_name_en_first_name": {
              "value": "Somchai",
              "created_at": "2026-08-26T09:09:58.618052+00:00",
              "updated_at": "2026-08-26T09:09:58.618067+00:00"
            },
            "full_name_en_last_name": {
              "value": "Jaidee",
              "created_at": "2026-08-26T09:09:58.645995+00:00",
              "updated_at": "2026-08-26T09:09:58.646012+00:00"
            },
            "gender": {
              "value": "M",
              "created_at": "2026-08-26T09:09:58.597442+00:00",
              "updated_at": "2026-08-26T09:09:58.597459+00:00"
            },
            "date_of_birth": {
              "value": "1970-01-01",
              "created_at": "2026-08-26T09:09:58.488466+00:00",
              "updated_at": "2026-08-26T09:09:58.488481+00:00"
            },
            "date_of_issue": {
              "value": "2024-02-07",
              "created_at": "2026-08-26T09:09:58.508974+00:00",
              "updated_at": "2026-08-26T09:09:58.508990+00:00"
            },
            "date_of_expiry": {
              "value": "2034-02-06",
              "created_at": "2026-08-26T09:09:58.530236+00:00",
              "updated_at": "2026-08-26T09:09:58.530256+00:00"
            },
            "home_address_country": {
              "value": "IND",
              "created_at": "2026-08-26T09:09:58.571618+00:00",
              "updated_at": "2026-08-26T09:09:58.571633+00:00"
            },
            "home_address_full": {
              "value": "- - - - - - - - India",
              "created_at": "2026-08-26T09:11:02.780934+00:00",
              "updated_at": "2026-08-26T09:11:02.780962+00:00"
            }
          },
          "extra": {
            "ekyc": {
              "face_compare": {
                "status": "match",
                "score": 85.72
              },
              "liveness": {
                "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738493",
                "attempts": [
                  {
                    "attempt": 1,
                    "id": 100010,
                    "created_at": "2026-08-26T09:11:03.137966+00:00",
                    "status": "success",
                    "message": "",
                    "images": [
                      {
                        "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738493",
                        "action": "idle"
                      }
                    ]
                  }
                ]
              },
              "identity_document": {
                "type": "passport",
                "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738493",
                "face_image_url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738493",
                "attempts": [
                  {
                    "attempt": 1,
                    "id": 100011,
                    "created_at": "2026-08-26T09:09:58.660347+00:00",
                    "type": "passport",
                    "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738493",
                    "status": "success",
                    "message": ""
                  }
                ]
              }
            },
            "identity_aml": {
              "comply_advantage": [
                {
                  "input": {
                    "last_name": "Example",
                    "first_name": "Anucha",
                    "date_of_birth": "1970-01-01"
                  },
                  "output": {
                    "comply_advantage": [
                      {
                        "input": {
                          "last_name": "Example",
                          "first_name": "Anucha",
                          "date_of_birth": "1970-01-01"
                        },
                        "output": {
                          "code": 200,
                          "status": "success",
                          "content": {
                            "data": {
                              "id": 1000001,
                              "ref": "1000001-example",
                              "hits": [
                                {
                                  "doc": {
                                    "id": "EXAMPLEDOCID000",
                                    "name": "Anucha Example",
                                    "entity_type": "person",
                                    "types": [
                                      "sanction"
                                    ],
                                    "aka": [
                                      {
                                        "name": "A. Example"
                                      },
                                      {
                                        "name": "Anucha Exampl"
                                      },
                                      {
                                        "name": "__TRIMMED__ a real record can carry several hundred aliases"
                                      }
                                    ],
                                    "fields": [
                                      {
                                        "name": "Nationality",
                                        "value": "Exampleland",
                                        "source": "un-consolidated"
                                      },
                                      {
                                        "tag": "date_of_birth",
                                        "name": "Date of Birth",
                                        "value": "1970-01-01",
                                        "source": "un-consolidated"
                                      },
                                      {
                                        "tag": "passport",
                                        "name": "Passport",
                                        "value": "Passport: X0000000 Exampleland",
                                        "source": "un-consolidated"
                                      },
                                      {
                                        "name": "Program",
                                        "value": "Example sanctions programme",
                                        "source": "un-consolidated"
                                      },
                                      {
                                        "name": "Un Listing Id",
                                        "value": "EXi.000",
                                        "source": "un-consolidated"
                                      },
                                      {
                                        "name": "__TRIMMED__",
                                        "value": "a real record carries ~120 field entries across a dozen sources"
                                      }
                                    ],
                                    "sources": [
                                      "un-consolidated",
                                      "hm-treasury-list",
                                      "dfat-australia-list",
                                      "swiss-seco-list",
                                      "__TRIMMED__ source list truncated"
                                    ],
                                    "source_notes": {
                                      "un-consolidated": {
                                        "url": "https://scsanctions.un.org/consolidated/",
                                        "name": "United Nations Consolidated",
                                        "aml_types": [
                                          "sanction"
                                        ],
                                        "country_codes": [
                                          "XX"
                                        ],
                                        "listing_started_utc": "2001-01-01T00:00:00Z"
                                      }
                                    },
                                    "associates": [],
                                    "keywords": [],
                                    "media": [],
                                    "created_utc": "2020-01-01T00:00:00Z",
                                    "last_updated_utc": "2026-01-01T00:00:00Z"
                                  },
                                  "score": 1.9,
                                  "match_types": [
                                    "aka_exact",
                                    "year_of_birth"
                                  ],
                                  "match_types_details": [
                                    {
                                      "sources": [
                                        "United Nations Consolidated"
                                      ],
                                      "aml_types": [
                                        "sanction"
                                      ],
                                      "matching_name": "Anucha Example",
                                      "name_matches": [
                                        {
                                          "query_term": "anucha",
                                          "match_types": [
                                            "exact_match"
                                          ]
                                        },
                                        {
                                          "query_term": "example",
                                          "match_types": [
                                            "exact_match"
                                          ]
                                        }
                                      ],
                                      "secondary_matches": [
                                        {
                                          "query_term": "1970",
                                          "match_types": [
                                            "exact_birth_year_match"
                                          ]
                                        }
                                      ]
                                    }
                                  ]
                                }
                              ],
                              "tags": [],
                              "limit": 500,
                              "labels": [],
                              "offset": 0,
                              "filters": {
                                "types": [],
                                "fuzziness": 0,
                                "birth_year": 1970,
                                "exact_match": true,
                                "country_codes": [],
                                "remove_deceased": 0
                              },
                              "client_ref": null,
                              "created_at": "2026-08-26 09:11:20",
                              "risk_level": "unknown",
                              "total_hits": 9,
                              "updated_at": "2026-08-26 09:11:20",
                              "assignee_id": 100000,
                              "search_term": "Anucha Example",
                              "searcher_id": 100000,
                              "match_status": "potential_match",
                              "total_matches": 9,
                              "blacklist_hits": [],
                              "submitted_term": "Anucha Example",
                              "total_blacklist_hits": 0
                            }
                          }
                        },
                        "result": {
                          "is_found": true,
                          "is_in_pep": true,
                          "total_hits": 9,
                          "is_in_warnings": false,
                          "is_in_sanctions": true,
                          "is_in_adverse_media": false,
                          "is_in_fitness_probity": false
                        }
                      }
                    ]
                  },
                  "result": {
                    "is_all_found": true,
                    "is_any_found": true,
                    "is_all_in_pep": true,
                    "is_any_in_pep": true,
                    "is_all_in_warnings": false,
                    "is_any_in_warnings": false,
                    "is_all_in_sanctions": true,
                    "is_any_in_sanctions": true,
                    "is_all_in_adverse_media": false,
                    "is_any_in_adverse_media": false,
                    "is_all_in_fitness_probity": false,
                    "is_any_in_fitness_probity": false
                  },
                  "timestamp": "2026-08-26T09:11:20.644965+00:00",
                  "values_list": [
                    "is_any_in_adverse_media",
                    "is_any_in_warnings",
                    "is_any_in_fitness_probity",
                    "is_any_in_sanctions",
                    "is_any_in_pep"
                  ]
                }
              ]
            }
          }
        }
      },
      "WebhookSubmissionClean": {
        "summary": "Clean pass",
        "description": "A complete `submit_form` delivery with no screening matches. Note that this is a\npassport submission, so it carries **no `nid`** and none of the Thai address\nparts — fields that do not apply to a document type are absent, not null.\n",
        "value": {
          "event": {
            "nounce": "SjG5y60YsKnnTHBxiGWVrI8FDAcXC3",
            "type": "submit_form",
            "version": 2,
            "created_at": "2026-08-26T09:51:56.458646+00:00"
          },
          "application": {
            "form": "YOUR_FORM_SLUG",
            "id": 100002,
            "no": "COK00100002",
            "slug": "YOUR_APPLICATION_SLUG",
            "submitted_at": "2026-08-26T09:51:48.165780+00:00",
            "status": "complete",
            "other_status": {
              "ekyc": "pass"
            }
          },
          "answers": {
            "consent_version": {
              "value": "",
              "created_at": "2026-08-26T09:51:09.072084+00:00",
              "updated_at": "2026-08-26T09:51:09.072096+00:00"
            },
            "consent_date": {
              "value": "2026-08-26T09:51:06.156Z",
              "created_at": "2026-08-26T09:51:09.072168+00:00",
              "updated_at": "2026-08-26T09:51:09.072173+00:00"
            },
            "consent_checkbox_1": {
              "value": true,
              "created_at": "2026-08-26T09:51:09.072227+00:00",
              "updated_at": "2026-08-26T09:51:09.072233+00:00"
            },
            "ekyc_document_type": {
              "value": "passport",
              "created_at": "2026-08-26T09:51:14.907185+00:00",
              "updated_at": "2026-08-26T09:51:14.907210+00:00"
            },
            "document_number": {
              "value": "AB0000001",
              "created_at": "2026-08-26T09:51:21.629758+00:00",
              "updated_at": "2026-08-26T09:51:21.629782+00:00"
            },
            "name_prefix": {
              "value": "Mr.",
              "created_at": "2026-08-26T09:51:21.663374+00:00",
              "updated_at": "2026-08-26T09:51:21.663393+00:00"
            },
            "full_name_type": {
              "value": "parts",
              "created_at": "2026-08-26T09:51:21.738390+00:00",
              "updated_at": "2026-08-26T09:51:21.738409+00:00"
            },
            "full_name_first_name": {
              "value": "Somchai",
              "created_at": "2026-08-26T09:51:21.684738+00:00",
              "updated_at": "2026-08-26T09:51:21.684753+00:00"
            },
            "full_name_last_name": {
              "value": "Jaidee",
              "created_at": "2026-08-26T09:51:21.709491+00:00",
              "updated_at": "2026-08-26T09:51:21.709510+00:00"
            },
            "full_name_en_first_name": {
              "value": "Somchai",
              "created_at": "2026-08-26T09:51:21.891338+00:00",
              "updated_at": "2026-08-26T09:51:21.891357+00:00"
            },
            "full_name_en_last_name": {
              "value": "Jaidee",
              "created_at": "2026-08-26T09:51:21.912462+00:00",
              "updated_at": "2026-08-26T09:51:21.912479+00:00"
            },
            "gender": {
              "value": "M",
              "created_at": "2026-08-26T09:51:21.870009+00:00",
              "updated_at": "2026-08-26T09:51:21.870026+00:00"
            },
            "date_of_birth": {
              "value": "1996-08-16",
              "created_at": "2026-08-26T09:51:21.761786+00:00",
              "updated_at": "2026-08-26T09:51:21.761805+00:00"
            },
            "date_of_issue": {
              "value": "2024-02-07",
              "created_at": "2026-08-26T09:51:21.783307+00:00",
              "updated_at": "2026-08-26T09:51:21.783321+00:00"
            },
            "date_of_expiry": {
              "value": "2034-02-06",
              "created_at": "2026-08-26T09:51:21.802611+00:00",
              "updated_at": "2026-08-26T09:51:21.802629+00:00"
            },
            "home_address_country": {
              "value": "THA",
              "created_at": "2026-08-26T09:51:21.847874+00:00",
              "updated_at": "2026-08-26T09:51:21.847893+00:00"
            }
          },
          "extra": {
            "ekyc": {
              "face_compare": {
                "status": "match",
                "score": 88.09
              },
              "liveness": {
                "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738816",
                "attempts": [
                  {
                    "attempt": 1,
                    "id": 100012,
                    "created_at": "2026-08-26T09:51:32.007437+00:00",
                    "status": "success",
                    "message": "",
                    "images": [
                      {
                        "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738816",
                        "action": "idle"
                      }
                    ]
                  }
                ]
              },
              "identity_document": {
                "type": "passport",
                "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738816",
                "face_image_url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738816",
                "attempts": [
                  {
                    "attempt": 1,
                    "id": 100013,
                    "created_at": "2026-08-26T09:51:21.926040+00:00",
                    "type": "passport",
                    "url": "https://app.uppass.io/api/ekyc/YOUR_APPLICATION_SLUG/result/image/?q=SIGNED_TOKEN_REDACTED&exp=1787738816",
                    "status": "success",
                    "message": ""
                  }
                ]
              }
            },
            "identity_aml": {
              "comply_advantage": [
                {
                  "input": {
                    "last_name": "Jaidee",
                    "first_name": "Somchai",
                    "date_of_birth": "1996-08-16"
                  },
                  "output": {
                    "comply_advantage": [
                      {
                        "input": {
                          "last_name": "Jaidee",
                          "first_name": "Somchai",
                          "date_of_birth": "1996-08-16"
                        },
                        "output": {
                          "code": 200,
                          "status": "success",
                          "content": {
                            "data": {
                              "id": 1000002,
                              "ref": "1000002-example",
                              "hits": [],
                              "tags": [],
                              "limit": 500,
                              "labels": [],
                              "offset": 0,
                              "filters": {
                                "types": [],
                                "fuzziness": 0,
                                "birth_year": 1996,
                                "exact_match": true,
                                "country_codes": [],
                                "remove_deceased": 0
                              },
                              "client_ref": null,
                              "created_at": "2026-08-26 09:51:51",
                              "risk_level": "unknown",
                              "total_hits": 0,
                              "updated_at": "2026-08-26 09:51:51",
                              "assignee_id": 100000,
                              "search_term": "Somchai Jaidee",
                              "searcher_id": 100000,
                              "match_status": "no_match",
                              "total_matches": 0,
                              "blacklist_hits": [],
                              "submitted_term": "Somchai Jaidee",
                              "total_blacklist_hits": 0
                            }
                          }
                        },
                        "result": {
                          "is_found": false,
                          "is_in_pep": false,
                          "total_hits": 0,
                          "is_in_warnings": false,
                          "is_in_sanctions": false,
                          "is_in_adverse_media": false,
                          "is_in_fitness_probity": false
                        }
                      }
                    ]
                  },
                  "result": {
                    "is_all_found": false,
                    "is_any_found": false,
                    "is_all_in_pep": false,
                    "is_any_in_pep": false,
                    "is_all_in_warnings": false,
                    "is_any_in_warnings": false,
                    "is_all_in_sanctions": false,
                    "is_any_in_sanctions": false,
                    "is_all_in_adverse_media": false,
                    "is_any_in_adverse_media": false,
                    "is_all_in_fitness_probity": false,
                    "is_any_in_fitness_probity": false
                  },
                  "timestamp": "2026-08-26T09:51:51.771894+00:00",
                  "values_list": [
                    "is_any_in_adverse_media",
                    "is_any_in_warnings",
                    "is_any_in_fitness_probity",
                    "is_any_in_sanctions",
                    "is_any_in_pep"
                  ]
                }
              ]
            }
          }
        }
      },
      "WebhookEventDropOff": {
        "summary": "Form expired without submission",
        "description": "Lifecycle events carry `event` and `application` only — no `answers`, no `extra`, and no `event.version`.\n",
        "value": {
          "event": {
            "type": "drop_off",
            "nounce": "8mQr2VtXbN4pLz7KcW9dYs3JhF1gAe",
            "created_at": "2026-08-26T09:40:00.000000+00:00"
          },
          "application": {
            "id": 100001,
            "no": "COK00100001",
            "form": "YOUR_FORM_SLUG",
            "slug": "YOUR_APPLICATION_SLUG",
            "status": "expired",
            "other_status": {
              "ekyc": "pending"
            },
            "submitted_at": null
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Malformed request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "status_code": 400,
                "message": "Bad Request",
                "detail": "Malformed request"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`Authorization` header present but the token is invalid or revoked.\n\n\nDo not retry — fix the token.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "status_code": 401,
                "message": "Unauthorized",
                "detail": "No permission -- see authorization schemes"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "No `Authorization` header at all, or the method is not permitted on this path.\n\n\nNote this is `403`, not `401` — the API reserves `401` for a token that is\npresent but bad. `GET` on a `POST`-only endpoint also lands here rather than\nreturning `405`.\n\n\nDo not retry.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "status_code": 403,
                "message": "Forbidden",
                "detail": "Request forbidden -- authorization will not help"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No match for the given `form_slug`, or `slug`. Do not retry — check both.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "status_code": 404,
                "message": "Not Found",
                "detail": "Nothing matches the given URI"
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "One or more answers failed validation.\n\n\nThe rejected fields are under `error.detail`, keyed by `question_key`. Do not\nretry — fix the payload.\n\n\nReturned whenever a supplied value fails a format, regex or checksum rule.\nMissing required fields are reported at submission, not here.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "failedChecksum": {
                "summary": "Format rule on a draft",
                "description": "Format validation runs at creation. `1234567890123` is not a\nchecksum-valid Thai national ID.\n",
                "value": {
                  "error": {
                    "status_code": 422,
                    "message": "Unprocessable Entity",
                    "detail": {
                      "nid": [
                        "The ID Card Number is invalid (checksum)"
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Request was throttled. Retry with exponential backoff.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "status_code": 429,
                "message": "Too Many Requests",
                "detail": "Request was throttled"
              }
            }
          }
        }
      },
      "InsufficientCredit": {
        "description": "Workspace is out of credit.\n\n\nBefore creating an application UpPass checks that the workspace holds enough\ncredit to complete the whole flow. **Alert on this specifically** — every create\nfails with it until the workspace is topped up. Do not retry.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "detail": {
                  "type": "string"
                },
                "code": {
                  "type": "string"
                }
              }
            },
            "example": {
              "detail": "Insufficient Credit",
              "code": "InsufficientCredit"
            }
          }
        }
      },
      "ServerError": {
        "description": "A server error occurred. Retry up to three times with backoff, then alert.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "status_code": 500,
                "message": "Internal Server Error",
                "detail": "A server error occurred"
              }
            }
          }
        }
      },
      "ValidationErrorFlat": {
        "description": "One or more answers failed validation.\n\n\n> **This endpoint returns a flat map**, keyed by `question_key`, with no `error`\n> wrapper — unlike [Create application](#tag/applications/createAppliedForm), which\n> wraps the same content in `error.detail`. Handle both shapes.\n\n\nDo not retry — fix the payload.\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationErrors"
            },
            "example": {
              "ekyc_document": [
                "The ekyc_document field is required."
              ],
              "ekyc_liveness": [
                "The field is required."
              ]
            }
          }
        }
      },
      "NotFoundBare": {
        "description": "No application matches `form_slug` + `slug`.\n\n\n> **This endpoint uses a different error shape** — a bare `detail` string with no\n> `error` wrapper and no `status_code`. Your handler must tolerate both.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "detail": {
                  "type": "string"
                }
              }
            },
            "example": {
              "detail": "No AppliedForm matches the given query."
            }
          }
        }
      }
    }
  },
  "x-tagGroups": [
    {
      "name": "API reference",
      "tags": [
        "Applications"
      ]
    },
    {
      "name": "Webhooks",
      "tags": [
        "Webhooks"
      ]
    }
  ]
}