Para el equipo de tecnología de una marca que vende con CloserCat y tiene su propio sistema de ventas (ERP, sistema de inventario o de pedidos). Versión del protocolo:
commerce/1.0· Especificaciones:openapi-marca.yaml(lo que expone tu sistema) yopenapi-closercat.yaml(lo que expone CloserCat) · Ejemplos:ejemplos/.
1. Qué es y cómo funciona
CloserCat atiende a tus compradoras por WhatsApp y por el chat de tu sitio, les ayuda a elegir y cierra la venta. Con este protocolo, tu sistema sigue siendo la fuente de verdad del catálogo, del stock y de los pedidos. El protocolo lo define CloserCat y es el mismo para todas las marcas. Tu sistema lo implementa una vez y queda conectado; no adaptamos el protocolo a la API de cada sistema.
El recorrido de una venta:
1. Catálogo y disponibilidad Tu sistema nos envía productos y stock (o nos deja consultarlos).
2. Datos del pedido La compradora llena su pedido. CloserCat te pide APARTAR el stock → POST /holds
3. Pago Le enviamos el link de pago. Vence a la misma hora que el apartado.
4. Pago confirmado CloserCat te CREA EL PEDIDO con el apartado y los soportes del pago → POST /orders
5. Después Tu sistema nos avisa: aceptado, despachado, entregado, factura → POST /events
Si el apartado vence sin pago, CloserCat te lo libera (POST /holds/{reference}/release) y le dice a la compradora la
verdad. Tu sistema también vence el apartado por su cuenta, como respaldo (§9).
Lo que nunca hace CloserCat: cobrar en tu nombre sin un apartado confirmado, inventar stock, guardar datos de tarjeta ni escribir directo en tu base de datos.
2. Capacidades: mínimas y opcionales
Declaras tus capacidades en GET /health. El validador (§14) verifica cada una antes de aceptarla.
| Nivel | Capacidad | Qué haces | ¿Obligatoria? |
|---|---|---|---|
| 1 · Venta con apartado | catalog.push o catalog.pull | Nos envías el catálogo (PUT /catalog) o lo exponemos y lo leemos (GET /catalog) | Sí, una de las dos |
availability.push o availability.pull | Nos envías la disponibilidad (POST /availability) o la consultamos (POST /availability/query) | Sí, una de las dos | |
holds | Apartas, consultas y liberas (POST /holds, GET /holds/{reference}, POST …/release) | Sí | |
orders.create | Creas y consultas el pedido (POST /orders, GET /orders/{reference}) | Sí | |
| 2 · Seguimiento | orders.events | Nos avisas aceptado, rechazado, despachado, entregado, cancelado | Recomendada |
orders.cancel | Cancelas un pedido por retracto o devolución (POST /orders/{reference}/cancel) | Recomendada | |
invoice.in_order | Tu sistema factura con el bloque billing del pedido | Opcional | |
invoice.events | Nos avisas la factura emitida con su PDF (invoice.issued) | Opcional | |
| 3 · Tiempo real | availability.live | Respondes consultas de stock durante la conversación en ≤ 1,5 s | Opcional |
holds.extend | Extiendes un apartado (POST /holds/{reference}/extend) | Opcional | |
holds.async | Respondes el apartado con 202 y lo confirmas por evento (≤ 60 s) | Opcional |
Con el nivel 1 completo ya puedes vender con apartado real. Si ofreces push y pull de lo mismo, preferimos el push.
3. Ambientes, conexión y llaves
- Dos ambientes por marca:
sandbox(prueba) yproduction. Cada uno es una conexión con suconnection_id, que ves en CloserCat en Tu Oferta › Tu sistema de ventas. - Llaves: CloserCat genera el secreto (32 bytes aleatorios o más) y te lo muestra una sola vez. Su
identificador va en
X-CloserCat-Key-Id:ck_test_…en sandbox,ck_live_…en producción. - Rotación sin corte: cada conexión admite dos llaves activas al mismo tiempo. Creas la nueva, la cargas en tu
sistema, verificas con
GET /pingy revocas la vieja. - La misma llave firma en las dos direcciones: tus llamadas a CloserCat y las de CloserCat a tu sistema.
- Tu URL base (
base_url): solohttps, resuelta a una IP pública, sin credenciales en la URL y sin redirecciones. Si no cumple, la conexión no se guarda. - IPs de salida de CloserCat: las publicamos en la pantalla de la conexión para que puedas filtrar. Confírmalas ahí antes de cerrar tu firewall; la firma es la protección obligatoria y el filtro por IP es adicional.
4. Cabeceras comunes
Todas las llamadas, en las dos direcciones:
Content-Type: application/json; charset=utf-8
X-CloserCat-Protocol: commerce/1.0
X-CloserCat-Key-Id: ck_live_7f3a
X-CloserCat-Timestamp: 1792540800 # segundos Unix, UTC
X-CloserCat-Signature: v1=<hex HMAC-SHA256(secreto, "{timestamp}.{cuerpo_crudo}")>
Idempotency-Key: <reference | batch_id:part | seq | event_id> # en todo POST y PUT
Montos y cantidades decimales siempre como cadenas ("189900", "12.50"), nunca como número de punto flotante.
Fechas en ISO-8601 con zona horaria (2026-10-21T15:40:00-05:00).
5. Firma HMAC
Qué se firma: la cadena "{X-CloserCat-Timestamp}.{cuerpo crudo}", con el secreto de la llave, usando
HMAC-SHA256. El resultado va en hexadecimal minúscula con el prefijo v1=. En un GET el cuerpo crudo es la cadena
vacía: se firma "{timestamp}.".
Cómo se verifica:
- Busca la llave por
X-CloserCat-Key-Id. Si no existe o está revocada →401. - Si
|ahora − timestamp| > 300segundos →401. - Calcula la firma sobre los bytes exactos que recibiste (no sobre el JSON re-serializado).
- Compara con comparación de tiempo constante. Si no coincide →
401.
Python
import hashlib, hmac, json, time
def firmar(secreto: str, cuerpo: bytes, ts: int | None = None) -> dict:
ts = ts or int(time.time())
mac = hmac.new(secreto.encode(), f"{ts}.".encode() + cuerpo, hashlib.sha256).hexdigest()
return {"X-CloserCat-Timestamp": str(ts), "X-CloserCat-Signature": f"v1={mac}"}
def verificar(secreto: str, cabeceras: dict, cuerpo: bytes, tolerancia: int = 300) -> bool:
try:
ts = int(cabeceras["X-CloserCat-Timestamp"])
except (KeyError, ValueError):
return False
if abs(time.time() - ts) > tolerancia:
return False
esperada = "v1=" + hmac.new(secreto.encode(), f"{ts}.".encode() + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, cabeceras.get("X-CloserCat-Signature", ""))
# Enviar: serializa UNA vez y firma esos mismos bytes.
cuerpo = json.dumps({"seq": 1842}, separators=(",", ":"), ensure_ascii=False).encode()
cabeceras = firmar("tu-secreto", cuerpo)
Node.js
const crypto = require('node:crypto')
function firmar(secreto, cuerpo /* Buffer */, ts = Math.floor(Date.now() / 1000)) {
const mac = crypto.createHmac('sha256', secreto).update(`${ts}.`).update(cuerpo).digest('hex')
return { 'X-CloserCat-Timestamp': String(ts), 'X-CloserCat-Signature': `v1=${mac}` }
}
function verificar(secreto, cabeceras, cuerpo /* Buffer crudo */, tolerancia = 300) {
const ts = Number.parseInt(cabeceras['x-closercat-timestamp'], 10)
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > tolerancia) return false
const esperada = Buffer.from('v1=' + crypto.createHmac('sha256', secreto).update(`${ts}.`).update(cuerpo).digest('hex'))
const recibida = Buffer.from(String(cabeceras['x-closercat-signature'] || ''))
return esperada.length === recibida.length && crypto.timingSafeEqual(esperada, recibida)
}
En Express, lee el cuerpo crudo (
express.raw({ type: 'application/json' })) antes de parsearlo; si lo parseas primero, la firma no coincide.
6. Idempotencia
Cada operación que crea o cambia algo tiene una llave estable. Repetirla con el mismo cuerpo devuelve el mismo
resultado; repetirla con otro cuerpo devuelve 409 idempotency_key_reused.
| Operación | Llave | Regla |
|---|---|---|
POST /holds | reference (hold_{número de pedido}) | CloserCat consulta GET /holds/{reference} antes de reintentar |
POST /holds/{reference}/release | la reference | Liberar dos veces = 200. Desconocida = 404, que tomamos como liberada |
POST /orders | reference (número de pedido) | CloserCat consulta GET /orders/{reference} antes de crear |
PUT /catalog | {batch_id}:{part} | La misma parte no se aplica dos veces |
POST /availability | seq | Debe crecer por conexión. Un seq menor o igual al último se ignora (applied: false) |
POST /events | event_id | Un evento repetido es un no-op (204) |
La llave también viaja en la cabecera Idempotency-Key.
7. Tiempos y cortes
| Operación | Quién llama | Tiempo exigido (p95) | Corte | Reintentos |
|---|---|---|---|---|
POST /holds | CloserCat, al enviar la compradora sus datos | ≤ 3 s | 4 s | 1 en el momento; después, en segundo plano hasta 10 min (§11) |
POST /holds/{reference}/release | CloserCat (vence o se cancela) | ≤ 3 s | 5 s | Cola durable hasta 24 h |
POST /orders | CloserCat, al confirmarse el pago | ≤ 10 s, o 202 + evento | 15 s | Cola durable: 1 min, 5 min, 15 min, 1 h, 6 h, 24 h |
POST /availability/query (availability.live) | CloserCat, en la conversación | ≤ 1,5 s | 1,5 s | Ninguno: usamos el último dato |
PUT /catalog, POST /availability | Tu sistema | Respondemos 202 en ≤ 2 s | — | Reintenta con la misma llave |
POST /events | Tu sistema | Respondemos 204 en ≤ 1 s | — | Reintenta hasta recibir 2xx |
Límites de CloserCat: 120 solicitudes por minuto por conexión; catálogo hasta 5 MB y 1000 productos por parte;
disponibilidad y eventos hasta 256 KB. Ante 429, respeta Retry-After.
8. Errores
Todos los errores usan el mismo cuerpo:
{"protocol_version": "commerce/1.0", "reference": "CC-1042",
"error": {"code": "insufficient_stock", "message": "…", "fields": [], "lines": [{"sku": "…", "requested": 2, "available": 0}]}}
La lista de códigos es cerrada:
| HTTP | error.code | Cuándo | ¿Reintenta CloserCat? |
|---|---|---|---|
| 400 / 422 | validation_error | El cuerpo no cumple el esquema o una regla de negocio. Indica los campos en fields | No |
| 401 | unauthorized | Firma inválida, llave desconocida o timestamp fuera de 300 s | No (avisamos a la marca) |
| 404 | not_found | El apartado o pedido no existe | — |
| 409 | insufficient_stock | No hay stock para una o más líneas. Detalle en lines | No |
| 409 | hold_expired_no_stock | Al crear el pedido, el apartado venció y ya no hay stock | No (pasa a una persona) |
| 409 | idempotency_key_reused | La misma llave con otro cuerpo | No (avisamos: es un error de integración) |
| 429 | rate_limited | Demasiadas solicitudes | Sí, respetando Retry-After |
| 5xx, corte, error de red | unavailable | Tu sistema no puede responder | Sí (§7) |
message es para el equipo técnico; nunca se le muestra a la compradora.
9. Qué pasa si tu sistema no responde: garantías para tu marca
| Momento | Qué hace CloserCat | Qué le dice a la compradora |
|---|---|---|
| No llega catálogo o disponibilidad a tiempo | Sigue con el último dato. No dice «quedan N» si el dato tiene más de 15 minutos. Tu equipo ve «Datos de hace…» en CloserCat | Nada distinto |
| El apartado falla o se pasa del tiempo | No envía el link de pago. Reintenta en segundo plano hasta 10 minutos | «Estoy confirmando con la bodega que tu producto esté disponible. Te escribo en unos minutos con el link de pago.» Si no se confirma, una persona de tu equipo continúa la conversación |
| El apartado es parcial o rechazado | Ajusta el pedido solo si la compradora acepta | «De la talla 38 en negro queda 1. ¿Te la aparto así o prefieres ver otro color?» |
| Crear el pedido falla después del pago | El pago ya está y el pedido queda confirmado en CloserCat. Su envío a tu sistema queda en cola durable 24 h y tu equipo recibe un aviso | «¡Pago recibido! Tu pedido está confirmado.» |
| Tu sistema rechaza el pedido porque el apartado venció y no hay stock | Pasa a una persona de tu equipo, que gestiona el cambio o la devolución | «Tu pago llegó cuando ya no quedaba tu producto. Una persona del equipo te escribe ahora.» |
Defensa en profundidad: tu sistema vence cada apartado por su cuenta en expires_at + 10 minutos, aunque no le
llegue la liberación. Así el stock nunca queda colgado.
10. Catálogo
Un producto tiene una o más variantes. El SKU de cada variante es la llave de todo el protocolo: debe ser estable y único en tu cuenta. Por ejemplo: referencia + color + talla.
{"external_id": "REF-1042", "name": "Sandalia plataforma Lía", "active": true,
"category_path": ["Mujer", "Sandalias"], "attributes": {"material": "Cuero"},
"images": ["https://cdn.ejemplo.com/ref-1042-negro-1.jpg"],
"variants": [{"sku": "REF-1042-NEG-37", "attributes": {"color": "Negro", "talla": "37"},
"price": {"amount": "189900", "currency": "COP"}, "compare_at": null, "barcode": null, "active": true}]}
Ejemplo completo en ejemplos/catalog-batch.json.
Push:
- Envía lotes con
batch_id, en partes de hasta 1000 productos. mode: fulles una foto completa: lo que no venga queda archivado, nunca se borra.mode: deltaenvía solo cambios.- Te respondemos
202con lo rechazado por ítem, y consultas el resultado enGET /catalog/batches/{batch_id}.
Pull: expones GET /catalog paginado con cursor y updated_since.
Quién manda en cada campo:
| Campo | Autoridad |
|---|---|
sku, price, compare_at, active, disponibilidad | Tu sistema. CloserCat nunca los cambia |
name, category_path | Tu sistema, salvo que tu equipo los edite a mano en CloserCat (la edición manual se respeta) |
| descripción, fotos, beneficios y atributos que tu sistema no envía | CloserCat (tu equipo los completa o los enriquece). Si los envías, se usan como punto de partida |
Atributos que CloserCat no reconoce (por ejemplo, un nuevo eje de talla) no se descartan: quedan para que tu equipo confirme su rol en CloserCat.
11. Disponibilidad
available = unidades vendibles en línea, ya descontados tus propios apartados. Si no envías warehouse, es el
total de las bodegas que despachan pedidos en línea.
Push (recomendado):
- Envía cada cambio en ≤ 60 s (
mode: delta) y una foto completa diaria (mode: full). seqdebe crecer de forma estricta por conexión.as_ofes el momento del dato en tu sistema.
Pull: respondes POST /availability/query con lotes de hasta 500 SKUs.
Cómo usa CloserCat el dato:
| Momento | Fuente |
|---|---|
| Buscar, mostrar opciones, filtrar por talla | El último dato que nos enviaste |
| Decir «quedan N» | Solo si N ≤ 3 y el dato tiene ≤ 15 minutos |
| Al apartar | Tu respuesta a POST /holds. Es la verificación real |
Para no vender dos veces entre un envío y el siguiente, CloserCat resta del último dato los apartados que te pidió después de ese momento.
12. Apartados
- La compradora envía los datos de su pedido y CloserCat llama
POST /holdscon:reference(hold_{número de pedido});- las líneas;
- el
expires_atque pide: 30 minutos con link de pago, 2 horas con cierre por una persona o transferencia, y hasta 24 horas con contra entrega. Cada marca los ajusta.
- Respondes:
201 held(todo apartado);201 partial(lines[].helddice cuánto);409 insufficient_stock, con el detalle por línea.- Puedes devolver un
expires_atmenor al pedido: CloserCat le dice a la compradora la hora efectiva.
- Con
holds.asyncpuedes responder202y confirmar conhold.confirmed/hold.rejecteden ≤ 60 s. - Después del pago, el apartado se confirma al crear el pedido (
hold_reference). - Si nadie paga, CloserCat llama
POST /holds/{reference}/releaseconreason: expired. - Con
holds.extend, CloserCat puede pedir más tiempo cuando el pago queda en verificación del banco. - Tu sistema vence el apartado por su cuenta en
expires_at+ 10 minutos.
requested ─► held ─► committed (pedido creado)
│ └─► released (cancelado)
└────► expired ─► released
─► partial ─► held | released
─► rejected
13. Pedidos, pagos y factura
CloserCat crea el pedido solo con el pago confirmado: pasarela capturada, contra entrega aceptada o pago confirmado por una persona del equipo. El pedido incluye:
reference(número de pedido) yhold_reference;buyer: persona natural o empresa, nombre, teléfono. El documento solo si tu sistema lo exige o si la compradora pidió factura;shipping: modo, dirección, quien recibe o recoge, valor del envío;linesytotals, con montos como cadenas;payment, con los soportes:- pasarela e id conciliable de la transacción;
- estado (
captured,pending_on_deliveryoconfirmed_by_staff); - medio y cuotas;
- monto, moneda y fecha de captura;
proof_url: el comprobante que subió la compradora en una transferencia, con URL firmada por 7 días.- Nunca datos de tarjeta.
billing:null= la compradora no pidió factura a su nombre (tu sistema emite su documento según su política);- con datos = sí la pidió: persona o empresa, documento, nombre o razón social y correo.
- Esos datos los llenó una sola vez en su pedido; nunca se le piden dos veces.
closercat.is_test:trueen pruebas. No es una venta real.
Respondes 201 con tu erp_order_id, o 202 y nos avisas con order.accepted / order.rejected. Ejemplo completo
en ejemplos/order-request.json.
Factura, tres modos (los eliges en CloserCat):
| Modo | Quién factura | Qué recibes |
|---|---|---|
erp | Tu sistema | El bloque billing en el pedido. Si declaras invoice.events, nos avisas invoice.issued con el PDF y CloserCat se lo envía a la compradora solo si pidió factura |
connector | Tu proveedor de facturación electrónica, conectado aparte en CloserCat | Nada adicional en este protocolo |
none | — | — |
14. Eventos
POST /events con event_id, type, occurred_at y data. Tipos:
type | Capacidad | data |
|---|---|---|
hold.confirmed, hold.rejected, hold.released | holds.async (o liberaciones hechas por tu sistema) | reference, erp_hold_id, expires_at, lines, reason |
order.accepted, order.rejected, order.shipped, order.delivered, order.cancelled | orders.events | reference, erp_order_id, reason, transportadora y guía |
invoice.issued | invoice.events | reference, number, issued_at, cufe, pdf_url (temporal, mínimo 24 h) |
reason va en una frase y sin datos personales.
15. Simulador
En sandbox, mientras tu sistema no esté listo, la conexión puede quedar en simulación: CloserCat responde por tu
sistema con un catálogo de prueba. Así ves el recorrido completo en la conversación antes de escribir una línea. Cuando
cargas tu base_url de sandbox, CloserCat empieza a llamarte a ti.
SKUs de prueba (en simulación y en el validador):
| SKU | Comportamiento esperado |
|---|---|
TEST-OK-* | Hay stock: el apartado y el pedido se crean |
TEST-AGOTADO | 409 insufficient_stock con available: 0 |
TEST-PARCIAL | Solo hay 1 unidad: una solicitud de 2 queda partial |
TEST-LENTO | Responde en 6 s: CloserCat corta a los 4 s y aplica §9 |
TEST-ERROR | 503 unavailable |
Tu sandbox debe reconocer estos SKUs para pasar el validador.
16. Validador
En Tu Oferta › Tu sistema de ventas › Probar mi integración, CloserCat corre esta suite contra tu sandbox y te muestra el resultado por escenario. Puedes volver a correrla cuantas veces quieras.
| # | Qué verifica |
|---|---|
| V1 | GET /health firmado responde ok, commerce/1.0 y las capacidades |
| V2 | Rechazas con 401 una firma mala y un timestamp de más de 300 s |
| V3 | POST /holds de TEST-OK-1 responde 201 held con expires_at |
| V4 | POST /holds de TEST-AGOTADO responde 409 insufficient_stock con lines |
| V5 | Repetir el mismo apartado devuelve el mismo resultado; otro cuerpo con la misma llave da 409 idempotency_key_reused |
| V6 | Liberar dos veces responde 200; liberar una desconocida responde 404 |
| V7 | POST /orders con un apartado vigente responde 201 (o 202 + evento) con erp_order_id |
| V8 | GET /orders/{reference} antes de crear da 404; repetir el pedido no lo duplica |
| V9 | Un pedido con apartado vencido de TEST-AGOTADO responde 409 hold_expired_no_stock |
| V10 | Tiempos p95 dentro de §7 en 20 llamadas |
| V11 | Si empujas: un lote de catálogo y uno de disponibilidad válidos se aplican, y uno inválido devuelve el rechazo por ítem |
| V12 | Un evento con event_id repetido se acepta una sola vez |
V1–V9 son obligatorios. V10–V12 aplican según tus capacidades.
17. Lista de salida a producción
GET /healthfirmado, con capacidades.- Firma verificada (V2).
- Idempotencia por
reference,batch_id:part,seqyevent_id. - Vencimiento propio del apartado (
expires_at+ 10 minutos). 409 insufficient_stockcon detalle por línea.GET /orders/{reference}consultable antes de crear.- Tiempos p95 de §7.
- Sandbox con los SKUs de prueba.
- Validador V1–V9 en verde.
- Contacto técnico y ventanas de mantenimiento informados en CloserCat.
- Un pedido real de bajo valor, aprobado por tu equipo.
Con todo esto, tu equipo pasa la conexión a Producción en CloserCat.
18. Versionado y compatibilidad
- La versión va en
X-CloserCat-Protocoly enprotocol_version:commerce/1.0. - Cambios menores (1.x): siempre aditivos (campos opcionales, capacidades o tipos de evento nuevos). Tu implementación debe ignorar los campos que no conoce.
- Cambios mayores (2.0): conviven al menos 6 meses con la versión anterior, anunciados con anticipación.
- Si recibes una versión que no soportas, responde
422 validation_errorcon el mensaje «versión no soportada».
19. Privacidad y seguridad
- Qué datos personales viajan y por qué:
- el nombre, el teléfono y la dirección de la compradora, para que tu sistema registre y despache su pedido;
- su documento, solo si tu sistema lo exige o si pidió factura a su nombre;
- su correo, solo para la factura.
- Tu marca es la responsable de esos datos (son tus clientes); CloserCat los trata por tu cuenta.
- Nunca viajan datos de tarjeta. Los pagos los procesa la pasarela.
- No registres documentos, correos ni teléfonos en logs. Registra la
referencey el código de error. - Las URLs de comprobantes y facturas son temporales y firmadas. No las publiques.
- Guarda los secretos de las llaves en un gestor de secretos, nunca en el código ni en el repositorio.
- Si sospechas que una llave se filtró, créala de nuevo y revoca la anterior en CloserCat de inmediato.
