---
title: "Crear un comando de terminal"
method: POST
path: "/terminal_session_commands"
tags: ["terminalSessionCommands"]
---

# Crear un comando de terminal

`POST /terminal_session_commands`

Envía un comando de cobro a una terminal POS. Recurrente crea un checkout y lo despacha a la terminal indicada. Si ya existe un comando activo con el mismo `external_id`, retorna el comando existente en vez de crear uno nuevo (idempotencia).

Cuando la terminal recibe el comando, muestra automáticamente la pantalla de cobro para que el cliente pague con tarjeta.

**Usa una llave LIVE para una terminal física.** Una llave TEST heredada que todavía apunta a la cuenta LIVE se rechaza con `403 terminal_test_key_requires_sandbox`, antes de crear el checkout o mover dinero. Las llaves TEST solo se aceptan cuando la solicitud ya está aislada dentro de un Sandbox; ese flujo no contacta hardware ni procesadores reales.

La terminal debe estar en **Modo espera** y reportando disponibilidad. Si no lo está, la API responde `409` con `code: terminal_not_in_standby` sin crear un checkout nuevo. Los reintentos con un `external_id` existente conservan la idempotencia y retornan el comando original. Una respuesta exitosa incluye `terminal_availability`.

### Flujo

1. Tu sistema envía `POST /api/terminal_session_commands` con el monto, moneda y terminal.
2. Recurrente crea un checkout y un comando en estado `pending`.
3. La terminal levanta el comando y lo pasa a `dispatched`.
4. El cliente paga en la terminal.
5. Recibes un webhook `payment_intent.succeeded` con el resultado.

También puedes consultar el comando con `GET /api/terminal_session_commands/{random_id}`. Los estados definitivos son `canceled`, `superseded`, `consumed` y `failed`; `pending`, `dispatched` y `cancel_requested` todavía pueden cambiar.

### Idempotencia

Si envías dos requests con el mismo `external_id` dentro de la misma cuenta, el segundo retorna el comando original sin crear uno duplicado. Dos cuentas distintas pueden usar el mismo `external_id`. Esto te permite reintentar de forma segura.

### Superseding

Si envías un nuevo comando a la misma terminal (con un `external_id` diferente), los comandos anteriores pendientes se marcan como `superseded` y la terminal solo procesa el más reciente.

### Meses sin intereses (installments)

Si quieres que el cobro se procese en cuotas, envía `installments` con el número de meses. Solo aplica a cobros en `GTQ` y los valores permitidos son `3`, `6`, `12` o `18` (algunas cuentas tienen configuraciones distintas). Si la tarjeta del cliente no soporta la opción elegida, el cobro se rechaza con `unsupported_installments`.

### Pantallas post-pago

Por defecto, después de un pago exitoso la terminal muestra las pantallas para solicitar NIT, correo y teléfono. Envía `show_post_payment_screens: false` para omitirlas y volver automáticamente a Modo espera. Recurrente emite la factura como C/F cuando corresponde y adelanta el webhook y los correos que normalmente esperan a que el comprador termine esas pantallas.

Si el monto y la configuración de facturación hacen obligatorio un NIT válido, Recurrente conserva las pantallas aunque envíes `false`.

### Cuentas conectadas

Para originar el cobro desde una plataforma y registrarlo en una cuenta hija:

1. Autentica el request con la llave LIVE de la plataforma en `X-SECRET-KEY` y envía el ID `ac_...` de la cuenta hija en `X-ACCOUNT-ID`.
2. Obtén el `terminal_id` público (`trm_...`) en el panel de la cuenta hija, en **POS → detalle de la terminal**, y guárdalo en tu configuración. Actualmente no existe un endpoint público para listar terminales.
3. Confirma que el dispositivo inició sesión en esa misma cuenta hija y está en **Modo espera**. Un pinpad emparejado con la plataforma es invisible para la hija (y viceversa).
4. Envía el comando con `terminal_id`, monto, moneda y un `external_id` único de tu sistema. La respuesta incluye el `id` del comando (`tsc_...`), `checkout_id`, `status` y `terminal_availability`.

El checkout, el pago y la factura se crean bajo la cuenta hija. La
plataforma recibe `payment_intent.succeeded` con `connected: true` y el
`account_id` de la hija; si la hija también tiene un webhook endpoint,
Recurrente entrega el evento a ambos. Usa `checkout.metadata.external_id`
para conciliar la orden original, `checkout.metadata.terminal_id` para
identificar el dispositivo y `tax_invoice_url` para recuperar la factura
cuando exista.

Cuando algo no calza, el 404 incluye un `code` que identifica cuál de las tres cosas falta:

| `code` | Qué revisar |
|---|---|
| `connected_account_not_found` | La cuenta que enviaste no está conectada a la tuya (o es nieta, no hija directa). |
| `terminal_not_found` | La terminal no está asociada a la cuenta que va a cobrar. |
| `connected_account_mismatch` | Enviaste `account_id` de una hermana distinta a la del header `X-ACCOUNT-ID`. |
| `recipient_not_found` | El `recipient_id` de un `transfer_setup` no es tu cuenta ni una hija conectada. |

## Headers

- `X-SECRET-KEY` string, required

## Request body

- object
  - `terminal_id` string, required — ID público `trm_...` de la terminal POS donde se enviará el cobro. Cópialo desde **POS → detalle de la terminal** en la cuenta que va a cobrar; actualmente no existe un endpoint público para listar terminales.
  - `amount_in_cents` integer — Monto a cobrar en centavos. Envía `amount_in_cents` o `amount`, no ambos.
  - `amount` number, double — Monto a cobrar en unidades (ej. 50.00). Alternativa a `amount_in_cents`.
  - `currency` 'GTQ' | 'USD', required — Moneda del cobro
  - `external_id` string, required — ID único de tu sistema para este cobro dentro de la cuenta autenticada. Se usa para idempotencia — si envías el mismo `external_id` dos veces en esa cuenta, no se crea un duplicado.
  - `installments` '3' | '6' | '12' | '18' — Número de meses sin intereses. Solo válido con `currency: GTQ`. Valores permitidos por defecto `[3, 6, 12, 18]` (puede variar por cuenta).
  - `show_post_payment_screens` boolean — Muestra las pantallas post-pago de NIT, correo y teléfono. Envía `false` para omitirlas y volver a Modo espera, salvo cuando un NIT válido sea obligatorio.
  - `transfer_setups` TerminalSessionCommandsPostRequestBodyContentApplicationJsonSchemaTransferSetupsItems[] — (Opcional) Transferencias a ejecutar tras un cobro exitoso. Úsalo para enrutar fondos a una cuenta conectada (modelo destino) o para cobrar una comisión a una subcuenta. El destinatario debe ser tu cuenta o una cuenta conectada.
    - `amount_in_cents` integer — Monto en centavos a transferir. No puede exceder el monto neto disponible después de fees, FEL e IVA.
    - `recipient_id` string — ID de la cuenta destinataria (tu cuenta o una cuenta conectada). Si se omite, se usa tu cuenta.
    - `purpose` 'fund_split' | 'platform_commission' — Usa `platform_commission` cuando una cuenta conectada paga esta comisión a tu plataforma.

## Response `201`

Comando creado exitosamente

- TerminalSessionCommand
  - `id` string — ID público aleatorio del comando
  - `external_id` string — ID único de tu sistema dentro de la cuenta autenticada
  - `status` 'pending' | 'dispatched' | 'cancel_requested' | 'canceled' | 'superseded' | 'consumed' | 'failed' — Estado del comando
  - `final` boolean — `true` cuando el comando alcanzó un resultado definitivo y ya no puede cambiar
  - `terminal_id` string — ID de la terminal POS
  - `amount_in_cents` integer — Monto en centavos
  - `currency` 'GTQ' | 'USD' — Moneda del cobro
  - `installments` integer, nullable — Meses sin intereses solicitados, o `null` si el cobro va sin cuotas
  - `show_post_payment_screens` boolean — Indica si el comando solicita mostrar las pantallas post-pago de NIT, correo y teléfono
  - `terminal_availability` 'available' | 'unavailable' — Disponibilidad observada de la terminal para recibir comandos
  - `checkout_id` string — ID del checkout generado
  - `checkout_url` string, uri — URL del checkout (la terminal usa esta URL internamente)
  - `checkout_status` 'unpaid' | 'paid' | 'failed' | 'payment_in_progress' | 'needs_verification' | 'no_payment_required' | 'verification_failed' | 'needs_payer_authentication' — Estado actual del checkout asociado
  - `cancellation_requested_at` string, date-time, nullable — Momento en que se solicitó detener un comando ya despachado
  - `canceled_at` string, date-time, nullable — Momento en que la cancelación se volvió definitiva

## Other responses

- `403` — La llave TEST no está aislada en un Sandbox
- `404` — No encontramos la terminal o la cuenta conectada
- `409` — La terminal no está disponible para recibir comandos
- `422` — Error de validación

---

[API](https://skmtc.dev/recurrente/apis/referencia-api.md) · [All operations](https://skmtc.dev/recurrente/apis/referencia-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/recurrente/referencia-api/revisions/f494a078482c/schema)
