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.
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.jsonGuarda 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.jsonLa 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.
| Campos | Cómo usarlos |
|---|---|
| id / folio | id identifica el recurso y permite construir la URL; folio es el número visible del comprobante. |
| base_amount / tip_amount / amount | Cadenas decimales: 170.00 + 17.00 = 187.00. No confundir qty y price numéricos de entrada con las cadenas de salida. |
| receipt_svg | Representación del comprobante. Renderizar según el dispositivo; no es un comando de impresión. |
| created_at / expires_at | Fecha 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_id | Hora que reportó la terminal y correlación de soporte. |
| merchant_id / terminal_serial / terminal_merchant_name / views_count / last_viewed_at / view_dates | Campos del listado/panel. Pueden estar en null en la respuesta de emisión; conserva tu serie configurada. |
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.pdfNo 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
- Importa la colección o el contrato OpenAPI.
- Configura
baseUrl,terminalSerialyenrollmentCodecon tus datos. - Envía 1 · Alta una sola vez. Copia
tokena una variable local secretaterminalToken; no compartas ni exportes su valor. - Asigna a
operationKeyun UUID para la primera operación. Envía 2a · Ticket en efectivo: espera 201. El script guardaticketIdal recibir 200/201. - Sin cambiar cuerpo ni clave, vuelve a enviar 2a: espera 200 y el mismo id. Esto prueba idempotencia.
- Para 2b · Ticket con tarjeta, cambia la clave porque es otra operación. Ajusta los datos de prueba del JSON antes de enviarlo.
- 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.jsonUsa 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.