Centro de Ayuda de Dardo

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 ventaVía B — API estándar
EndpointsPOST /api/v2/marketplace/resolve-credentials
POST /api/v2/marketplace/accrue
POST /api/v2/marketplace/reverse
GET /api/v2/cards
POST /api/v2/cards/{id}/add-...
Autenticaciónheader X-App-Tokenheader X-API-Key
Identificación del clienteLa resuelve el servidor: mandás teléfono, mail o número de serie y busca o registra la tarjetaLa hacés vos: buscás la tarjeta y después operás sobre su id
Datos del ticketObjeto Check completo: ítems, impuestos, propina, servicio, horariosUn monto, o una cantidad de puntos/sellos/visitas
Reglas de acumulaciónLas aplica Dardo del lado del servidor, incluso por productoLas calculás vos antes de llamar
Revertir una operaciónSí, POST /marketplace/reverse con el transactionIdNo hay endpoint de reversa: compensás con el subtract-... equivalente
Unidad de los montosCentavos (integer)Decimal (float)
Errores declarados200, 400, 403Solo 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.

CampoTipoObligatorioQué es
namesstring[]Nombres de credenciales a devolver. Su propia descripción aclara que si no se especifican, se devuelven todas las del comercio.
credentials{name, value}[]NoCredenciales 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.

CampoTipoObligatorioQué es
checkCheckEl ticket. Desglose completo abajo.
transactionIdstringIdentificador único de la transacción. Es la llave con la que después vas a poder revertirla.
phonestringNo*Teléfono del cliente.
emailstringNo*Mail del cliente.
serialNumberstringNo*Número de serie de la tarjeta del cliente.
firstNamestringNoNombre del cliente.
lastNamestringNoApellido del cliente.
credentials{name, value}[]NoCredenciales 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.pricetodos 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.

CampoTipoObligatorioUnidadQué es
amountintegercentavosMonto total del ticket.
currencystringMoneda en código ISO 4217 de 3 letras, por ejemplo ARS.
selectionsSelection[]Los ítems del ticket.
externalTransactionIdstringNoEl identificador de la transacción en tu sistema de cobro.
checkOpenedAtstringNoISO 8601Cuándo se abrió la mesa o el ticket.
checkClosedAtstringNoISO 8601Cuándo se cerró.
taxAmountintegerNocentavosImpuestos.
discountAmountintegerNocentavosDescuentos.
tipAmountintegerNocentavosPropina.
serviceAmountintegerNocentavosServicio de mesa.
totalTipReceivedstringNoPropina total recibida. Se guarda como dato crudo.
orderUpdatedAtstringNoFecha 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:

CampoTipoObligatorioQué es
quantityintegerCantidad de unidades. Tiene que ser mayor que cero.
totalPriceintegerPrecio total de la línea.
idstringNoIdentificador único del ítem de inventario. Necesario para que funcione una regla de acumulación por producto.
groupIdstringNoGrupo del ítem. Necesario para reglas por familia de productos.
displayNamestringNoNombre visible del ítem.
priceintegerNoPrecio 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.

CampoTipoObligatorioQué es
transactionIdstringEl mismo que mandaste en accrue.
credentials{name, value}[]NoCredenciales 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ódigoSignificadoCuerpo de ejemplo
400Pedido mal formado{"errors": ["Could not decode request body."]}
400Falta un campo obligatorio{"errors": ["Credentials should not be blank"]}
403Falta el header{"errors": ["Missed header X-App-Token"]}
403El 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 el customerId.
  • 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ónCampoRango válido
add-transaction-amount / subtract-transaction-amountamount (integer)mayor que 0, hasta 1000000000
add-point / subtract-pointpoints (float)mayor que 0, hasta 1000000000
add-purchase / subtract-purchaseamount (float)mayor que 0. Sin máximo declarado.
add-stamp, add-visit, add-scores, add-reward y sus subtract-...según el casomayor 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:

  1. 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.
  2. Nunca reintentes a ciegas. Ante un tiempo de espera agotado, consultá primero GET /api/v2/operations filtrando por cardId y por la fecha de hoy, y buscá tu número de ticket en el campo comment antes de reenviar.
  3. Escribí el número de ticket en comment siempre. Es el único campo de texto libre disponible y es lo único que hace posible el chequeo del punto 2. Tené en cuenta que GET /api/v2/operations no filtra por comment: vas a tener que traer las operaciones del día de esa tarjeta y buscar del lado tuyo.
  4. Backoff exponencial y cola, nunca reintento inmediato en un bucle.
  5. Conciliación nocturna contra GET /api/v2/operations para 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óndeLectura o escritura
Operation.locationIdSalida. El dato existe y se persiste por operación.
ManagerOutput.locationIdSalida.
CreateManagerInput.locationIdEntrada.
UpdateManagerInput.locationIdEntrada.

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 templateId de tu tarjeta de fidelidad con GET /api/v2/templates.
  • Confirmá el type de 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 comment en 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 locationId con soporte primero.
  • Confirmá la unidad de purchaseSum con soporte antes de mandarlo.

Ver también

On this page