Saltar al contenido principal

Comunicación con punto de venta

Tu terminal habla con SPIDI en dos fases: pairing (una sola vez) y luego operación firmada con HMAC (en cada petición).

Comunicación con punto de venta: pairing y firma HMAC

Fase 1 — Pairing (vinculación)

Ocurre una sola vez en la vida del dispositivo, y son dos llamadas.

1) El admin registra el serial y genera un OTP. Con tu token:

curl -X POST https://dev.api.spuntodeventa.com/api/v1/merchants/MERCHANT-001/pos-terminals/98202003219630/pairing \
-H "Authorization: Bearer <tu-token>" \
-H "Content-Type: application/json" \
-d '{ "serial": "98202003219630" }'

Sí, el serial va dos veces: en la ruta y en el cuerpo. Así lo exige el contrato. La respuesta trae el código que el técnico va a teclear en el aparato:

{ "data": { "code": "889904" } }

2) El POS canjea el OTP y recibe su Secret Key. Esta llamada es pública —el terminal todavía no tiene con qué firmar:

curl -X POST https://dev.api.spuntodeventa.com/api/v1/pos-terminals/98202003219630/pairing/activate \
-H "Content-Type: application/json" \
-d '{ "serial": "98202003219630", "code": "889904" }'
{ "data": { "secret_key": "sk_live_…" } }

El servidor valida el código, genera la Secret Key y la asocia al serial; el POS la guarda en memoria segura. A partir de aquí, la Secret Key nunca vuelve a viajar por internet.

Fase 2 — Firma HMAC en cada petición

Ya vinculado, toda petición del POS se firma con HMAC-SHA256 a partir de cuatro elementos:

#ElementoDetalle
1BodyEl JSON del cuerpo, tal cual se envía. En GET o sin cuerpo, cadena vacía ""
2TimestampISO 8601 en UTC, p. ej. 2026-07-29T14:03:11Z. El mismo que envías en la cabecera
3URLLa URL completa, con esquema y host, incluyendo el query string
4Secret KeyLa clave persistida en el pairing
message = body + "\n" + timestamp + "\n" + url
signature = HMAC_SHA256(message, secret_key) → hexadecimal

Cabeceras obligatorias en toda petición del terminal:

CabeceraValor
x-pos-signatureLa firma HMAC-SHA256 en hexadecimal
x-pos-timestampEl mismo timestamp ISO 8601 usado en la firma

Vector de prueba

Antes de pelearte con el servidor, comprueba que tu implementación arma la cadena igual que esta doc. Con esta clave de juguete:

secret_key = sk_test_3f8a1c9e5b2d4a6f8e0c1b3d5f7a9c2e

Un GET (cuerpo vacío, así que el mensaje empieza por un salto de línea):

timestamp = 2026-07-29T14:03:11Z
url = https://dev.api.spuntodeventa.com/api/v1/pos-terminals/98202003219630/transactions?status=PENDING
body = ""

x-pos-signature = 8469bfcc3e38bae7c5cfe57d8de0830d58720ee9cc8198e5fc4639236b33c8bf

Un POST con cuerpo:

timestamp = 2026-07-29T14:05:02Z
url = https://dev.api.spuntodeventa.com/api/v1/pos-terminals/98202003219630/transactions/ORD-123456/commit
body = {"authorizationCode":"171599","processCode":"002000","commitAt":"2026-07-29T14:05:00Z","amountReference":10.5}

x-pos-signature = 03ff4aabc6098b6917b864300dda7b3a1dc91f5ee68f57b900c06a16c6950759

Si te salen esos dos hexadecimales, tu ensamblado del mensaje y tu codificación coinciden con las de aquí. Ojo con el cuerpo: se firma exactamente la cadena que mandas, así que serialízalo una sola vez y firma esa misma variable — no vuelvas a serializar el objeto para enviarlo, o un espacio de diferencia te tumba la firma.

En Node:

const crypto = require("crypto");

function firmar(body, timestamp, url, secretKey) {
const message = `${body}\n${timestamp}\n${url}`;
return crypto.createHmac("sha256", secretKey).update(message).digest("hex");
}

Operaciones del terminal

Casi todas van firmadas, pero no todas: la consulta de un cierre concreto pide token, no firma.

OperaciónMétodoEndpointBodyAutenticación
Listar pagosGET/api/v1/pos-terminals/{serialNumber}/transactions?status=PENDING""firma
Consultar una ordenGET/api/v1/pos-terminals/{serialNumber}/transactions/{orderId}""firma
Confirmar pagoPOST/api/v1/pos-terminals/{serialNumber}/transactions/{orderId}/commitJSONfirma
Confirmar anulaciónPOST/api/v1/pos-terminals/{serialNumber}/transactions/{orderId}/cancellation/commit""firma
Crear cierre de lotePOST/api/v1/pos-terminals/{serialNumber}/settlementsJSONfirma
Listar cierresGET/api/v1/pos-terminals/{serialNumber}/settlements""firma
Consultar un cierreGET/api/v1/pos-terminals/{serialNumber}/settlements/{batchNumber}""Bearer
Dos desajustes del contrato que verás en la referencia

El GET de pagos aparece con cuerpo obligatorio. No lo tiene: el servidor de sandbox responde a esa ruta sin cuerpo, y en un GET se firma cadena vacía. Es una errata del OpenAPI.

La cabecera x-pos-timestamp no está declarada. El contrato solo describe x-pos-signature, pero el timestamp es obligatorio: sin él la firma no se puede verificar. Envíalo siempre.

Las dos están reportadas a SPIDI.

Por qué es seguro

  • Sin contraseñas en el dispositivo: nada que anotar en un papel.
  • Integridad: si alguien altera el monto, la URL o el timestamp, la firma falla y el servidor rechaza.
  • Anti-replay: el x-pos-timestamp permite descartar peticiones viejas o duplicadas.
  • Aislamiento: la llave del terminal no sirve para tocar tu cuenta, y tu token no sirve para firmar como el terminal. Robar el aparato no te compromete el comercio.
Si se pierde un terminal

La API todavía no expone una operación para desvincular un POS o revocar su Secret Key. Si necesitas dar de baja un aparato, escríbele a SPIDI. Está pedido como capacidad pendiente.

Recibir una venta · Conceptos (Merchant)