# Take a payment in person

Send a cart to an enrolled phone or terminal with POST /ipp/v1/sessions, handle the ipp.* callbacks and store the order_id that ipp.session.finalized carries.

When the customer is ready to pay, your backend creates a session — the cart plus the device
in front of them. Kustom pushes it to that device, the clerk collects the payment, and
callbacks tell you how it went. The one that matters carries the `order_id` of the order
Kustom created.

```mermaid
sequenceDiagram
    actor Counter as Clerk and customer
    participant POS as Kustom POS app
    participant Kustom
    participant Server as Your server

    critical Create a session
        %% href: #1-create-a-session
        Server->>+Kustom: POST /ipp/v1/sessions
        Note over Server,Kustom: the cart and a device_id
        Kustom-->>-Server: session_id
        Kustom-)Server: ipp.session.created
        Note over Kustom,Server: asynchronous: it can arrive before the response
    end

    critical Kustom pushes it to the device
        Kustom->>POS: push notification with the session_id
        Note over Kustom,POS: you don't poll
        POS->>Kustom: fetch the session
    end

    critical The clerk collects the payment
        %% href: #2-handle-the-callbacks
        Counter->>POS: the clerk picks a payment method
        POS->>Kustom: attach the payment
        Kustom-)Server: ipp.session.activated
        Counter->>POS: the customer pays by card
    end

    alt Payment succeeds
        %% href: #3-store-the-order-from-ippsessionfinalized
        POS->>Kustom: finalize the payment
        Kustom-)Server: ipp.session.finalized
        Note over Kustom,Server: carries the order_id: store it
        POS->>Counter: receipt or QR code
    else Card declined, and the clerk retries
        POS->>Kustom: finalize the payment
        Kustom-)Server: ipp.payment.failed
        Note over Kustom,Server: nothing for your backend to do
        POS->>Counter: payment failed, try again
    else The sale is off
        %% href: #4-cancel-when-the-sale-is-off
        Server->>Kustom: POST /ipp/v1/sessions/{session_id}/cancel
        Kustom-)Server: ipp.session.cancelled
        Note over Kustom,Server: final, and also sent when the clerk cancels
    end
```

## Before you start

- At least one [enrolled device](/in-person/enroll-a-device/) and its `device_id` — or, in
  Playground, a [simulated one](/in-person/test-without-hardware/).
- A webhook endpoint configured for your account in the Kustom Portal. It's one endpoint for
  the whole account, not one per session — see [Webhooks](/basics/conventions/webhooks/).

## Step by step

### 1. Create a session

[`POST /ipp/v1/sessions`](/api/in-person-payments/create-session/) with the cart and the
`device_id`. Kustom pushes the session to the device by push notification — you don't poll
for delivery.

- **All amounts are in minor units.** `19900` is `199.00` in the session's currency.
- **There's no `location_id` on the session.** The location comes from the device, which
  was bound to one when it [enrolled](/in-person/enroll-a-device/#1-create-an-enrollment).

Store the `session_id` from the response. There's no `order_id` until the session is
finalized.

### 2. Handle the callbacks

Kustom `POST`s lifecycle events to your webhook endpoint as the session moves. **Answer
`200`** — Kustom takes anything else as a failure. Every in-person event is prefixed `ipp.`,
so you can route them apart from other Kustom webhooks arriving at the same endpoint.

`ipp.session.created` is **asynchronous** and can reach you before you've finished
handling the create response. Correlate on `session_id`, which both carry, and make the
handler idempotent.

### 3. Store the order from `ipp.session.finalized`

That's the event that matters. It carries 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"
  }
}
```

Link it to your own order record. From here it's an ordinary order, and the Order
Management API takes it by that `order_id`:

- **[Capture](/order-management/capture/)** it once the goods are handed over, if
  your capture method is manual.
- **[Refund](/order-management/refunds/)** all or part of the amount.
- **[Cancel](/order-management/update-and-cancel/)** it to void the transaction, while
  nothing is captured.

**A failed payment needs nothing from your backend.** `ipp.payment.failed` is information,
not the end of the sale: the clerk retries on the device and the session carries on.
[Session states and callbacks](/in-person/sessions-and-callbacks/) has every state and payload.

### 4. Cancel when the sale is off

[Cancel the session](/api/in-person-payments/cancel-session/) from your backend at any
point before it reaches a final state. Cancellation is final — no payment can be collected
on that session afterwards.

To read a session's current state without waiting for a callback,
[get the session](/api/in-person-payments/get-session/).

## Check that it works

[Test without hardware](/in-person/test-without-hardware/) runs this whole flow in
Playground with a simulated device and card tap. You're done when:

- `ipp.session.finalized` reaches your handler and its `order_id` is linked to your order.
- A declined tap reaches you as `ipp.payment.failed` without ending the sale.
- A session you cancel arrives as `ipp.session.cancelled`.

## Troubleshooting

**Callbacks never arrive.** The webhook endpoint is set per account in the Portal, not in
the session call. Check it's set, and that it answers `200`.

**A callback arrived before the create response was handled.** Expected —
`ipp.session.created` is asynchronous. Correlate on `session_id`.

**The `/ipp/v1` endpoints return `404` or `403`.** In-person payments isn't enabled on the
account. Ask your Kustom representative.

## Related

- [Capture an order](/order-management/capture/) — collecting the money when capture is manual
- [Refunds](/order-management/refunds/) — returning it
- [Session states and callbacks](/in-person/sessions-and-callbacks/) — every state, payload and card tap outcome
