# Session states and callbacks

Every in-person session state, the ipp.* callback that reports it, the payload fields each event carries, and the card tap outcomes Playground can simulate.

A session reports each move to the webhook endpoint configured for your account. How to
handle them is in [Take a payment](/in-person/take-a-payment/#2-handle-the-callbacks).

## States

| State | Callback | Final | What happened |
|---|---|---|---|
| `CREATED` | `ipp.session.created` | No | The session was dispatched and the device notified |
| `ACTIVE` | `ipp.session.activated` | No | The clerk chose a payment method and the device is waiting for the card |
| `FINALIZED` | `ipp.session.finalized` | Yes | The payment was collected and an order exists in Order Management |
| `CANCELLED` | `ipp.session.cancelled` | Yes | Cancelled by the clerk or by your backend |
| `EXPIRED` | — | Yes | The session's TTL elapsed with no payment |
| — | `ipp.payment.failed` | No | A payment attempt was declined. The clerk may retry |

Three states are final. Everything else can still move.

## Payloads

Every event carries `id`, `merchant_id`, `timestamp` and `type`, with the event's fields in
`data`. Every `data` carries the `session_id`, and two events add one field each.

`ipp.session.finalized` adds the `order_id` of the order in Order Management:

```json
{
  "id": "kevt_62EvxyMEaAaHSGeoDTza47",
  "merchant_id": "DM11917000",
  "timestamp": "2026-06-05T18:33:39.608920367Z",
  "type": "ipp.session.finalized",
  "data": {
    "session_id": "dc01aa86-fb99-4d05-97dc-e79a22650f9a",
    "order_id": "ccf4e76a-f56d-4d3a-abf9-f4fe941f0a1f"
  }
}
```

`ipp.payment.failed` adds a `failure_reason`:

```json
{
  "id": "215c4e46-7614-4cda-9400-e88b5bb6e2b1",
  "type": "ipp.payment.failed",
  "merchant_id": "DM11917000",
  "timestamp": "2026-06-16T12:41:23.544282548Z",
  "data": {
    "session_id": "3574c250-4673-45f2-b9f0-baf21ec6c946",
    "failure_reason": "CARD_DECLINED"
  }
}
```

## Card tap outcomes

The outcomes a Playground [simulated tap](/in-person/test-without-hardware/#3-simulate-a-card-tap)
accepts. Every one but `SUCCESS` is a declined tap, which arrives as `ipp.payment.failed` and
needs nothing from your backend: the clerk retries on the device and the session carries on.
They're also listed in the [error catalog](/api/errors/#errors-in-person-payments).

| Outcome | Result |
|---|---|
| `SUCCESS` | Approved. The session finalizes. The default if you omit `outcome` |
| `DECLINED_GENERIC` | Declined, no specific reason |
| `INSUFFICIENT_FUNDS` | Declined for funds |
| `LOST_CARD` | Reported lost |
| `STOLEN_CARD` | Reported stolen |
| `EXPIRED_CARD` | Expired |
| `INCORRECT_CVC` | Wrong CVC |
| `INCORRECT_NUMBER` | Invalid card number |
| `PROCESSING_ERROR` | Terminal processing error |
| `CARD_VELOCITY_EXCEEDED` | Velocity limit hit |

## Related

- [Webhooks](/basics/conventions/webhooks/) — delivery, signatures and retries for every Kustom event
- [In-Person Payments API](/api/in-person-payments/) — the session endpoints
