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

# Card terminals

> Find the card terminals assigned to your account, name them, send a payment to one, and share a few terminals between many tablets.

A **terminal** is a card reader that Payra assigns to your account. Your server sends a payment to it, the
customer presents the card there, and the payment then behaves like any other: you read it back, receive
its [webhooks](https://docs.payra.com/webhooks.md) and [refund](https://docs.payra.com/refunds.md) it.

Calls to the API always come from your server. Never put a secret key on a tablet or phone that sits next to
the terminal: have it ask your server, and have your server call Payra.

Terminal endpoints, including check-out and check-in, use the `terminals:read` and `terminals:write`
[scopes](https://docs.payra.com/authentication.md#scopes); sending and canceling a payment use the `payments` scopes.

## Find a terminal

```bash theme={null}
curl "https://api-dashboard.payra.com/v1/terminals?serial_number=1853000123" \
  -H "Authorization: Bearer sk_test_..."
```

```json Response theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "terminal",
      "id": "019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61",
      "label": "Truck 412",
      "model": "AMS1",
      "serial_number": "1853000123",
      "checked_out_to": null,
      "checked_out_at": null,
      "checked_out_by": null,
      "livemode": false
    }
  ],
  "has_more": false
}
```

* Only terminals that are assigned to your account and ready to take payments are listed, in the key's environment. A terminal
  still being set up, moved to another account or retired is not listed, and `GET /terminals/{id}` answers it
  `404 resource_missing`.
* `label` and `serial_number` are exact filters, applied before `starting_after`. Neither is unique, so a
  filter can return more than one terminal.
* Unlike most lists, this one runs oldest first. It takes `limit` from 1 to 100 (default 20), `starting_after`
  and `has_more` as described in [Pagination](https://docs.payra.com/pagination.md). One more difference: a `starting_after` whose
  terminal was retired or moved since the last page still continues after it, so a long walk does not stall.
* `model` and `serial_number` come from the hardware; the serial number is printed on the back of the
  terminal. `label` is your name for the terminal, `null` until you give it one.
* The `id` does not change when you rename the terminal. Pass it as `terminal` on a payment.
* `checked_out_to` and `checked_out_at` say who has the terminal right now, when you
  [share payment devices](#share-payment-devices); `null` while it sits free.

## Name a terminal

```bash theme={null}
curl -X POST https://api-dashboard.payra.com/v1/terminals/019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61 \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "label": "Truck 412" }'
```

The name is 1 to 120 characters, and it also shows in RevOS and in the Virtual Terminal. `null` removes it: the
API then shows `label: null`, and RevOS goes back to the terminal's default name. A name equal to one of the
terminal's own identifiers, such as its serial number, reads back as `null` too.

## Send a payment to a terminal

```bash theme={null}
curl -X POST https://api-dashboard.payra.com/v1/payments \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: 3b9e7a1c-2d4f-4e8a-9c6b-5f1a2e3d4c7b" \
  -H "Content-Type: application/json" \
  -d '{
    "terminal": "019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61",
    "customer": "019e...",
    "amount": 12550,
    "currency": "USD"
  }'
```

The answer is `201` with a `processing` payment: the terminal shows the amount and waits for the card. See
[Terminal payments](https://docs.payra.com/payments.md#terminal-payments) for the request rules and the payment it returns.

* **Follow it** with `GET /payments/{id}`, no faster than every 5 seconds, or with the
  [webhooks](https://docs.payra.com/webhooks.md). It ends `succeeded`, `failed` or `canceled`.
* **One payment per terminal at a time.** A second payment to a terminal still on one answers
  `400 terminal_busy` and creates nothing, so the same `Idempotency-Key` works once the terminal is free.
  If you are unsure whether a payment went through, read it back before sending it again: a new payment with
  a new key, sent after the first one finished, is a second charge.
* **Cancel** with `POST /payments/{id}/cancel` while the terminal waits for the card. Once the card is
  presented it is too late (`400 payment_not_cancelable`), and the payment finishes on its own. A
  `409 conflict` means the cancel could not be confirmed (the terminal's answer was lost, or the payment
  changed while the cancel ran): the payment was not canceled, so retry the cancel.
* **Nobody pays**: after five minutes without a card the payment is `canceled` with
  `cancellation_reason: "expired"`. If the terminal does not confirm that cancel, the payment stays
  `processing` while Payra retries.
* **A lost answer**: if Payra loses the processor's answer when it sends the payment, or the processor fails
  with a server error, the payment stays
  `processing` until Payra learns the outcome. It does not expire, cannot be canceled through the API, and the
  terminal refuses new payments with `terminal_busy` until then. Contact support if it lasts.
* **Refund** it with [`POST /refunds`](https://docs.payra.com/refunds), as any other payment.

## Share payment devices

A few terminals can serve many tablets, trucks or people: a plant keeps its terminals on a charging shelf,
and whoever needs one for the day takes it and brings it back. Payra keeps track of who has which, so your
app does not have to.

* **Terminals belong to your account, not to a device.** Any tablet may send a payment to any of your
  terminals; what ties a terminal to a tablet for the day is the check-out below.
* **Give each terminal a fixed name** (`POST /terminals/{id}` with a `label`, say the name on its sticker)
  and keep it. The check-out, not the name, says who has it, so nothing is renamed as terminals change hands.
* **One payment per terminal at a time**, as always: a second payment to a terminal still on one answers
  `terminal_busy`. A check-out does not change that; a checked-out terminal takes payments as before.

### Check a terminal out

```bash theme={null}
curl -X POST https://api-dashboard.payra.com/v1/terminals/019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61/check-out \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "checked_out_to": "Tablet TB0011", "checked_out_by": "j.doe" }'
```

```json Response theme={null}
{
  "object": "terminal",
  "id": "019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61",
  "label": "PAY101801",
  "model": "AMS1",
  "serial_number": "1853000123",
  "checked_out_to": "Tablet TB0011",
  "checked_out_at": "2026-10-21T12:05:00.000Z",
  "checked_out_by": "j.doe",
  "livemode": false
}
```

* `checked_out_to` is your name for who takes it, 1 to 120 characters: a tablet, a truck, a driver. It shows in
  RevOS next to the terminal.
* `checked_out_by` is optional: your own user doing the check-out, such as the driver or dispatcher signed in to
  your app. Payra keeps it in the terminal's [check-out history](#read-the-history) for your audit.
* A terminal checked out to someone else since the last nightly release answers `400 terminal_checked_out`, and the error carries
  `checked_out_to` naming who has it. Of two apps checking the same terminal out at the same moment, one gets the
  terminal and the other this error, so pick again from the free ones. Nothing changes on a refusal, so the same
  `Idempotency-Key` works once the terminal is free.
* A check-out to whoever already has the terminal since the last nightly release answers `200` as it is, so a request whose answer was lost can
  be sent again.
* `"replace": true` takes the terminal over from whoever has it instead of that error, for a check-in someone
  forgot since the last nightly release. Send it only when you know the terminal is in hand: a replace on a terminal still out in a truck
  leaves Payra saying the wrong person has it.
* Without `replace`, a check-out still open from before the last nightly release (2 AM in your account's time zone) is released
  first, with `end_reason` `nightly_release`, and the terminal is checked out to you, whoever had it. See
  [A day on the shelf](#a-day-on-the-shelf).

### Find a free terminal

```bash theme={null}
curl "https://api-dashboard.payra.com/v1/terminals?checked_out=false" \
  -H "Authorization: Bearer sk_test_..."
```

`checked_out=false` lists the terminals nobody has; `checked_out=true` the ones out, and
`checked_out_to=Tablet%20TB0011` the ones a given tablet has. When the names carry the plant, as in
`PAY101801` for plant 1018, `label_prefix=PAY1018` narrows the list to that plant, so
`?label_prefix=PAY1018&checked_out=false` is "a free terminal at plant 1018". All of these are filters on the
[terminal list](#find-a-terminal), applied before `starting_after`. The plant lives only in the name: renaming
`PAY101801` to `Truck 575` moves it out of that filter, so keep the names fixed and let the check-out say who has
the terminal. A terminal reads `checked_out_to: null`
and `checked_out_at: null` while it sits free.

### Check it back in

```bash theme={null}
curl -X POST https://api-dashboard.payra.com/v1/terminals/019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61/check-in \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "checked_out_to": "Tablet TB0011", "checked_in_by": "j.doe" }'
```

* The answer is the terminal, free again. A terminal that was already free answers `200` as it is.
* `checked_out_to` is optional. When you send it, the check-in only goes through if that is who has the
  terminal; otherwise it answers `400 terminal_checked_out` and nothing changes. Send it from the device
  returning the terminal, so a late check-in cannot free a terminal someone else took since. Leave it out
  from an admin screen that frees a terminal whoever has it.
* `checked_in_by` is optional, your user doing the check-in, kept in the history.

### Read the history

```bash theme={null}
curl "https://api-dashboard.payra.com/v1/terminals/019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61/check-outs" \
  -H "Authorization: Bearer sk_test_..."
```

```json Response theme={null}
{
  "object": "list",
  "data": [
    {
      "object": "terminal_check_out",
      "id": "019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a71",
      "terminal": "019f2c4e-7b1a-7d3e-9a10-3c5b8e2f4a61",
      "checked_out_to": "Tablet TB0011",
      "checked_out_by": "j.doe",
      "checked_out_at": "2026-10-21T12:05:00.000Z",
      "checked_in_at": null,
      "checked_in_by": null,
      "end_reason": null,
      "livemode": false
    }
  ],
  "has_more": false
}
```

* One row per check-out, newest first. The first row is the current one while its `checked_in_at` is `null`.
* `end_reason` says how a check-out ended: `check_in`; `replaced`, when a check-out with `replace` took the
  terminal over; `nightly_release`, when Payra freed it because it was still out from before the last 2 AM (see [A day on the shelf](#a-day-on-the-shelf)),
  with `checked_in_by` set to `Payra`; `dashboard_check_in`, when one of your users checked it in from RevOS,
  with `checked_in_by` set to their name; `unassigned`, when Payra took the terminal off your account; or
  `transferred`, when Payra moved it to another account. Either of the last two ends the check-out with it.
  More values may be added, so treat one you do not recognize as an ended check-out.
* The history lists your account's check-outs and is readable while the terminal is assigned to your account;
  otherwise it answers `404 resource_missing`, like the terminal itself.
* `limit` 1 to 100 (default 20) and `starting_after` page it as in [Pagination](https://docs.payra.com/pagination.md).

### A day on the shelf

1. At clock-in, your app lists `GET /terminals?checked_out=false`, picks one and checks it out to the
   tablet. It tells the driver which terminal to grab, by its name.
2. During the day, the tablet sends payments with that terminal's `id`.
3. At clock-out, the tablet checks it in with its own `checked_out_to`. If the driver forgot, an admin
   screen checks it in without `checked_out_to`, someone who manages devices checks it in from Devices in
   RevOS, or the next check-out sends `"replace": true` once someone has confirmed the terminal is back on
   the shelf. Without `replace`, a new check-out of a terminal someone has had since the last nightly release answers `terminal_checked_out`.
4. Every night Payra checks in any terminal still checked out from before 2 AM in your account's time zone
   (US Central when none is set; 3 AM on the night clocks spring forward), so each day starts with every
   terminal free. The hourly release does it at 2 AM in every US time zone (within the hour after 2 AM in half-hour zones such as Newfoundland), or at the next hour if a run is late, and a check-out
   that finds such a terminal first frees it then. A terminal checked out after 2 AM is left alone.
5. A terminal Payra takes off your account or moves to another account leaves the list; its check-out ends with
   it.


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