03 — Transacciones

Una transacción representa un cobro capturado por Yape, Plin o BIM en uno de los dispositivos vinculados al negocio. Requiere el alcance transactions:read para consultar y transactions:write para confirmar o disputar.

Ciclo de vida

CAPTURED ──confirm──► CONFIRMED
    │                     │
 dispute               dispute
    │                     │
    └────► DISPUTED ◄─────┘
  • CAPTURED: el parser extrajo la transacción de una notificación real. Estado inicial siempre.
  • CONFIRMED: un operador (o tu integración, con transactions:write) verificó que el cobro corresponde a una venta.
  • DISPUTED: se marcó como sospechosa o incorrecta, antes o después de confirmarla.
  • VOIDED: anulada — no se puede transicionar a VOIDED vía API pública, es un estado interno.

Confirmar una transacción que ya no está en CAPTURED, o disputar una que ya está DISPUTED o VOIDED, responde 409 CONFLICT con el estado y la nota actuales en details (ver confirm).

GET /v1/transactions

Lista paginada por cursor, con filtros compuestos.

curl "https://api.yallego.app/v1/transactions?wallet_code=YAPE&min_amount=20&limit=20" \
  -H "Authorization: Bearer yk_live_..."
ParámetroTipoDescripción
from / toISO 8601Rango sobre occurred_at
wallet_codestringYAPE, PLIN_BBVA, PLIN_INTERBANK, BIM
device_iduuidFiltra por dispositivo de captura
statusenumCAPTURED, CONFIRMED, DISPUTED, VOIDED
min_amount / max_amountdecimalRango de monto
searchstringNombre del remitente, código de seguridad o monto exacto (ver abajo)
disputed_from / disputed_toISO 8601Rango sobre disputed_at (cuándo se reportó)
disputed_byuuidUsuario que reportó (ver GET /v1/transactions/disputers)
limitint1–100, por defecto 50
cursorstringVer paginación

search siempre busca en sender_name, sin distinguir tildes ni mayúsculas. Además:

  • Si son exactamente 3 dígitos (247), también trae los cobros con ese security_code.
  • Si es un monto (18.5, 18.50, 18,50, S/ 18.50, 1,250.00), también trae los cobros de ese amount exacto.

Las coincidencias se suman: search=120 trae el cobro con código 120 y el de S/ 120.00. Los demás filtros se aplican encima.

{
  "data": [
    {
      "id": "9f8c2a1e-...",
      "wallet": { "code": "YAPE", "display_name": "Yape" },
      "sender_name": "JUAN CARLOS PEREZ R.",
      "amount": "35.50",
      "currency": "PEN",
      "security_code": "247",
      "approval_code": null,
      "status": "CAPTURED",
      "occurred_at": "2026-05-14T18:32:07.412Z",
      "confirmed_at": null,
      "confirmed_by": null,
      "confirmed_by_name": null,
      "disputed_at": null,
      "disputed_by": null,
      "disputed_by_name": null,
      "note": null,
      "device": { "id": "...", "label": "Celular caja principal" }
    }
  ],
  "pagination": { "has_more": true, "next_cursor": "eyJpZCI6...", "limit": 20 }
}

confirmed_by y disputed_by son null cuando quien confirmó o reportó fue una integración por clave de API en vez de un usuario del panel — la clave de API no tiene un user_id que atribuir. *_by_name es el nombre de ese usuario. En cobros reportados antes de que existiera el registro, disputed_at puede ser null.

La respuesta trae además time_zone, la zona horaria del negocio (America/Lima), para mostrar occurred_at en su hora local.

Un wallet_code que no existe no es un error: la lista sale vacía. Un status fuera del enum, un limit fuera de 1–100 o un parámetro mal formado responden 400 VALIDATION_ERROR con el mensaje genérico y el parámetro en details.fields (ver errores de validación).

Paginación

Las transacciones vienen de la más reciente a la más antigua (occurred_at). Para la página siguiente, repite la consulta con cursor=<pagination.next_cursor> y los mismos filtros; cuando has_more es false, next_cursor es null. Un cursor que no salió de esta ruta responde 400 VALIDATION_ERROR («El cursor de paginación no es válido.»).

GET /v1/transactions/{id}

Detalle completo de una transacción individual. 404 NOT_FOUND si no existe, pertenece a otro tenant o el id no es un UUID (lo mismo en confirm y dispute).

POST /v1/transactions/{id}/confirm

curl -X POST https://api.yallego.app/v1/transactions/{id}/confirm \
  -H "Authorization: Bearer yk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"note": "Verificado contra el pedido #4821"}'

note es opcional, hasta 500 caracteres (se guarda sin espacios al inicio ni al final). El cuerpo también es opcional: sin cuerpo equivale a {}. Un campo que no sea note responde 400 VALIDATION_ERROR. Dispara el evento transaction.confirmed a tus webhooks suscritos; el evento se registra junto con el cambio de estado, así que sale aunque haya una caída justo después.

Acepta Idempotency-Key: si se corta la conexión, repite la solicitud con la misma clave y recibes la respuesta original en vez de un 409. Ver 10 — Idempotencia.

Si la transacción ya no está en CAPTURED, el 409 CONFLICT dice cómo quedó. Compara note con la tuya para saber si la confirmación fue tuya:

{
  "error": {
    "code": "CONFLICT",
    "message": "Solo una transacción capturada puede confirmarse.",
    "details": { "status": "CONFIRMED", "note": "Verificado contra el pedido #4821" },
    "request_id": "b7f3a2e1-..."
  }
}

POST /v1/transactions/{id}/dispute

Misma forma que confirm, incluida Idempotency-Key. Procede desde CAPTURED o desde CONFIRMED: un cobro ya confirmado también se puede reportar. Guarda disputed_at y disputed_by y dispara transaction.disputed. Una transacción VOIDED o ya DISPUTED no puede volver a disputarse: 409 CONFLICT con details.status y details.note.

GET /v1/transactions/counts

Cuántos cobros esperan una acción, de todo el historial: CAPTURED (por confirmar) y DISPUTED (reportados). Útil para un contador en tu propio sistema sin paginar el listado.

{
  "by_status": { "CAPTURED": 5, "DISPUTED": 3 },
  "disputed": {
    "this_week": 2,
    "this_month": 3,
    "amount_this_month": "92.50",
    "week_start": "2026-10-12T05:00:00.000Z",
    "month_start": "2026-10-01T05:00:00.000Z",
    "time_zone": "America/Lima"
  }
}

disputed cuenta los cobros que siguen reportados según cuándo se reportaron (disputed_at), en la zona horaria del negocio; la semana empieza el lunes. week_start y month_start sirven tal cual como disputed_from para listar esos mismos cobros.

GET /v1/transactions/disputers

Quiénes reportaron los cobros que siguen reportados, con cuántos reportes cada uno, ordenados por nombre. Úsalo para ofrecer el filtro disputed_by. Los reportes hechos con una clave de API no tienen persona y no aparecen.

{ "data": [{ "user_id": "5d1e…", "name": "Rosa Quispe", "count": 2 }] }

GET /v1/transactions/summary

Totales agregados — útil para un dashboard propio sin traer cada transacción individual.

curl "https://api.yallego.app/v1/transactions/summary?from=2026-05-01T00:00:00Z&to=2026-05-14T23:59:59Z" \
  -H "Authorization: Bearer yk_live_..."
{
  "period": { "from": "2026-05-01T00:00:00Z", "to": "2026-05-14T23:59:59Z" },
  "totals": { "count": 1247, "amount": "42817.50", "currency": "PEN", "average": "34.34" },
  "by_wallet": [
    { "wallet_code": "YAPE", "count": 981, "amount": "33420.00" },
    { "wallet_code": "PLIN_BBVA", "count": 266, "amount": "9397.50" }
  ],
  "by_day": [{ "date": "2026-05-14", "count": 87, "amount": "2984.00" }],
  "by_hour": [{ "hour": "2026-05-14T12", "count": 14, "amount": "512.00" }],
  "by_device": [
    { "device_id": "6f1c…", "label": "Caja principal", "count": 1102, "amount": "38120.00" }
  ]
}
  • Sin from ni to, el resumen cubre los últimos days días (de 1 a 30; 14 por omisión) contados en la zona del negocio, y period.days dice cuántos.
  • by_day y by_hour usan la zona horaria del negocio (period.time_zone). hour tiene la forma AAAA-MM-DDTHH porque el rango puede cubrir varios días.
  • Las series sin prefijo incluyen todos los estados. Cada una tiene una variante received_* (received_totals, received_by_wallet, received_by_day, received_by_hour, received_by_device) que cuenta solo los cobros recibidos: CAPTURED y CONFIRMED. Los reportados (DISPUTED) y anulados (VOIDED) quedan fuera. confirmed_totals y confirmed_by_day cuentan solo CONFIRMED, y by_status reparte el período por estado.
  • by_device viene ordenado de mayor a menor monto.
  • El rango puede cubrir hasta 366 días. Si from es posterior a to, o el rango es más largo, la respuesta es 400 VALIDATION_ERROR.

POST /v1/transactions/export

Devuelve un CSV en la misma respuesta (no es una exportación asíncrona: no hay un trabajo que consultar después), con los mismos filtros que el listado — pensado para descargas puntuales, no para integraciones que necesitan datos estructurados (usa GET /v1/transactions con cursor para eso).

curl -X POST "https://api.yallego.app/v1/transactions/export?from=2026-05-01T00:00:00Z&status=CONFIRMED" \
  -H "Authorization: Bearer yk_live_..." \
  -o transacciones.csv
id,billetera,remitente,monto,moneda,codigo_seguridad,estado,ocurrio_en,dispositivo
9f8c2a1e-...,Yape,JUAN CARLOS PEREZ R.,35.50,PEN,247,CONFIRMED,2026-05-14T18:32:07.412Z,Celular caja principal
  • Hasta 10 000 filas, de la más reciente a la más antigua. Si los filtros abarcan más, el archivo trae las 10 000 más recientes sin avisar: acota con from y to y exporta por tramos.
  • Acepta los filtros del listado; limit no tiene efecto y cursor exporta desde esa posición en adelante.
  • UTF-8, separado por comas, una fila por línea. Un valor con comas, comillas o saltos de línea va entre comillas; uno que empieza con =, +, - o @ lleva un apóstrofo delante para que una hoja de cálculo no lo tome como fórmula. Viene con Content-Disposition: attachment; filename="transacciones-{comercio}.csv".

Con Idempotency-Key, repetir la descarga durante 24 h devuelve el mismo archivo; los mismos filtros con otra clave generan uno nuevo.

Además del límite del plan, cada clave puede exportar hasta 10 veces por minuto; la siguiente responde 429 con details.scope: "route" (ver 09 — Límites de tasa).