Skip to content

Terminal sessions

A terminal session is a cashier's working period on one terminal. The terminal opens it when work starts, tells the platform it is still there with a heartbeat every heartbeat_interval_seconds (30 seconds by default), resumes it after a short loss of connection, and closes it when work stops. Like every terminal call, these are made by the app on the terminal, over mutual TLS with the certificate it got by pairing: a merchant's server never calls them.

A session belongs to the terminal that opened it, and is honoured only from the terminal that opened it. It carries no credential of its own: each call presents the terminal's certificate, which is checked on every call, and names the session by its id. Another terminal naming the same id is answered 404 resource_missing, as for a session that does not exist. A terminal has one session at a time.

The merchant and store are the terminal's assignment, never anything the request says. Opening a session is also when the terminal picks up a parameter profile the operations team rolled out to it.

What a session's timing depends on is set by the platform. These are the defaults:

Default
How often to send a heartbeat (heartbeat_interval_seconds)30 seconds
How long without a heartbeat before a session is interrupted3 heartbeat intervals, so 90 seconds after its last heartbeat
How long an interrupted session can be resumed, from its last heartbeat (recovery_deadline)5 minutes
The longest a session lasts, however well it heartbeats (expires_at)12 hours

Each is set by the platform, so go by what the session says, not by these figures: heartbeat every heartbeat_interval_seconds that opening answered, and read recovery_deadline and expires_at from the session itself.

How a terminal session's status changes. Its states: active, recoverable_interrupted, closed (final), expired (final).

  1. active to recoverable_interrupted: missed heartbeats
  2. recoverable_interrupted to active: resume
  3. active or recoverable_interrupted to closed: close
  4. active or recoverable_interrupted to expired: time runs out
  • active: opened, and taking heartbeats.
  • recoverable_interrupted: it missed its heartbeats. It can be resumed until its recovery_deadline.
  • expired: it was not resumed in time, or it reached its expires_at. Final.
  • closed: the terminal closed it, or the platform did. Final.

Time is checked when a heartbeat, a resume or a close is made, not only when the platform's timer runs: a terminal that comes back after six minutes is told its session expired whether or not the timer has got to it yet. Opening is the exception. A replayed open answers the session as it is stored, without checking the clock, so read its status and recovery_deadline, and resume it or open a new session as this guide says.

Open a session when a cashier starts work, with a client_session_token: a string of up to 64 characters that you make for this session and no other. Keep it until opening has answered. The same token sent again answers the same session, as it stands now, so a retry never opens a second one.

Terminal window
curl https://terminal.api.example.com/v1/terminal_sessions \
--cert terminal-certificate.pem --key terminal-key.pem \
-H "Content-Type: application/json" \
-d '{"client_session_token": "shift-2026-10-08-0001"}'
{
"id": "6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10",
"object": "terminal_session",
"livemode": true,
"terminal_id": "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63",
"status": "active",
"opened_at": 1791000000,
"last_heartbeat_at": 1791000000,
"interrupted_at": null,
"recovery_deadline": null,
"expires_at": 1791043200,
"expired_at": null,
"closed_at": null,
"heartbeat_interval_seconds": 30,
"tids": [
{ "route": "card_processor", "tid": "T0001" },
{ "route": "domestic_debit", "tid": "T0002" }
],
"store_label": "Front counter",
"parameter_profile": null
}

The answer is 201, sent with Cache-Control: no-store. Besides the session, it carries what the terminal needs to start work:

  • heartbeat_interval_seconds: how often to send a heartbeat.
  • tids: the terminal IDs bound to the terminal now, one for each card route it has one for.
  • store_label: the store it is assigned to.
  • parameter_profile: the newest parameter profile a rollout completed for this terminal, as name, version and parameters, or null until one has. A profile waiting for the terminal is picked up by opening the session.

Keep the session's id and its client_session_token where they survive the app restarting: the id is how every later call names the session.

Send a heartbeat every heartbeat_interval_seconds, 30 seconds by default. It takes no body, moves the session's last_heartbeat_at to now and changes nothing else, so sending one twice does no harm.

Terminal window
curl -X POST https://terminal.api.example.com/v1/terminal_sessions/6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10/heartbeat \
--cert terminal-certificate.pem --key terminal-key.pem

The answer is the session, still active.

When heartbeats stop. A session is interrupted once 3 heartbeat intervals, 90 seconds by default, have passed after its last heartbeat, so a heartbeat that arrives only at the third interval is already too late. Heartbeat on the interval, not at the edge of it. It is then: its status is recoverable_interrupted, interrupted_at says when, and its recovery_deadline is 5 minutes after its last heartbeat. A heartbeat no longer brings it back: it is refused with 422 session_interrupted, and the terminal resumes the session instead. If it is not resumed by its recovery_deadline, it expires.

After a loss of connection, resume the session before its recovery_deadline. It takes no body.

Terminal window
curl -X POST https://terminal.api.example.com/v1/terminal_sessions/6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10/resume \
--cert terminal-certificate.pem --key terminal-key.pem

The answer is the session, active again, its interrupted_at and recovery_deadline back to null and its last heartbeat now. Then carry on with heartbeats. Resuming a session that is still active answers it as it is, so when a heartbeat is refused with session_interrupted, or the terminal is unsure, resuming is safe.

Past its recovery_deadline the session has expired: resuming it is refused with 422 session_expired, and the terminal opens a new session with a new client_session_token.

Close the session when the cashier stops work. It takes no body, and an interrupted session can be closed too.

Terminal window
curl -X POST https://terminal.api.example.com/v1/terminal_sessions/6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10/close \
--cert terminal-certificate.pem --key terminal-key.pem

The answer is the session, closed, with closed_at. Closing it again answers it unchanged, with the same closed_at, so a retry is safe. An expired session cannot be closed: it is refused with 422 session_expired, and there is nothing left to do with it.

The platform closes a terminal's session too when the operations team suspends, retires, reassigns or unassigns the terminal. Its next call is then refused with 401 terminal_credential_refused. A suspended terminal that is reinstated opens a new session with the same certificate; a terminal moved to another store or merchant is paired again.

A session continues across a certificate renewal: it belongs to the terminal, so heartbeat, resume and close it with the new certificate.

In the usual error envelope, with a stable code.

CallStatuscodeWhenWhat to do
Open400invalid_parameterclient_session_token is missing, not a string, blank or longer than 64 characters.Send a token.
Open409session_already_openEither the terminal already has a session that is active or recoverable_interrupted, or this client_session_token was first sent with an earlier certificate (before a renewal), even if that session is now closed or expired.In the first case, resume or close that session first; a session time has already ended does not count. In the second, resume the session that token opened if it is still live (it carries on across a renewal), or open with a new token if it is over.
Open422merchant_not_activeThe terminal's merchant is suspended or closed.Nothing the terminal can fix.
Open422tenant_not_activeThe merchant's tenant is suspended or closed.Nothing the terminal can fix.
Open422terminal_not_assignedThe terminal is not assigned to a store.Ask the operations team.
Open422terminal_credential_refusedThe credential stopped counting while the session was being opened.As the 401.
Heartbeat422session_interruptedIt missed its heartbeats.Resume it.
Heartbeat, resume422session_closedIt is closed.Open a new session.
Heartbeat, resume, close422session_expiredIt was not resumed in time, or reached its expires_at.Open a new session.
Heartbeat, resume, close404resource_missingThis terminal has no session with this id.Check the id you kept.
Any401terminal_credential_refusedThe certificate does not count, or the terminal is suspended or retired.See When a terminal is refused.
Any503service_unavailableThe platform could not answer, or failed part-way. Whether the call took effect is not known.Retry the same request shortly. When opening, send the same client_session_token, so a session that was opened is answered, not opened twice.

Recovering from errors says what to do when a call times out, and how each of these calls is retried safely.