> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.recurrente.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recurrente.com/_mcp/server.

# Terminal Sessions

> Send charges to Recurrente POS terminals from your system

**Terminal Sessions** lets you send charges to a Recurrente POS terminal directly from your system (ERP, custom POS, or any integration). Your system sends a command with the amount, and the terminal automatically displays the payment screen for the customer to pay with their card.

## Use cases

* **ERP/POS integration**: Your billing or point-of-sale system generates the charge, and the Recurrente terminal processes it.
* **Remote charges**: Send charges to terminals at different locations from a centralized system.
* **Automation**: Eliminate the need to manually enter amounts on the terminal.

## Prerequisites

* A Recurrente account with API keys
* At least one POS terminal linked to your account
* Your terminal ID (found in **POS** in your dashboard — click on a terminal to see its ID)

## Set up the terminal in standby mode

For the terminal to receive commands from your system, it must be on the **standby screen**. This screen shows "Listo para cobrar" (Ready to charge) and automatically waits for new commands.

1. In your dashboard, go to **POS**
2. Click on the terminal you want to configure
3. On the terminal detail page, click **Modo espera** (top-right corner)
4. The terminal will display the waiting screen

When your system sends a command, the terminal automatically displays the payment screen. After each payment (successful or failed), the terminal returns to standby mode to receive the next command.

> **Note**
>
> In standby mode, the terminal operator cannot manually enter amounts. All charges are controlled from your system via the API.

## Integration flow

```
Your System                   Recurrente                    POS Terminal
    │                              │                              │
    │  POST /terminal_session_     │                              │
    │  commands                    │                              │
    │  {terminal_id, amount,       │                              │
    │   currency, external_id}     │                              │
    │─────────────────────────────>│                              │
    │                              │  Creates checkout + command  │
    │                              │  (status: pending)           │
    │  {id, status, checkout_url}  │                              │
    │<─────────────────────────────│                              │
    │                              │                              │
    │                              │  Terminal picks up command   │
    │                              │  (status: dispatched)        │
    │                              │─────────────────────────────>│
    │                              │                              │
    │                              │             Customer pays    │
    │                              │                with card     │
    │                              │                              │
    │  Webhook:                    │                              │
    │  payment_intent.succeeded    │                              │
    │<─────────────────────────────│                              │
```

## 1. Create a terminal command

Send a `POST` to `/api/terminal_session_commands` with the amount and target terminal.

```bash
curl -X POST https://app.recurrente.com/api/terminal_session_commands \
  -H "X-SECRET-KEY: your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "terminal_id": "trm_abc123",
    "amount_in_cents": 5000,
    "currency": "GTQ",
    "external_id": "order-1234"
  }'
```

### Response

* **`201 Created`** — a new command was created
* **`200 OK`** — an active command with the same `external_id` already exists (the existing one is returned)

If you receive `200` and the status is `dispatched`, it means the command was already picked up by the terminal. Use a new `external_id` to create a fresh command.

```json
{
  "id": 42,
  "external_id": "order-1234",
  "status": "pending",
  "terminal_id": "trm_abc123",
  "amount_in_cents": 5000,
  "currency": "GTQ",
  "checkout_id": "ch_xyz789",
  "checkout_url": "https://app.recurrente.com/checkout-session/ch_xyz789"
}
```

### Parameters

| Parameter         | Type    | Required | Description                                                                                                                                                                                          |
| ----------------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `terminal_id`     | string  | Yes      | POS terminal ID                                                                                                                                                                                      |
| `amount_in_cents` | integer | Yes\*    | Amount in cents (e.g. 5000 = Q50.00)                                                                                                                                                                 |
| `amount`          | number  | Yes\*    | Amount in units (e.g. 50.00). Alternative to `amount_in_cents`                                                                                                                                       |
| `currency`        | string  | Yes      | Currency: `GTQ` or `USD`                                                                                                                                                                             |
| `external_id`     | string  | Yes      | Your system's ID for this charge (used for idempotency)                                                                                                                                              |
| `description`     | string  | No       | Charge concept (e.g. your order number). Shown on the customer receipt and in the merchant's activity feed. Cannot be combined with `items`                                                          |
| `items`           | array   | No       | Existing products to charge: `[{price_id \| product_id, quantity}]`. The total is computed from the items — don't send `amount` or `description`. One-time products only, all in the charge currency |
| `tax_id`          | string  | No       | The buyer's NIT, when your system already has it. The terminal skips the NIT screen and opens the receipt step with the invoice already issued                                                       |

> **Note**
>
> Send `amount_in_cents` **or** `amount`, not both. If you send `amount`, it is automatically converted to cents.

## 2. Receive the payment result

Register a [webhook endpoint](/referencia-api/api-reference/webhook-endpoints/create-webhook-endpoint) to receive notifications when the payment completes.

Listen for the `payment_intent.succeeded` event:

```json
{
  "event": "payment_intent.succeeded",
  "data": {
    "checkout": {
      "id": "ch_xyz789",
      "metadata": {
        "source": "terminal_session_webhook",
        "terminal_id": "trm_abc123",
        "external_id": "order-1234"
      }
    },
    "amount_in_cents": 5000,
    "currency": "GTQ"
  }
}
```

Use the `metadata.external_id` field to match the payment back to the order in your system.

## Command statuses

| Status       | Description                                                |
| ------------ | ---------------------------------------------------------- |
| `pending`    | Created, waiting for the terminal to pick it up            |
| `dispatched` | Terminal received the command and is processing the charge |
| `consumed`   | Charge completed successfully                              |
| `superseded` | Replaced by a newer command to the same terminal           |
| `failed`     | Charge failed                                              |

## Printing receipts

After an approved charge the terminal prints the **customer copy** if you sent `print_receipt: true`. For the **merchant copy** — or to reprint an earlier sale — send a separate print command:

```bash
curl -X POST https://app.recurrente.com/api/terminal_print_commands \
  -H "X-SECRET-KEY: $RECURRENTE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "terminal_id": "trm_abc123",
    "checkout_id": "ch_xyz789",
    "copy": "merchant",
    "external_id": "order-1234-merchant-copy"
  }'
```

The terminal prints without leaving standby, so the next charge still arrives. The `checkout_id` comes from the charge command's response.

| Parameter     | Type    | Required | Description                                                                            |
| ------------- | ------- | -------- | -------------------------------------------------------------------------------------- |
| `terminal_id` | string  | Yes      | POS terminal ID                                                                        |
| `checkout_id` | string  | Yes      | Checkout ID of the sale to print                                                       |
| `copy`        | string  | No       | `merchant` (default) or `customer`                                                     |
| `duplicate`   | boolean | No       | Prints **DUPLICADO** on the paper. Use it when reprinting a copy that already came out |
| `external_id` | string  | No       | Your own ID, for idempotency                                                           |

> **Note**
>
> Printing is best-effort: it reaches a terminal sitting in standby and the result is not exposed over the API. It never affects the payment's financial state.

## Idempotency

The `external_id` is unique per account. If you send the same `external_id` twice:

* If the previous command is **active** (pending or dispatched), the existing command is returned.
* If the previous command was **superseded** or **failed**, the existing command is returned without creating a new one.

This lets you safely retry requests without creating duplicate charges.

## Superseding (command replacement)

If you send a new command to the same terminal with a different `external_id`, all previous pending commands for that terminal are marked as `superseded`. The terminal only processes the most recent command.

This is useful when:

* The customer changes the order amount
* You need to cancel a charge and send a new one
* Your system retries with a new order ID

## Full example

```python
import requests

API_URL = "https://app.recurrente.com/api"
HEADERS = {
    "X-SECRET-KEY": "your_secret_key",
    "Content-Type": "application/json"
}

def send_to_terminal(terminal_id, amount, currency, order_id):
    """Send a charge to a POS terminal."""
    response = requests.post(
        f"{API_URL}/terminal_session_commands",
        headers=HEADERS,
        json={
            "terminal_id": terminal_id,
            "amount": amount,
            "currency": currency,
            "external_id": order_id
        }
    )
    return response.json()

# Send a Q150.00 charge to the terminal
result = send_to_terminal("trm_abc123", 150.00, "GTQ", "order-5678")
print(f"Command created: {result['status']}")
```