▤ ControlPOS DevelopersReferencia de API ↗

DE LA SOLICITUD AL COMPROBANTE

Integra tu primer ticket,
paso a paso.

Dos casos completos, los JSON que debes enviar y cómo interpretar cada respuesta.

Los datos son ilustrativos. Necesitas un código de alta y un comercio de pruebas. La API no ejecuta el pago; los campos EMV y autorización del servidor actual son simulados.

1. Preparar el entorno

Estos comandos usan Bash, curl y Node.js con fetch disponible. En Windows puedes usar Git Bash o seguir el ejemplo Postman sin terminal. Solicita el origen HTTPS de pruebas, la serie y el código al administrador.

# Bash (Linux, macOS o Git Bash). Sustituye los valores REEMPLAZAR.
export BASE_URL='https://REEMPLAZAR-ORIGEN-DE-PRUEBAS'
export TERMINAL_SERIAL='MARCA-MODELO-0001'
# Obtén primero el código de alta del administrador del comercio.
# Nunca uses estos datos ilustrativos como credenciales reales.

El origen publicado es https://ticket.control-pos.com; úsalo para pruebas solo con un comercio autorizado. BASE_URL no debe terminar en /api ni en una barra. En todos los archivos sustituye terminalSerial por la misma serie que registraste.

2. Registrar la terminal

POST /api/terminals/enroll · Sin token. Sustituye el código y la serie en el JSON. Ejecuta este paso una sola vez.

curl --silent --show-error --include --request POST "$BASE_URL/api/terminals/enroll" \
  --header 'Content-Type: application/json' \
  --data '{"enrollmentCode":"CPE-REEMPLAZAR","serial":"MARCA-MODELO-0001","tokenLabel":"Modelo · Sucursal de pruebas"}'

Esperado: 201 Created. La respuesta contiene terminal, token_id, token y token_expires_at. Copia el token real al almacén seguro de tu aplicación. El servidor no vuelve a mostrarlo. Un token_expires_at: null indica que no hay una expiración programada, pero puede revocarse.

Si perdiste la respuesta, revisa el alta con el administrador antes de volver a canjear. Un código inválido, vencido, agotado o revocado devuelve 401 con code: "codigo_de_alta_invalido".

3. Caso A: consumo en efectivo

Resultado esperado: 110.00 MXN. Un concepto de 100.00 más 10.00 de propina. Descarga ticket-example.json en tu carpeta de trabajo.

{
  "terminalSerial": "MARCA-MODELO-0001",
  "currency": "MXN",
  "items": [
    {
      "name": "Consumo",
      "qty": 1,
      "price": 100
    }
  ],
  "tip": 10,
  "transactionType": "venta",
  "capture": {
    "orderReference": "ORDEN-0001",
    "paymentMethod": "efectivo"
  }
}
# Descarga ticket-example.json y sustituye terminalSerial por la serie registrada.
# Introduce el token sin guardarlo en el historial del shell:
read -r -s -p 'Token de terminal: ' TERMINAL_TOKEN; echo
export TERMINAL_TOKEN
# Una clave NUEVA para la primera operación. No cambies esta clave al reintentar.
export OPERATION_KEY="$(node -e 'console.log(require("node:crypto").randomUUID())')"
curl --silent --show-error --include --request POST "$BASE_URL/api/tickets" \
  --header 'Content-Type: application/json' \
  --header "X-Terminal-Token: $TERMINAL_TOKEN" \
  --header "Idempotency-Key: $OPERATION_KEY" \
  --data-binary @ticket-example.json

Guarda el cuerpo y la clave antes de enviar en una integración real. Si falla la red, vuelve a ejecutar únicamente el comando curl, sin generar otra clave. Nunca cambies el cuerpo del mismo intento.

4. Caso B: consumo con tarjeta y propina

Resultado esperado: 187.00 MXN. Dos cafés × 45.00 + un sándwich de 80.00 = 170.00; propina de 17.00. La aplicación de pagos ya debe haber confirmado la operación. Descarga ticket-card-example.json.

{
  "terminalSerial": "MARCA-MODELO-0001",
  "currency": "MXN",
  "items": [
    {
      "name": "Café americano",
      "qty": 2,
      "price": 45
    },
    {
      "name": "Sándwich",
      "qty": 1,
      "price": 80
    }
  ],
  "tip": 17,
  "transactionType": "venta",
  "card": {
    "brand": "visa",
    "type": "debito",
    "last4": "1234",
    "entryMode": "contactless"
  },
  "capture": {
    "orderReference": "ORDEN-0002",
    "cashier": "CAJA-01",
    "operationNumber": "000123",
    "batchNumber": "001",
    "affiliation": "AFILIACION-DE-PRUEBA",
    "paymentMethod": "tarjeta",
    "capturedAt": "2026-09-04T18:00:00Z",
    "transaccionId": "3f2a9c1e-7b4d-4e8f-9a6b-1c2d3e4f5a6b",
    "rrn": "000012345678",
    "location": { "latitude": 25.651234, "longitude": -100.289876, "label": "Av. Garza Sada, Monterrey" }
  },
  "receiptTemplate": "consumo-restaurante"
}

Reemplaza la serie, referencia, hora, afiliación y datos enmascarados por los datos de tu operación de prueba. brand es la marca de la tarjeta, no la de la terminal. last4 es texto de cuatro dígitos. No envíes PAN, PIN o CVV. capture.transaccionId (UUID) y capture.rrn (DE-37) identifican la autorización que ya dio el switch para este cobro: en ventas con tarjeta son obligatorios y deben coincidir con el RRN, la terminal y el importe autorizados. capture.location es opcional: coordenadas del punto de venta que se imprimen en el voucher.

# Otra operación: asigna una clave nueva y conserva su cuerpo y clave para reintentar.
export OPERATION_KEY="$(node -e 'console.log(require("node:crypto").randomUUID())')"
curl --silent --show-error --include --request POST "$BASE_URL/api/tickets" \
  --header 'Content-Type: application/json' \
  --header "X-Terminal-Token: $TERMINAL_TOKEN" \
  --header "Idempotency-Key: $OPERATION_KEY" \
  --data-binary @ticket-card-example.json

La clave de este caso es distinta a la del caso A porque representa otra operación. Para recuperar esta emisión se conserva la nueva clave.

5. Interpretar la respuesta

201 Created significa que se creó un ticket. 200 OK significa que se recuperó una emisión anterior con la misma clave y cuerpo. En ambos casos guarda el mismo id.

{
  "id": "11111111-2222-4333-8444-555555555555",
  "terminal_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
  "folio": 123,
  "amount": "187.00",
  "base_amount": "170.00",
  "tip_amount": "17.00",
  "transaction_type": "venta",
  "currency": "MXN",
  "items": [
    {
      "name": "Café americano",
      "qty": "2",
      "price": "45.00"
    },
    {
      "name": "Sándwich",
      "qty": "1",
      "price": "80.00"
    }
  ],
  "card_brand": "visa",
  "card_type": "debito",
  "card_last4": "1234",
  "entry_mode": "contactless",
  "emv_aid": "A0000000031010",
  "emv_app_label": "VISA",
  "emv_arqc": "0123456789ABCDEF",
  "emv_tvr": "0000000000",
  "auth_code": "123456",
  "receipt_svg": "<svg xmlns=\"http://www.w3.org/2000/svg\"><!-- contenido abreviado --></svg>",
  "created_at": "2026-09-04T18:00:01Z",
  "expires_at": "2026-10-04T18:00:01Z",
  "order_reference": "ORDEN-0002",
  "cashier": "CAJA-01",
  "operation_number": "000123",
  "batch_number": "001",
  "affiliation": "AFILIACION-DE-PRUEBA",
  "installments": null,
  "payment_method": "tarjeta",
  "terminal_captured_at": "2026-09-04T18:00:00Z",
  "receipt_template_code": "consumo-restaurante",
  "issued_source": "tpv",
  "issued_by": "MARCA-MODELO-0001",
  "issued_trace_id": "ejemplo-traza-001",
  "merchant_id": null,
  "terminal_serial": null,
  "terminal_merchant_name": null,
  "views_count": null,
  "last_viewed_at": null,
  "view_dates": null
}

Ejemplo ilustrativo con todos los campos del DTO; el contenido de receipt_svg está abreviado. Los UUID, folio, horas y valores EMV no corresponden a una emisión real.

CamposCómo usarlos
id / folioid identifica el recurso y permite construir la URL; folio es el número visible del comprobante.
base_amount / tip_amount / amountCadenas decimales: 170.00 + 17.00 = 187.00. No confundir qty y price numéricos de entrada con las cadenas de salida.
receipt_svgRepresentación del comprobante. Renderizar según el dispositivo; no es un comando de impresión.
created_at / expires_atFecha de emisión y vencimiento. La retención depende de la instancia; no calcules una fecha fija en el cliente.
terminal_captured_at / issued_trace_idHora que reportó la terminal y correlación de soporte.
merchant_id / terminal_serial / terminal_merchant_name / views_count / last_viewed_at / view_datesCampos del listado/panel. Pueden estar en null en la respuesta de emisión; conserva tu serie configurada.
Descargar respuesta de referencia ↓

6. Entregar QR, enlace o PDF

El contenido del QR es exactamente el enlace HTTPS al ticket. Genera el QR con la biblioteca de tu aplicación. Para descargar el PDF no se envía token.

# Sustituye por el id real de la respuesta; el UUID de esta página es ilustrativo.
export TICKET_ID='REEMPLAZAR-ID-RECIBIDO'
echo "$BASE_URL/t/$TICKET_ID"
curl --fail --silent --show-error "$BASE_URL/t/$TICKET_ID/pdf" --output ticket.pdf

No hay un campo qr_url en esta respuesta: construye la URL con el origen configurado y el id. Si la impresora falla, reutiliza ese comprobante; no vuelvas a cobrar ni crear otro ticket.

7. Ejemplos de errores y corrección

400 · Últimos cuatro dígitos inválidos

Enviar last4: "123" puede devolver:

{
  "error": "last4 deben ser exactamente 4 digitos",
  "code": "datos_invalidos",
  "traceId": "ejemplo-traza-002",
  "details": [
    {
      "field": "card.last4",
      "message": "last4 deben ser exactamente 4 digitos"
    }
  ]
}

Corrige el campo indicado. La integración debe usar code para decidir y mostrar error al operador; el texto puede cambiar.

409 · Reutilizar la clave con otro cuerpo

{
  "error": "Esa Idempotency-Key ya se uso con un cuerpo diferente. Usa una llave nueva.",
  "code": "idempotencia_cuerpo_distinto",
  "traceId": "ejemplo-traza-003"
}

Detén el intento y concilia: posiblemente ya existe el ticket original. No generes automáticamente otra clave para el mismo cobro. Para una operación realmente nueva, genera y guarda una clave nueva.

409 · Venta con tarjeta sin autorización del switch

{
  "error": "El switch aun no ha publicado la autorizacion 3f2a9c1e-7b4d-4e8f-9a6b-1c2d3e4f5a6b; reintenta.",
  "code": "AUTHORIZATION_NOT_FOUND",
  "traceId": "ejemplo-traza-006"
}

code: "AUTHORIZATION_NOT_FOUND" es reintentable: el evento del switch puede llegar al servidor después de que la terminal pidió el voucher. Reintenta la misma solicitud (mismo cuerpo y misma clave) con espera creciente. Si persiste, revisa que transaccionId, rrn, serie e importe sean los de la autorización. No se creó ningún ticket.

429 · Demasiadas peticiones

HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
  "error": "Demasiadas peticiones. Reintenta en 30 segundos.",
  "code": "limite_excedido",
  "traceId": "ejemplo-traza-004"
}

Espera como mínimo el valor de Retry-After y reintenta con el mismo cuerpo y clave. El valor de 30 segundos es ilustrativo. En timeout o 5xx conserva el pendiente y usa espera creciente. En 401/403 revisa credenciales y detén el ciclo automático.

8. Probar con Postman

  1. Importa la colección o el contrato OpenAPI.
  2. Configura baseUrl, terminalSerial y enrollmentCode con tus datos.
  3. Envía 1 · Alta una sola vez. Copia token a una variable local secreta terminalToken; no compartas ni exportes su valor.
  4. Asigna a operationKey un UUID para la primera operación. Envía 2a · Ticket en efectivo: espera 201. El script guarda ticketId al recibir 200/201.
  5. Sin cambiar cuerpo ni clave, vuelve a enviar 2a: espera 200 y el mismo id. Esto prueba idempotencia.
  6. Para 2b · Ticket con tarjeta, cambia la clave porque es otra operación. Ajusta los datos de prueba del JSON antes de enviarlo.
  7. Abre 3 · Ver comprobante o 4 · PDF, que usan el último ticketId.

Envía las solicitudes una por una. No ejecutes toda la colección como un lote: consumirías el código de alta y mezclarías operaciones con una misma clave.

9. Ejemplo JavaScript descargable

Cliente para Node.js con fetch disponible. Lee el token desde el entorno y un JSON ya guardado, exige una clave estable, limita el tiempo de espera y genera el enlace del ticket. No realiza reintentos ni cobros automáticos.

node emit-ticket.mjs ticket-card-example.json

Usa las variables BASE_URL, TERMINAL_TOKEN y OPERATION_KEY configuradas antes. Descargar emit-ticket.mjs ↓. Para Android consulta el SDK Kotlin y aplicación de ejemplo.