> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qa.esectra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Return flow

> Bring the person back to your platform with a one-time result code, and exchange it for the result.

A published flow may carry a **return URL**. When a hosted session under that flow finishes, the completion screen says only that the submission is done, shows a *Return* button as a fallback, and then sends the browser to your URL. The redirect carries four query parameters, all set by Esectra:

```text theme={null}
https://client.example/verification/complete
  ?code=<one-time result code>
  &session_id=<verification session id>
  &kyc_id=<your subject_id for this session>
  &state=<the state you opened the session with>
```

For example:

```text theme={null}
https://client.example/verification/complete?code=3f9c…e1a0&session_id=vs_123&kyc_id=kyc_t5l7ogyk9m8v09mj&state=checkout-42
```

| Parameter | What it is |
| - | - |
| `code` | A one-time result code: 256 random bits, valid for ten minutes, exchangeable once by your API key. |
| `session_id` | The `verification_session_id` you were given when you opened the session. |
| `kyc_id` | The `subject_id` you gave when you opened the session - your own reference for the person - taken from the stored session and from nowhere else. Percent-encoded; decode it before comparing. |
| `state` | The value you passed as `state` when opening the session, unchanged. Absent if you passed none. |

Nothing else is in the URL. No verdict, no risk level, no reasons, no document data, no API key, and never the capture token - the code is minted separately, after the session has concluded, and the two have nothing in common.

> [!WARNING]
>
> `kyc_id` and `state` are **correlation data, not a result**. Anyone can type a URL into a browser. Treat the redirect as "a session for this reference may have finished": exchange the code server-to-server, and decide from the exchange answer alone.

## What to put in `subject_id`

Because `kyc_id` travels in a URL, it will appear in browser history, in your web server's access log and in any proxy or CDN log between the person and you. Make `subject_id` an **opaque reference** - an id from your own system that means nothing outside it, such as `kyc_t5l7ogyk9m8v09mj` - and never a name, an email address, a phone number, a national id number or anything else that identifies the person on its own.

Two rules apply, at two moments:

* **At open**, the value is bounded the way `state` is: non-empty, at most 200 characters, no control characters. Anything else is refused with `422 SUBJECT_TOO_LONG` or `422 SUBJECT_INVALID`, on the hosted and the server-to-server path alike. Nothing narrower is refused here - a name with a space or an email address still opens a session - so references your integration already uses keep working.
* **At the redirect**, `kyc_id` is sent only when the stored value is an opaque URL token: ASCII letters, digits, `-`, `_`, `.` and `~`, nothing else, and not a run of eight or more digits alone. A value outside that - `jane.doe@example.com`, `Jane Doe`, `+2348012345678`, `Doe,Jane`, `acct/42`, a non-ASCII letter, `12345678901` - opens and completes normally but is **not** put in the URL; the redirect then carries `code`, `session_id` and `state` only, the exchange still names the subject, and the `RESULT_CODE_ISSUED` audit event carries `kyc_id_omitted` with `NOT_OPAQUE`, `DIGITS_ONLY`, or `TOO_LONG` / `EMPTY` / `CONTROL_CHARACTER` for a session stored before the open-time rule.

> [!WARNING]
>
> **This guard is a shape check, not a judgement.** It stops the common shapes of personal data - an email address, a name with a space or punctuation, a phone number, a long run of digits such as a national identity or bank verification number - but it cannot recognise a surname written alone (`Okonkwo`), a date of birth with separators (`1990-05-14`), or a short number that happens to identify someone. Such a value fits and **will be sent**. If any of your existing sessions were opened with one as `subject_id`, the first completion after this version is deployed puts it in the redirect URL, in browser history and in your access logs. Review what you send as `subject_id` before publishing a flow with a return URL, or leave the return URL empty until you have.

## Configure the return URL

The URL belongs to the flow, set by an organisation admin, and is frozen into each session when it opens. Nothing in the capture link, the query string or the person's browser can change where they are sent. It must be `https` with a host and no credentials or `#fragment`; a deployment running a development profile also accepts `http://localhost` and `http://127.0.0.1`.

Query parameters of your own are kept, in the order you wrote them, and Esectra's four follow. Your URL may **not** name `code`, `session_id`, `kyc_id` or `state` itself, in any letter case or percent-encoded spelling: saving or publishing such a flow is refused with `422 INVALID_FLOW` and a message naming the parameter, so the redirect can never carry two values for one name or a value you set where your server expects Esectra's.

```json theme={null}
{ "code": "INVALID_FLOW", "message": "return_url: a return URL must not carry a \"kyc_id\" query parameter: Esectra sets code, session_id, kyc_id and state itself when it sends the person back" }
```

## Exchange the code

Your server receives the redirect and exchanges the code under its API key. The answer names the session's `subject_id`; check it against the `kyc_id` you were redirected with before you act on the result, and treat a mismatch as a request to ignore:

```bash theme={null}
# From the redirect your server received:
CODE='3f9c…e1a0'
KYC_ID='kyc_t5l7ogyk9m8v09mj'   # URL-decoded

RESULT="$(curl -sS -X POST "$ESECTRA_BASE_URL/v1/verifications/result-codes/exchange" \
  -H "Authorization: Bearer $ESECTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"code":"'"$CODE"'"}')"

SUBJECT_ID="$(printf '%s' "$RESULT" | jq -r '.subject_id')"
if [ "$SUBJECT_ID" != "$KYC_ID" ]; then
  echo "redirect kyc_id ($KYC_ID) does not match the exchanged subject_id ($SUBJECT_ID); ignoring" >&2
  exit 1
fi
printf '%s' "$RESULT" | jq '{status, decision, risk_level}'
```

The answer is the session's result:

```json theme={null}
{
  "verification_session_id": "vs_123",
  "subject_id": "kyc_t5l7ogyk9m8v09mj",
  "status": "PASSED",
  "intent": "FIRST_TIME",
  "flow": { "flow_id": "vf_...", "revision": 1, "flow_type": "FULL_IDENTITY" },
  "decision_id": "verify-vs_123",
  "decision": "ALLOW",
  "risk_level": "LOW",
  "reasons": [{ "code": "ALL_REQUIRED_CHECKS_PASSED", "message": "Every check the policy required was satisfied." }],
  "checks": [{ "check": "FACE_MATCH", "outcome": "PASS", "reason_code": "FACE_MATCH", "detail": "The live capture matches the reference face." }],
  "review_case_id": null,
  "created_at": "...",
  "completed_at": "..."
}
```

Reasons and check details are worded from fixed vocabularies keyed on stable codes - the same wording Control shows. No score, model name or threshold is ever in them; a stored code the API does not describe is reported as `UNSPECIFIED`.

The exchange is atomic and single-use: the code is bound to your tenant, spent by the request that wins, and refused afterwards. Refusals are `404 RESULT_CODE_INVALID` (no such code for your tenant - another tenant's code looks exactly like this), `409 RESULT_CODE_CONSUMED` (already exchanged) and `410 RESULT_CODE_EXPIRED`. If a store fails after the code has been spent, the answer is `503 RESULT_UNAVAILABLE_AFTER_EXCHANGE` and its message names the session id to read instead; the code is not given back.

## Read the result without the code

The code is a pointer, not the only way in. Read the same result at any time by session id with your API key, so a lost exchange response or a missed webhook costs nothing - and nothing here depends on a webhook having arrived:

```bash theme={null}
curl "$ESECTRA_BASE_URL/v1/verifications/sessions/$SESSION_ID" \
  -H "Authorization: Bearer $ESECTRA_API_KEY"
```

A session that has not finished reads as `status: "PENDING"` with no decision. A completion that fails on the server - the session could not be marked finished - issues no code and sends the person nowhere; the page stays on its own screen.

## What is recorded

Issuing, exchanging and refusing a code are audit events (`RESULT_CODE_ISSUED`, `RESULT_CODE_EXCHANGED`, `RESULT_CODE_REFUSED`) that name the session and the outcome; `RESULT_CODE_ISSUED` carries `kyc_id_omitted` when the redirect went out without a `kyc_id`. A completion that could not be given a redirect at all - the frozen return URL no longer passes this deployment's rules, or the code could not be stored - is `RETURN_REDIRECT_NOT_ISSUED` with the cause; the person still sees the finished screen, and the result is readable by session id. The code itself is stored only as a hash and appears in no log or audit event.

A return URL published before `code`, `session_id`, `kyc_id` and `state` were reserved keeps working: the stale parameter is dropped and the server's value sent, each name exactly once. It cannot be re-saved until the parameter is removed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.