---
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`.

### Impresión automática del comprobante

Envía `print_receipt: true` para mandar el comprobante de pago a la impresora una vez que el cobro sea exitoso. La instrucción es best-effort: el resultado de impresión no se expone por API y una impresora sin papel, ocupada o con error no revierte el pago ni retrasa `payment_intent.succeeded`.

### 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`, `amount` o `items` — solo uno.
  - `amount` number, double — Monto a cobrar en unidades (ej. 50.00). Alternativa a `amount_in_cents`.
  - `items` TerminalSessionCommandsPostRequestBodyContentApplicationJsonSchemaItemsItems[] — Cobra productos existentes en vez de un monto suelto: cada item referencia un precio y el total se calcula sumando los items. No se puede combinar con `amount_in_cents`/`amount` ni con `description` (cada producto ya tiene nombre). Solo productos de pago único, todos en la moneda del cobro (`currency`). Con una cuenta conectada como comercio de registro, los precios deben pertenecer a esa cuenta.
    - `price_id` string — ID del precio a cobrar (`pri_...`). Envía `price_id` o `product_id` en cada item.
    - `product_id` string — ID del producto (`prod_...`); se cobra su precio vigente. Alternativa a `price_id`.
    - `quantity` integer — Unidades de este item
  - `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.
  - `description` string — (Opcional) Concepto del cobro, por ejemplo el número de orden de tu sistema. Aparece en el recibo del cliente y en la actividad del comercio (ej. "Pago POS por Orden #1234"). Si se omite, el cobro se muestra sin concepto, como un pago rápido. No se puede combinar con `items`.
  - `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.
  - `print_receipt` boolean — Imprime automáticamente el comprobante de pago después de un cobro exitoso. El resultado de impresión no cambia el estado financiero del pago.
  - `application_fee_amount` integer — (Opcional) Comisión de plataforma en centavos. Requiere que el cobro sea a nombre de una cuenta conectada (modelo directo): cuando el cobro se completa, Recurrente transfiere este monto del balance de la cuenta conectada al de tu plataforma. La comisión se puede revertir al reembolsar con `refund_application_fee: true`, y se incluye en la facturación diaria de comisiones (DTE) si la conexión la tiene habilitada. No se puede combinar con un `transfer_setups` de `purpose: platform_commission`.
  - `transfer_setups` TerminalSessionCommandsPostRequestBodyContentApplicationJsonSchemaTransferSetupsItems[] — (Opcional) Transferencias para enrutar fondos a una cuenta conectada tras un cobro exitoso (modelo destino). El destinatario debe ser tu cuenta o una cuenta conectada. Para cobrar una comisión de plataforma usa `application_fee_amount` en su lugar.
    - `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' — Legacy — omítelo. `fund_split` (el valor predeterminado) es una distribución ordinaria de fondos. `platform_commission` marca la transferencia como comisión de plataforma; para eso usa `application_fee_amount`, que crea la misma transferencia sin que tengas que armarla a mano. Se sigue aceptando por compatibilidad y no se puede combinar con ese parámetro.

## 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. Para un cobro con `items`, la suma de los items.
  - `currency` 'GTQ' | 'USD' — Moneda del cobro
  - `installments` integer, nullable — Meses sin intereses solicitados, o `null` si el cobro va sin cuotas
  - `description` string, nullable — Concepto del cobro enviado al crear el comando, o `null` si no se envió uno
  - `show_post_payment_screens` boolean — Indica si el comando solicita mostrar las pantallas post-pago de NIT, correo y teléfono
  - `print_receipt` boolean — Indica si el comando solicita imprimir automáticamente el comprobante de pago
  - `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

## Changes

- **2026-09-02** `da12dce50f30` — 6 info
  - added the new optional request property `application_fee_amount`
  - added the new optional request property `description`
  - added the new optional request property `items`
  - added the new optional request property `print_receipt`
  - …2 more

[Change history](https://skmtc.dev/recurrente/apis/referencia-api/changes/terminal_session_commands/post.md)

---

[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/da12dce50f30/schema)
