# Terminal & Session API

Your till already knows the basket total. BaxPay is just the tender it calls — and the card and
the fingerprint never touch your system.

The Session API is how a point-of-sale system — a supermarket ECR, a fuel forecourt controller, a
kiosk — hands a sale to a BaxPay terminal and gets a result back. Your system stays the source of
truth for the basket. BaxPay owns the money and the authentication.

- [You call us](undefined):
  Your till opens a session with the amount. BaxPay drives the terminal — prompts, biometric, PIN —
  and returns the outcome.
- [Cardholder data stays out](undefined):
  PANs and fingerprints flow between the terminal and BaxPay only. Your system never sees them, so
  your PCI scope stays small.
- [Idempotent by design](undefined):
  Every session carries your reference. Retries reconcile to the same sale instead of
  double-charging.

## Authenticating as a connection

In-store calls don't use your API key. They use a **connection credential**, a `bxc_` key provisioned
per till or lane (see [connections](https://developers.baxpay.com/connections)). Every path below starts with
`https://sandbox-api.baxpay.com/v1/integration`.

```text
Authorization: Bearer bxc_q7Vd2kP9xR4mT1sN…
```

The connection decides which terminal gets the sale (its `terminalId`) and the mode: a connection
created with a `bx_test_` key only ever opens test sessions.

## Open a session

- `POST /integration/sessions` — Idempotency-Key required

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions \
  -H "Authorization: Bearer $BAXPAY_CONNECTION" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "saleReferenceId": "till-04-receipt-99182",
    "amountMinor": 4730,
    "currency": "EUR"
  }'
```

```js
const res = await fetch('https://sandbox-api.baxpay.com/v1/integration/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BAXPAY_CONNECTION}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    // Your own receipt id: what you reconcile against later, and what makes retries safe.
    saleReferenceId: 'till-04-receipt-99183',
    amountMinor: 4730, // €47.30, in cents
    currency: 'EUR',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(JSON.stringify(body));
console.log(JSON.stringify(body, null, 2));
```

```json
{
  "success": true,
  "data": {
    "sessionId": "4fba7ed1-…",
    "saleReferenceId": "till-04-receipt-99182",
    "livemode": false,
    "status": "CREATED",
    "type": "SALE",
    "amountMinor": 4730,
    "capturedAmountMinor": null,
    "currency": "EUR",
    "resultCode": null,
    "resultDetail": null,
    "authCode": null,
    "maskedInstrument": null,
    "final": false,
    "updatedAt": "2026-07-16T14:02:10.000Z"
  }
}
```

| Field | Required | Meaning |
| --- | --- | --- |
| `saleReferenceId` | yes | Your receipt or basket id, 1–128 characters. Opening a second session with the same reference on the same connection returns the **existing** session, never a second charge |
| `amountMinor` | yes | Positive integer in minor units (`4730` = €47.30) |
| `currency` | no | ISO code, default `EUR` |
| `type` | no | `SALE` (default) or `PREAUTH` for [fuel](https://developers.baxpay.com/fuel) |
| `methodHint` | no | A hint for the terminal's first screen |

BaxPay queues the sale for the connection's terminal, which prompts the shopper and collects the
authentication factors: the same [SCA gate](https://developers.baxpay.com/payments) as any other BaxPay payment.

## Follow the outcome

- `GET /integration/sessions/{id}` — poll
- `GET /integration/sessions/by-reference/{saleReferenceId}` — find it by your own reference

```bash
curl https://sandbox-api.baxpay.com/v1/integration/sessions/4fba7ed1-… \
  -H "Authorization: Bearer $BAXPAY_CONNECTION"
```

```json
{ "success": true, "data": { "sessionId": "4fba7ed1-…", "status": "CREATED", "final": false } }
```

**Poll until `final` is `true`**, then act on `status`:

| `status` | Final | Meaning |
| --- | --- | --- |
| `CREATED` | no | Received, waiting for the terminal |
| `AWAITING_CUSTOMER` | no | The terminal is showing the amount |
| `AUTHENTICATING` | no | The shopper is authenticating |
| `AUTHORIZED` | no | Funds reserved (fuel pre-auth); capture next |
| `CAPTURED` | yes | Paid: the only outcome that means money moved |
| `DECLINED` | yes | Refused; `resultCode` says why (`DECLINED`, `TIMEOUT`, …) |
| `VOIDED` | yes | You voided it before capture |
| `EXPIRED` | yes | Nobody completed it within 3 minutes of opening; open a new session |
| `FAILED` | yes | Something broke on our side; open a new session |

If your till crashed or timed out and lost the session id, look it up by your own reference. The
lookup only sees sessions opened by the calling connection:

```bash
curl https://sandbox-api.baxpay.com/v1/integration/sessions/by-reference/till-04-receipt-99182 \
  -H "Authorization: Bearer $BAXPAY_CONNECTION"
```

```json
{ "success": true, "data": { "sessionId": "4fba7ed1-…", "saleReferenceId": "till-04-receipt-99182" } }
```

Keep references URL-safe (no `/`), or URL-encode them in this path.

## Void before capture

The shopper walked away? Void the session. Voiding works until the session is final; a captured,
declined or already voided session answers `409`.

- `POST /integration/sessions/{id}/void`

```bash
curl -X POST https://sandbox-api.baxpay.com/v1/integration/sessions/4fba7ed1-…/void \
  -H "Authorization: Bearer $BAXPAY_CONNECTION"
```

```json
{ "success": true, "data": { "sessionId": "4fba7ed1-…", "status": "VOIDED", "resultCode": "ABORTED", "final": true } }
```

There is no refund call on the connection API. A captured in-store payment is refunded from the
Business dashboard.

In sandbox, drive a session to `CAPTURED`, `DECLINED` or a timeout yourself with
[`/simulate`](https://developers.baxpay.com/sandbox#simulating-outcomes).

## Two topologies

- `Local` (you poll):
  Your controller polls `GET /integration/sessions/{id}` until `final: true`. Lowest latency, and it
  keeps working through a WAN blip.
- `Cloud` (we call you):
  Each time the session reaches an outcome (authorized, captured, declined, voided), BaxPay POSTs
  the session view to the connection's `callbackUrl`,
  signed with its `callbackSecret` using the same HMAC scheme as [webhooks](https://developers.baxpay.com/webhooks)
  (`X-BaxPay-Signature-V2`). The body is the session view itself, without a `type` envelope.

> **Callbacks are sent once**
> A cloud callback is best effort: one attempt, a 4-second timeout, no retries. Treat it as a fast
> path, and keep polling (or subscribe to the `payment_session.*` [webhooks](https://developers.baxpay.com/webhooks), which are
> retried) for anything a callback might miss. Verify its signature exactly like a webhook's.

## Who calls what

The till only needs the calls above. `POST /integration/terminal/claim-next`,
`/integration/sessions/{id}/biometric` and `/integration/sessions/{id}/pin` are called by BaxPay
terminals, not by your system.

A connection reaches every session of its merchant in its mode (by id), not only its own. Revoking
one connection cuts that till off, but treat every `bxc_` credential as able to read and void any
of your sessions.
