import { Code } from '@astrojs/starlight/components';
import { apiOrigin } from '../../../lib/docs-env';

`POST /v1/sessions` accepts a `metadata` object. It is your own key/value data. The platform stores it with the verification and gives your keys back unchanged on a read. A read can also carry keys the platform added later: see [Reading the values back](#reading-the-values-back).

The platform understands five key names inside it. Together they state when the transaction starts and ends, its amount and currency, and where it happens. A [document the applicant signs](/guides/workflows/document-signing) can print each one into its text. Every other key is yours, and the platform never interprets it.

```json
{
  "applicant_id": "app_01hqz8w7j1l2m3n4p5q6r7s8t9",
  "workflow_template_id": "wt_01hqz9x8k2m3n4p5q6r7s8t9u0",
  "metadata": {
    "start_datetime": "2026-03-14T15:00:00-05:00",
    "end_datetime": "2026-03-17T11:00:00-05:00",
    "amount": 125000,
    "currency": "usd",
    "location": "12 Main St, Austin, TX 78701",
    "your_booking_ref": "BK-4471"
  }
}
```

## The five keys

Each one is optional. If you send one in the wrong shape, the platform refuses the whole request with `422 validation_error`.

| Key              | Format                                                                                                      | Refused example         |
| ---------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------- |
| `start_datetime` | ISO 8601 with an explicit offset: `2026-03-14T15:00:00-07:00` or `2026-03-14T22:00:00Z`.                    | `"2026-03-14"`          |
| `end_datetime`   | The same format. It must fall at or after `start_datetime`.                                                 | `"2026-03-14T15:00:00"` |
| `amount`         | A whole number in the minor units of `currency`, from 0 to 999999999999. It requires `currency`.            | `"1250.00"`             |
| `currency`       | A three-letter ISO 4217 code. The platform keeps the case you send.                                         | `"US Dollars"`          |
| `location`       | One display line of 500 characters or less. No control characters, no bidirectional marks, no angle brackets. | `"<b>Suite 4</b>"`      |

### `amount` is in minor units, and no check can find a mistake here

Send the amount the way the currency counts it, with no decimal point:

- `125000` with `"currency": "usd"` prints **$1,250.00**.
- `1250` with `"currency": "usd"` prints **$12.50**. That is a valid request, and $12.50 is what the document shows.
- `50000` with `"currency": "jpy"` prints **¥50,000**. A currency with no decimal places takes the same whole number and applies no scaling, so do not multiply by 100 for it.

The wire carries no unit and the platform applies no plausible-range check. A major-unit value is therefore accepted, and it prints 100 times too small on a document that a person signs. Convert once, near the place where you read the price, and test the number that reaches the document.

You can send `currency` on its own. Always send `currency` with `amount`: the platform refuses an amount alone, because it cannot print a number with no currency.

### The offset you send is the time zone the document prints

Both datetimes need an explicit offset. A bare local datetime (`2026-03-14T15:00:00`) and a date alone (`2026-03-14`) are both refused. The value prints into a sealed document that is never rendered again, and an unlabeled instant reads as the wrong calendar date on either side of midnight.

Send the offset that applies where the transaction happens, not the offset where your systems run. `2026-03-14T15:00:00-07:00` prints as `2026-03-14 15:00 (UTC-07:00)`.

A value at exactly midnight in the offset you send prints as the date alone, with no clock. This is deliberate. A source system that records only a calendar date has no hour to state, and a printed `00:00` would read as a real start time. The offset decides this, not the instant: `2026-03-17T00:00:00Z` prints a bare date, and `2026-03-16T19:00:00-05:00` is the same moment but prints a clock.

The platform compares the two values as instants, so a pair written in different offsets still compares correctly.

## What a signed document prints

A document can place any of five variables, which the document editor offers by name. Each one reads a single key from this object:

| Variable in the document | Key you send     |
| ------------------------ | ---------------- |
| Start date and time      | `start_datetime` |
| End date and time        | `end_datetime`   |
| Transaction amount       | `amount`         |
| Currency                 | `currency`       |
| Location                 | `location`       |

**A document that places one of these makes the matching key required.** If the workflow template you create against runs a document signing step whose document places `Transaction amount`, a create with no `metadata.amount` is refused:

```json
{
  "error": {
    "code": "esign_requirements_not_met",
    "message": "The document uses \"Transaction amount\", which comes from the session metadata. Add \"amount\" to the metadata when you create the session, or remove those fields from the document.",
    "details": { "reason": "esign_session_metadata_required" }
  }
}
```

Every error also carries `type`, `request_id`, and `doc_url`. Those are the same on every response, and the [Error reference](/api/errors) shows them in full, so the examples on this page show only the fields that differ.

The status is `403` and `details.reason` is `esign_session_metadata_required`. The platform creates nothing. The `message` names every missing variable and every key to send, so you can show it to whoever operates your integration.

An empty string and `null` do not reach this refusal. Each of the five has a shape, so `""` and `null` fail that shape and come back as `422 validation_error` well before the document is consulted. Handle the 422 for a field you send empty, and the 403 above only for a field you omit.

This refusal cannot come any earlier. The value comes from the individual create call, and no saved workflow template knows it, so a workflow that uses these variables saves normally and only the create is refused. Every other `esign_requirements_not_met` reason describes the workflow or the document instead, and a template save is refused for it. See [`esign_requirements_not_met`](/api/errors/esign_requirements_not_met).

## Reserved key names

The platform writes to the same object, so it refuses seven key names:

`referenceDetails`, `notes`, `operatorNotes`, `cancellationReason`, `manualReviewReason`, `erroredReason`, `__requested_checks`.

A request that sends one gets `422 validation_error` naming the key. Rename it: `notes` becomes `your_notes`.

**The platform accepted these key names before this release.** A key that your integration sends today can be refused after you upgrade, so search your integration for all seven first.

Six of the seven were stored, and platform surfaces then read them. That is the reason for the change. A value you wrote under `operatorNotes` appeared to a reviewer as a note from your own team. A value you wrote under `erroredReason` reached the error boundary that decides how a stalled verification is shown. The seventh, `__requested_checks`, was accepted and then discarded, so nothing you sent under it was ever readable.

## Reading the validation error

Three rules apply to the whole object, not to one field: the reserved-key refusal, `amount` needing `currency`, and `end_datetime` not preceding `start_datetime`. There are two consequences.

- **These three rules do not appear in the OpenAPI schema.** A generated schema can describe the shape of one field, but not a rule that spans two of them. The three rules are therefore published as prose, here and in the `metadata` description in the [interactive reference](/reference).
- **`error.param` is `metadata` for all three.** The platform derives `param` from the first segment of the issue path, so `param` alone cannot tell the three rules apart. Read `details.issues[].path`, which carries the full path, and show the issue `message`, which names the key at fault.

```json
{
  "error": {
    "code": "validation_error",
    "message": "Invalid request body",
    "param": "metadata",
    "details": {
      "issues": [
        {
          "code": "custom",
          "path": ["metadata", "currency"],
          "message": "Provide \"currency\" alongside \"amount\" so the amount can be shown with its currency."
        }
      ]
    }
  }
}
```

## Reading the values back

`GET /v1/sessions/{id}` returns your own keys unchanged: the same snake_case names, the same capitalization on `currency`, and the same offsets on the datetimes. The platform rewrites one value and only one. It removes leading and trailing spaces from `location`, and the trimmed value is what you read back.

**The object you read is not always the object you sent.** The platform writes into the same object as the verification progresses, and those keys are returned to you along with your own. A verification that an operator canceled carries `cancellationReason`; one an operator sent to review carries `manualReviewReason`; one that stalled carries `erroredReason`; and a note added through the API or the console carries `operatorNotes`. None of them is present at create, and none is removed on read.

**So do not post a metadata object you read back on a create.** Before this release you could read a verification's `metadata`, merge your changes into it, and post the result as the `metadata` of the next verification. That now fails with `422 validation_error`, because the platform refuses every key in the list above on the way in. Build the object you post from your own data, or strip the reserved names before you send it.

<Code
  code={`curl -s ${apiOrigin('us')}/v1/sessions/vs_01H... \\\n  -H "Authorization: Bearer ah_sk_us_live_<your-key>" \\\n  | jq .data.metadata`}
  lang="bash"
/>

## Verifications that an integration starts

A verification that a [connected system](/guides/integrations) starts can fill part of this object automatically. Which fields arrive depends on the system, and some supply none of them.

**No integration supplies `amount`, `currency`, or `location`.** A document that places `Transaction amount`, `Currency`, or `Location` therefore refuses every verification an integration tries to start. The response is the same `esign_session_metadata_required`, and no applicant is invited.

- The [Hostaway integration](/guides/integrations/hostaway) supplies `start_datetime` and `end_datetime`, and only when the reservation webhook carries the time zone of the listing. The reservation gives a check-in and a check-out **date** with no time on it. Both values therefore arrive at local midnight, and the document prints a bare date such as `2026-08-01 (UTC-04:00)`. Do not write a clause around a check-in hour, because the document has no hour to print.
- The [Guesty integration](/guides/integrations/guesty) supplies **none of the five**. Its reservation carries no time zone, and the platform needs one before it can label a date. It therefore sends nothing, rather than a value that could print the wrong calendar day. A document that places any of the five variables refuses every verification that this integration starts.

To put these fields on a verification that an integration would start, create the verification from the API instead and send them yourself.

## Screening rules read a different object

A [screening rule](/developers/rules-field-reference) can read `session.metadata.<key>`. That resolves the metadata on the **workflow template**, not the object you send on a create. These five keys reach document variables only. A rule condition cannot address them.