#Terminal (in-person)
Accept card-present payments on a physical reader, driven from your own software. Your system creates the payment and hands it to the reader; the customer taps; the result comes back to you by webhook and is available by polling.
#Before you start: getting a reader
Physical readers are registered by PlutoPay. Email info@plutopayus.com
with the reader's serial number and the address it will live at. We register it on the
platform and link it to your account; it then appears in your Dashboard under Terminals and
in GET /v1/terminal/readers. There is no self-service registration for physical readers
yet.
Simulated readers, for test mode, you can create yourself — see Testing without a reader below.
#1 · Find your reader
GET /v1/terminal/readers
{
"data": [
{
"id": "01a0983f-…",
"name": "Office Reader",
"serial_number": "STR70Z1D525003156",
"model": "stripe_s700",
"status": "online",
"processor_terminal_id": "tmr_GqKoBAkoz3WmBy",
"last_seen_at": "2026-09-13T00:51:43+00:00"
}
]
}
Two ids, and they are used in different places:
| Field | Used for |
|---|---|
id (PlutoPay UUID) |
terminal_id when creating the payment — attributes the transaction to this reader |
processor_terminal_id (tmr_…) |
reader_id when processing on the reader |
A test key lists test-mode readers; a live key lists live ones.
#2 · Create the payment
POST /v1/terminal/create-payment
| Field | Type | Required | Description |
|---|---|---|---|
amount |
integer | ✔ | Cents. Minimum 50. |
terminal_id |
uuid | optional | The reader's PlutoPay id. Step 3 attributes the transaction to the reader it is processed on, so this only matters when you want the attribution before step 3 — for example, to report on a payment that was created but never sent to a reader. |
currency |
string | Defaults to your account currency. | |
description |
string | ||
customer_id |
uuid | ||
metadata |
object | Your own keys. Put your order or ticket id here (for example pos_order_id) — it is returned on every webhook and API read, and it is what lets you reconcile a payment to a sale. |
Send an Idempotency-Key header. Your software will retry on a timeout, and without the
key a retry creates a second payment. See Idempotency.
{
"data": {
"id": "019c8044-…",
"reference": "txn_…",
"payment_intent_id": "pi_…",
"client_secret": "pi_…_secret_…",
"amount": 4750,
"currency": "usd",
"status": "pending"
}
}
payment_intent_id is what step 3 needs. client_secret is for SDK-driven readers and can
be ignored on this path.
#3 · Hand it to the reader
POST /v1/terminal/process-payment
| Field | Type | Required | Value |
|---|---|---|---|
payment_intent_id |
string | ✔ | payment_intent_id from step 2 |
reader_id |
string | ✔ | processor_terminal_id from step 1 — the tmr_…, not the UUID |
{ "data": { "reader_id": "tmr_…", "status": "in_progress", "action_type": "process_payment_intent" } }
The reader now shows the amount and waits for a card.
#4 · What happens next
The reader's action moves through these states; you observe them through the transaction, not the reader:
| Reader | Transaction status |
You see it via |
|---|---|---|
| waiting for a card | pending |
— |
| card presented, processing | pending |
— |
| approved and captured | succeeded |
payment.succeeded webhook · GET /v1/transactions/{id} |
| card declined | failed (failure_code, e.g. card_declined) |
payment.failed webhook · GET /v1/transactions/{id} |
| customer cancels on the reader | stays pending |
nothing fires — see below |
A cancel on the device is silent. PlutoPay does not consume reader-side events, so when
the customer presses cancel on the reader the transaction simply stays pending. Treat a
pending that outlives your own timeout as abandoned: cancel it with
POST /v1/transactions/{id}/cancel (step 5). Our own sweep also cancels stale pending
terminal payments, but yours should not wait for it.
Listen for the webhook and poll as a fallback. Both carry card_brand,
card_last_four, receipt_url, platform_fee_cents and tip_amount. Tipping on the
reader changes the final amount — read amount and tip_amount from the result, not from
what you sent.
There is no endpoint that reports the reader's own action state. If you need "is the reader
still waiting?", poll the transaction: pending means yes.
#5 · Cancelling
The customer walked away:
POST /v1/transactions/{id}/cancel
{ "reason": "customer_left" } // optional
For a terminal payment this resets the reader first, then cancels the payment. Read
reader_reset on the response:
reader_reset |
Meaning |
|---|---|
true |
Screen cleared, payment canceled. Done. |
false |
Payment canceled, but the reader could not be reached (warning says why). A card presented now is declined; the screen clears on its own timeout. |
null |
No reader was attributed to this payment — it was never sent to one with process-payment, and no terminal_id was given at creation. There is nothing to reset; if a reader is showing it anyway, POST /v1/terminal/cancel-action. |
Two outcomes you must handle, both 409:
error.code |
What happened | What to do |
|---|---|---|
authorization_in_progress |
The customer had already tapped. The reader refuses to interrupt an authorisation, and so do we — the payment is not canceled. | Poll the transaction. It will be succeeded or failed within seconds. If it succeeded and must not stand, refund it. |
already_captured |
The tap completed and we captured before your cancel arrived. | Refund it. |
This is the race a POS always has — a tap and a cancel in the same second — and it is decided in the customer's favour by design. Your cancel button should expect it.
Cancelling an already-canceled payment returns it with 200; retries are safe.
The reader is stuck (showing "processing" after a network blip, or an amount nobody is going to pay, with no transaction to cancel):
POST /v1/terminal/cancel-action
{ "reader_id": "tmr_…" }
Clears the reader and touches nothing else. Also 409 authorization_in_progress if a
card is mid-authorisation.
A reader reset does not stop a payment. If a card has been read, the authorisation completes and captures even after the reader reports itself reset — observed 6 times in 8 on a simulated reader. To abandon a payment, use
POST /v1/transactions/{id}/cancel, which resets the reader and cancels the intent. Usecancel-actionalone only when there is no payment to cancel.
#6 · Refunds
POST /v1/transactions/{id}/refunds
Full or partial (amount). Send an Idempotency-Key; a retried refund without one is a
second refund. Details in Payments → Refund.
#Testing without a reader
Test mode does not need a physical device. With a test key:
1. Create a simulated reader — once.
POST /v1/terminals
{ "name": "Simulated reader", "registration_code": "simulated-wpe" }
This registers a simulated WisePOS E against your account's test-mode location. If you get
location is required, your account has no test location yet — email us and we will add
one; it takes a minute. The reader then appears in GET /v1/terminal/readers with your test
key.
2. Run steps 2 and 3 above with the simulated reader's tmr_….
3. Present a test card:
POST /v1/terminal/simulate-payment
{ "reader_id": "tmr_…" }
The payment succeeds, payment.succeeded fires to your test webhook endpoint, and
GET /v1/transactions/{id} shows visa ••4242, the receipt URL and the fee. That is the
whole production loop, minus the tap.
POST /v1/terminals with a live key registers a physical reader by its registration
code and requires a live location on your account — which is why physical readers are
registered by us for now.