Punto de venta
Conectar tu sistema de cobro para que cada compra sume en la tarjeta del cliente
El caso es siempre el mismo: se cierra una venta en tu sistema de cobro, el cliente está identificado, y esa compra tiene que sumar en su tarjeta de fidelidad sin que nadie toque un botón extra. Esta página es cómo se hace.
La API está disponible únicamente en el plan Business. Si no ves tu clave en Configuración, revisá primero el plan.
Las dos vías
Dardo expone dos caminos distintos para acreditar una compra desde un sistema externo. No son intercambiables: usan autenticación diferente, modelos de datos diferentes y unidades diferentes.
| Vía A — Punto de venta | Vía B — API estándar | |
|---|---|---|
| Endpoints | POST /api/v2/marketplace/resolve-credentialsPOST /api/v2/marketplace/accruePOST /api/v2/marketplace/reverse | GET /api/v2/cardsPOST /api/v2/cards/{id}/add-... |
| Autenticación | header X-App-Token | header X-API-Key |
| Identificación del cliente | La resuelve el servidor: mandás teléfono, mail o número de serie y busca o registra la tarjeta | La hacés vos: buscás la tarjeta y después operás sobre su id |
| Datos del ticket | Objeto Check completo: ítems, impuestos, propina, servicio, horarios | Un monto, o una cantidad de puntos/sellos/visitas |
| Reglas de acumulación | Las aplica Dardo del lado del servidor, incluso por producto | Las calculás vos antes de llamar |
| Revertir una operación | Sí, POST /marketplace/reverse con el transactionId | No hay endpoint de reversa: compensás con el subtract-... equivalente |
| Unidad de los montos | Centavos (integer) | Decimal (float) |
| Errores declarados | 200, 400, 403 | Solo 200 |
Vía A — el grupo "Punto de venta"
Es la vía diseñada explícitamente para esto. La descripción de accrue lo dice: realiza una acción de acumulación sobre una compra; se requiere el teléfono, el mail o el número de serie de la tarjeta del cliente para encontrar o registrar una tarjeta de fidelidad. Y el objeto Check que recibe tiene campos que solo tienen sentido en un salón: propina, servicio, hora de apertura y de cierre de la mesa.
La Vía A no usa X-API-Key. Las tres operaciones declaran security: [] en la especificación —anulan explícitamente el esquema de autenticación global— y en su lugar exigen un header X-App-Token (App static authorization token, obligatorio).
Si mandás la clave API de tu comercio a estos endpoints, te va a rebotar con 403. Es el primer error que comete todo el mundo.
POST /api/v2/marketplace/resolve-credentials
Dado cualquiera de los credenciales de una instalación, te devuelve el paquete completo. El caso de uso que da la especificación: en el webhook de un servicio, o a nivel de la aplicación, puede que solo tengas el identificador del comercio y necesites obtener el token de la API.
| Campo | Tipo | Obligatorio | Qué es |
|---|---|---|---|
names | string[] | Sí | Nombres de credenciales a devolver. Su propia descripción aclara que si no se especifican, se devuelven todas las del comercio. |
credentials | {name, value}[] | No | Credenciales del comercio que ya tenés en mano. Si lo mandás, al menos un elemento. |
curl -X POST 'https://api.dardo.ai/api/v2/marketplace/resolve-credentials' \
-H 'X-App-Token: EL_TOKEN_DE_TU_APP' \
-H 'Content-Type: application/json' \
-d '{
"names": ["apiToken", "companyId"],
"credentials": [
{ "name": "merchantId", "value": "resto-centro-001" }
]
}'Responde con credentials, el mismo formato de pares name / value.
POST /api/v2/marketplace/accrue
El endpoint central de la vía.
| Campo | Tipo | Obligatorio | Qué es |
|---|---|---|---|
check | Check | Sí | El ticket. Desglose completo abajo. |
transactionId | string | Sí | Identificador único de la transacción. Es la llave con la que después vas a poder revertirla. |
phone | string | No* | Teléfono del cliente. |
email | string | No* | Mail del cliente. |
serialNumber | string | No* | Número de serie de la tarjeta del cliente. |
firstName | string | No | Nombre del cliente. |
lastName | string | No | Apellido del cliente. |
credentials | {name, value}[] | No | Credenciales del comercio sobre el que operás. |
* Ninguno de los tres figura como obligatorio en el esquema, pero la descripción de la operación dice que uno de los tres es necesario para encontrar o registrar la tarjeta. La validación la hace el servidor.
El objeto Check
Los montos del Check van en CENTAVOS
La especificación es literal: amount es el monto total del ticket en centavos, con tipo integer. Lo mismo para taxAmount, discountAmount, tipAmount, serviceAmount y Selection.price — todos enteros, todos en centavos.
Un ticket de $12.500,50 se manda como 1250050, no como 12500.5.
Si tu sistema de cobro te entrega el total como decimal y lo mandás tal cual, estás acreditando 100 veces menos. Si convertís dos veces, acreditás 100 veces más. Y es silencioso: la API responde 200 en los dos casos, porque 1250050 y 12500 son enteros igual de válidos.
Regla práctica: una sola función aCentavos() en el adaptador, con un test unitario, y nunca multiplicar por 100 en dos lugares distintos.
Y ojo con el contraste: en la Vía B, el campo amount de add-purchase es un float en unidades monetarias normales. No son centavos. Dos campos que se llaman igual y usan unidades distintas según la vía. Mezclarlos es el segundo error garantizado.
| Campo | Tipo | Obligatorio | Unidad | Qué es |
|---|---|---|---|---|
amount | integer | Sí | centavos | Monto total del ticket. |
currency | string | Sí | — | Moneda en código ISO 4217 de 3 letras, por ejemplo ARS. |
selections | Selection[] | Sí | — | Los ítems del ticket. |
externalTransactionId | string | No | — | El identificador de la transacción en tu sistema de cobro. |
checkOpenedAt | string | No | ISO 8601 | Cuándo se abrió la mesa o el ticket. |
checkClosedAt | string | No | ISO 8601 | Cuándo se cerró. |
taxAmount | integer | No | centavos | Impuestos. |
discountAmount | integer | No | centavos | Descuentos. |
tipAmount | integer | No | centavos | Propina. |
serviceAmount | integer | No | centavos | Servicio de mesa. |
totalTipReceived | string | No | — | Propina total recibida. Se guarda como dato crudo. |
orderUpdatedAt | string | No | — | Fecha de actualización del pedido. Se guarda como dato crudo. |
Los dos últimos son campos de paso: la especificación aclara que se almacenan como dato crudo. No los uses para calcular nada.
Cada elemento de selections es un Selection:
| Campo | Tipo | Obligatorio | Qué es |
|---|---|---|---|
quantity | integer | Sí | Cantidad de unidades. Tiene que ser mayor que cero. |
totalPrice | integer | Sí | Precio total de la línea. |
id | string | No | Identificador único del ítem de inventario. Necesario para que funcione una regla de acumulación por producto. |
groupId | string | No | Grupo del ítem. Necesario para reglas por familia de productos. |
displayName | string | No | Nombre visible del ítem. |
price | integer | No | Precio unitario en centavos. |
id y groupId son lo que habilita reglas del tipo "esta promoción suma doble" o "el delivery no acumula". Sin ellos, el ticket entero se trata como un monto plano.
curl -X POST 'https://api.dardo.ai/api/v2/marketplace/accrue' \
-H 'X-App-Token: EL_TOKEN_DE_TU_APP' \
-H 'Content-Type: application/json' \
-d '{
"transactionId": "pos-2026-09-07-004512",
"phone": "+5491168421793",
"firstName": "Marina",
"lastName": "Ferreyra",
"credentials": [
{ "name": "merchantId", "value": "resto-centro-001" }
],
"check": {
"amount": 1875000,
"currency": "ARS",
"taxAmount": 325413,
"tipAmount": 187500,
"discountAmount": 0,
"serviceAmount": 0,
"externalTransactionId": "TCK-889231",
"checkOpenedAt": "2026-09-07T21:14:02-03:00",
"checkClosedAt": "2026-09-07T22:47:38-03:00",
"selections": [
{
"id": "SKU-MILA-NAP",
"groupId": "GRP-PRINCIPALES",
"displayName": "Milanesa napolitana",
"price": 745000,
"quantity": 2,
"totalPrice": 1490000
},
{
"id": "SKU-MALBEC-750",
"groupId": "GRP-BEBIDAS",
"displayName": "Malbec 750ml",
"price": 385000,
"quantity": 1,
"totalPrice": 385000
}
]
}
}'Ese ticket es de $18.750,00 con $1.875,00 de propina. Mirá los ceros: eso es lo que significa "centavos".
La respuesta trae el transactionId que mandaste y un array results, con un resultado por cada tarjeta afectada. Cada uno incluye isSuccess, errorMessage, serialNumber, checkAmount, accrualAmount, accruedValue, operationId, createdAt y reversedAt.
Un 200 no significa que se acreditó. results es un array y isSuccess es por elemento: podés recibir 200 con results: [{ "isSuccess": false, "errorMessage": "..." }]. Tu sistema tiene que recorrer results y chequear cada isSuccess.
Prestá atención también a accruedValue: la especificación dice explícitamente que puede ser 0 en determinados casos, típicamente cuando ninguna regla de acumulación aplica a ese ticket.
POST /api/v2/marketplace/reverse
Revierte una acumulación previa usando su transactionId. Es lo que usás cuando se anula un ticket ya cerrado.
| Campo | Tipo | Obligatorio | Qué es |
|---|---|---|---|
transactionId | string | Sí | El mismo que mandaste en accrue. |
credentials | {name, value}[] | No | Credenciales del comercio. |
curl -X POST 'https://api.dardo.ai/api/v2/marketplace/reverse' \
-H 'X-App-Token: EL_TOKEN_DE_TU_APP' \
-H 'Content-Type: application/json' \
-d '{
"transactionId": "pos-2026-09-07-004512",
"credentials": [
{ "name": "merchantId", "value": "resto-centro-001" }
]
}'Devuelve la misma estructura que accrue, con reversedAt cargado en los results.
Errores de la Vía A
Es el único grupo de la API que declara respuestas de error además del 200:
| Código | Significado | Cuerpo de ejemplo |
|---|---|---|
400 | Pedido mal formado | {"errors": ["Could not decode request body."]} |
400 | Falta un campo obligatorio | {"errors": ["Credentials should not be blank"]} |
403 | Falta el header | {"errors": ["Missed header X-App-Token"]} |
403 | El token no sirve | {"errors": ["Authentication failed"]} |
El formato es errors, un array de textos. No es un objeto con código: parsealo en consecuencia.
El cuerpo se valida antes que la credencial. Si mandás un pedido con el cuerpo incompleto, recibís 400 aunque tu X-App-Token también esté mal. El 403 recién aparece cuando el cuerpo pasa la validación.
Al depurar, resolvé primero lo que reporta el 400: hasta entonces no vas a saber si tu token funciona. Verificado contra api.dardo.ai.
Vía B — la API estándar
Es la API que ya tenés habilitada con tu clave de plan Business. No requiere ningún alta adicional. El flujo es explícito y en dos pasos: buscás la tarjeta, y operás sobre su id.
Cuál usar
Arrancá por la Vía B
Y el motivo es concreto: no está documentado cómo se emite un X-App-Token. La especificación lo exige como header obligatorio en las tres operaciones de la Vía A, pero no declara ningún endpoint, flujo ni pantalla para obtener uno. Tu comercio tiene una clave API, no un token de aplicación.
Además, con la Vía B:
- Ya tenés la credencial. Podés empezar hoy, sin ida y vuelta con nosotros.
- El control es tuyo. Tu sistema decide qué cliente, qué tarjeta y cuánto acredita. La Vía A resuelve la tarjeta sola e incluso la crea si no existe, lo cual es cómodo hasta el día que alguien tipea mal un teléfono en la caja.
- Desaparece el riesgo de los centavos. La Vía B trabaja con decimales en unidades monetarias normales. No hay conversión de 100x que puedas errar.
Lo que perdés: el endpoint de reversa, las reglas de acumulación por producto del lado del servidor, y el detalle de los ítems del ticket guardado en Dardo.
Cuándo vale migrar a la Vía A: si necesitás reglas por producto —"los postres no acumulan", "el vino suma doble"— o si las anulaciones de ticket son frecuentes. Para eso, y para conseguir un X-App-Token, escribinos a soporte@dardo.ai contando qué sistema de cobro usás.
El flujo completo, paso a paso
Escenario: se cierra una mesa con un consumo de $18.750,00. El cliente se identificó con su teléfono.
Identificar al cliente
Una sola llamada te trae todo lo que necesitás. GET /api/v2/cards filtrando por teléfono devuelve la tarjeta, su saldo, los datos del cliente y los segmentos a los que pertenece, sin tener que pasar antes por el endpoint de clientes.
Filtros disponibles: templateId, customerId, customerEmail, customerPhone, más page (por defecto 1) e itemsPerPage (por defecto 30, máximo 1000).
curl -G 'https://api.dardo.ai/api/v2/cards' \
-H 'X-API-Key: TU_CLAVE_API' \
--data-urlencode 'customerPhone=+5491168421793' \
--data-urlencode 'templateId=1187'{
"responseId": "3f1a8b04-6d27-4c9e-9a15-2b8e7c04d913",
"createdAt": "2026-09-07T22:47:39-03:00",
"code": 200,
"meta": { "totalItems": 1, "itemsPerPage": 30, "currentPage": 1 },
"data": [
{
"id": "430556-818-972",
"companyId": 1842,
"templateId": 1187,
"customerId": "9c4e1f80-5a3b-4d72-8e16-7f2a9b350c48",
"type": "reward",
"status": "active",
"customer": {
"id": "9c4e1f80-5a3b-4d72-8e16-7f2a9b350c48",
"phone": "+5491168421793",
"email": "marina.ferreyra@gmail.com",
"firstName": "Marina",
"surname": "Ferreyra",
"externalUserId": "POS-CLI-4471",
"segments": []
},
"balance": {
"balance": 340.0,
"numberRewardsUnused": 0,
"numberStampsTotal": null,
"stampsBeforeReward": null
},
"availableRewardTiers": [],
"installLink": "https://...",
"createdAt": "2026-03-14T19:02:11-03:00"
}
]
}Lo que te llevás de acá:
data[0].id→"430556-818-972". Este es el{id}de todos los endpoints de acumulación. No es elcustomerId.data[0].type→ determina qué endpoint podés llamar en el paso siguiente.data[0].balance→ el estado actual, útil para imprimir en el ticket.data[0].customer.externalUserId→ el lugar natural para guardar el identificador del cliente en tu sistema de cobro. Si ya tenés ese mapeo, podés resolver al cliente sin depender del teléfono, que la gente tipea mal.
Manejá el caso data: [], es decir cliente sin tarjeta. Ahí tenés que decidir si tu sistema emite la tarjeta —POST /api/v2/customers y después POST /api/v2/cards— o simplemente avisa "cliente no registrado".
Acreditar la compra
El endpoint depende del type de la tarjeta que te devolvió el paso anterior.
No adivines el endpoint. Cada tipo de tarjeta acumula en una unidad distinta —puntos, sellos, visitas, monto— y llamar al que no corresponde no acredita nada útil. La correspondencia completa entre tipo de tarjeta y operación está en Tipos de tarjeta.
Para el ejemplo, con una mecánica por monto gastado:
curl -X POST 'https://api.dardo.ai/api/v2/cards/430556-818-972/add-purchase' \
-H 'X-API-Key: TU_CLAVE_API' \
-H 'Content-Type: application/json' \
-d '{
"amount": 18750.00,
"comment": "Ticket TCK-889231 - mesa 12 - sucursal Centro"
}'La respuesta es un 200 con la tarjeta completa y el saldo ya actualizado:
{
"responseId": "b72d5e91-0c48-4a36-bf19-6d3e8a17c250",
"createdAt": "2026-09-07T22:47:41-03:00",
"code": 200,
"data": {
"id": "430556-818-972",
"templateId": 1187,
"customerId": "9c4e1f80-5a3b-4d72-8e16-7f2a9b350c48",
"type": "reward",
"balance": {
"balance": 527.5,
"numberRewardsUnused": 1,
"stampsBeforeReward": null
},
"updatedAt": "2026-09-07T22:47:41-03:00"
}
}Aprovechá esa respuesta: te devuelve la tarjeta entera, así que podés imprimir el saldo nuevo en el ticket sin hacer una segunda llamada. Y fijate en numberRewardsUnused: 1 — el cliente acaba de habilitar un beneficio con esta compra. Ese es el momento de decírselo, no un mail tres días después.
Conciliar
GET /api/v2/operations es el libro mayor. Filtros: templateId, customerId, cardId, startDate y endDate en formato Y-m-d, más page e itemsPerPage.
curl -G 'https://api.dardo.ai/api/v2/operations' \
-H 'X-API-Key: TU_CLAVE_API' \
--data-urlencode 'cardId=430556-818-972' \
--data-urlencode 'startDate=2026-09-07' \
--data-urlencode 'endDate=2026-09-07'Cada Operation trae id, cardId, customerId, eventId, eventName, amount, purchaseSum, balance, source, comment, managerId, locationId y createdAt. Es el endpoint del trabajo nocturno de conciliación contra tu cierre de caja.
Límites de monto
La especificación declara techos en dos de los campos de acumulación, y en el resto no declara ninguno:
| Operación | Campo | Rango válido |
|---|---|---|
add-transaction-amount / subtract-transaction-amount | amount (integer) | mayor que 0, hasta 1000000000 |
add-point / subtract-point | points (float) | mayor que 0, hasta 1000000000 |
add-purchase / subtract-purchase | amount (float) | mayor que 0. Sin máximo declarado. |
add-stamp, add-visit, add-scores, add-reward y sus subtract-... | según el caso | mayor que 0. Sin máximo declarado. |
El cero no es un valor válido. Todos estos campos declaran mínimo exclusivo: 0 se rechaza. No podés mandar points: 0 como operación neutra. Si un ticket no genera acumulación, simplemente no llames a la API.
Validá el rango del lado de tu sistema antes de enviar. Los endpoints de tarjetas solo declaran la respuesta 200, así que un valor fuera de rango te va a devolver un error cuyo formato no está documentado.
Idempotencia: no existe
Dos POST idénticos son dos acreditaciones
Buscamos en toda la especificación publicada: idempotency, Idempotency-Key, requestId, dedup. Cero resultados.
Los endpoints de acumulación de la Vía B aceptan exclusivamente el valor a acreditar, un comment de texto libre y purchaseSum. No hay campo ni header para deduplicar.
Esto importa porque la situación es cotidiana: tu sistema manda la acreditación, se corta la conexión antes de recibir la respuesta, tu sistema reintenta. Podés acreditar dos veces. No hay forma de que el servidor colapse ese reintento.
Con lo que hay hoy, la deduplicación es responsabilidad tuya. El patrón defensivo:
- Llevá una tabla local de pendientes. Cada acreditación se registra con el número de ticket de tu sistema como clave y un estado: pendiente, enviada, confirmada, fallida.
- Nunca reintentes a ciegas. Ante un tiempo de espera agotado, consultá primero
GET /api/v2/operationsfiltrando porcardIdy por la fecha de hoy, y buscá tu número de ticket en el campocommentantes de reenviar. - Escribí el número de ticket en
commentsiempre. Es el único campo de texto libre disponible y es lo único que hace posible el chequeo del punto 2. Tené en cuenta queGET /api/v2/operationsno filtra porcomment: vas a tener que traer las operaciones del día de esa tarjeta y buscar del lado tuyo. - Backoff exponencial y cola, nunca reintento inmediato en un bucle.
- Conciliación nocturna contra
GET /api/v2/operationspara detectar duplicados o faltantes que se hayan colado igual.
La Vía A está mejor parada en este punto: AccrueInput.transactionId es obligatorio y es la llave que usa reverse, y Check.externalTransactionId está descripto como el identificador de transacción externo del sistema de punto de venta. Que el transactionId sea la llave de reversa sugiere que el servidor lo persiste e indexa. Aun así, la especificación no declara qué pasa si mandás dos accrue con el mismo transactionId, así que tampoco ahí podemos prometerte idempotencia por escrito.
Sucursales
locationId no se puede escribir desde ninguna operación de acreditación. Si tu negocio tiene más de una sucursal, esto te afecta y conviene saberlo antes de diseñar la integración.
El campo locationId aparece en cuatro lugares de la especificación, y la dirección importa:
| Dónde | Lectura o escritura |
|---|---|
Operation.locationId | Salida. El dato existe y se persiste por operación. |
ManagerOutput.locationId | Salida. |
CreateManagerInput.locationId | Entrada. |
UpdateManagerInput.locationId | Entrada. |
Es decir: el único lugar donde locationId se puede escribir es el alta o la edición de un gerente. Ninguno de los endpoints de acumulación lo acepta en el cuerpo del pedido, ni los de la Vía B ni accrue en la Vía A —ni en la raíz ni dentro del Check—.
En el flujo normal eso funciona solo: quien escanea desde la app está logueado como gerente de su sucursal, y la operación queda asociada a esa sucursal. Pero una integración que se autentica con la clave API no tiene gerente: la clave es de la cuenta, no de una persona ni de un local.
Qué hacer mientras tanto: poné el identificador de sucursal en el comment de cada acreditación, con un formato estable, por ejemplo "CENTRO | TCK-889231". No es consultable por filtro y no alimenta ninguna métrica nativa, pero el dato queda registrado y lo podés extraer después con GET /api/v2/operations.
Y escribinos a soporte@dardo.ai antes de arrancar si tenés varias sucursales. Es una decisión de arquitectura que conviene resolver al principio, no después de seis meses de operaciones sin sucursal asignada.
purchaseSum
purchaseSum es un campo opcional que aceptan prácticamente todos los endpoints de acumulación de la Vía B: los add-... y subtract-... de monto, puntos, sellos, visitas, puntajes y premios, además de add-purchase y receive-reward. Está declarado como número decimal y admite nulo.
Del otro lado, Operation.purchaseSum figura como campo obligatorio en la salida: se persiste en cada operación. O sea, es el campo con el que le contás a Dardo cuánta plata movió una operación, más allá de cuántos puntos o sellos otorgó.
Dónde se nota: en un canje. Cuando un cliente usa un beneficio, la operación resta sellos o premios pero no declara por sí sola cuánto valía lo que se llevó. purchaseSum es donde lo informás.
curl -X POST 'https://api.dardo.ai/api/v2/cards/430556-818-972/subtract-reward' \
-H 'X-API-Key: TU_CLAVE_API' \
-H 'Content-Type: application/json' \
-d '{
"rewards": 1,
"comment": "Canje: postre de cortesia - ticket TCK-889244",
"purchaseSum": 4200.00
}'La especificación no declara en qué unidad va purchaseSum. Sabemos que es un decimal y que se persiste, pero no está escrito si se interpreta en unidades monetarias o en otra escala, ni cómo se relaciona con el amount que mandás en la misma llamada.
Es exactamente el tipo de ambigüedad que no conviene resolver adivinando cuando lo que está en juego son montos. Confirmalo con soporte@dardo.ai antes de empezar a mandar purchaseSum en producción. Preguntá dos cosas: la unidad, y si en un add-purchase que ya lleva amount hace falta mandar purchaseSum además o queda duplicado.
¿Ya hay un conector para tu sistema?
Antes de escribir código, vale la pena mirar si tu sistema de cobro ya tiene un conector nativo. Eso no está en la especificación —es información de tu cuenta, en vivo— y se consulta con una sola llamada:
curl 'https://api.dardo.ai/api/v2/external-services' \
-H 'X-API-Key: TU_CLAVE_API'Ese endpoint lista los servicios externos disponibles para tu cuenta, las claves que cada uno requiere para conectarse, y si ya está conectado. Los otros tres del mismo grupo son GET /api/v2/external-services/{code} para ver el estado de uno puntual, PUT para conectarlo o actualizarlo, y DELETE para desconectarlo.
No podemos decirte desde acá qué servicios vas a encontrar en esa lista. El catálogo depende de tu cuenta y puede cambiar. Corré la llamada con tu clave y fijate: si tu sistema de cobro está ahí, te ahorrás toda esta integración. Si no está, seguí con la Vía B de esta página.
Antes de dar fecha de entrega
- Confirmá el
templateIdde tu tarjeta de fidelidad conGET /api/v2/templates. - Confirmá el
typede esa tarjeta: determina qué endpoint de acumulación usar. Ver Tipos de tarjeta. - Probá contra una tarjeta inactiva antes de tocar tu programa real.
- Escribí el número de ticket en
commenten todas las llamadas. Es tu única vía de deduplicación. - Implementá la tabla de pendientes, el backoff y la conciliación nocturna antes de salir a producción, no después del primer duplicado.
- Si tenés varias sucursales, resolvé el tema de
locationIdcon soporte primero. - Confirmá la unidad de
purchaseSumcon soporte antes de mandarlo.