Ir al contenido
FINORA Academy
SistemaFINORA Merchant

FINORA Merchant · Tutorial

Cobro en la nube por API y webhooks

PilotoImplementadoIntermedio20 min

Cobrar un pedido desde tu servidor, sin app en el teléfono del motorizado: creas la venta, el Merchant la entrega a un datáfono, el cliente paga y tu servidor se entera por un webhook firmado. Es el cobro en la nube (en inglés, cloud payments).

  • La clave de API del comercio con alcance INTEGRACION o COMPLETO, guardada en tu servidor (variable FINORA_API_KEY). Nunca en la app ni en el repositorio.
  • La URL del Merchant que FINORA asigna a tu comercio, en la variable FINORA_URL (<URL_DEL_MERCHANT>).
  • El código del comercio (por ejemplo COM00042) y el identificador del datáfono (TERM0042).
  • Una URL https pública de tu servidor para recibir avisos.
  1. Suscríbete al resultado una sola vez. Guarda el secreto de la respuesta: solo se muestra ahí.

    Ventana de terminal
    curl -s -X POST /api/v1/pagos/webhooks/suscripciones \
    -H "X-API-Key: $FINORA_API_KEY" -H "Content-Type: application/json" \
    -d '{"merchantCode":"COM00042","nombre":"Despacho","url":"https://despacho.ejemplo.com/finora/eventos",
    "eventos":["intencion.resultado"]}'
  2. Crea la venta y envíala al datáfono en un solo viaje:

    Ventana de terminal
    curl -s -X POST /api/v1/pagos/intenciones \
    -H "X-API-Key: $FINORA_API_KEY" -H "Content-Type: application/json" \
    -d '{"idempotencyKey":"ped-8841-2026-10-21","merchantCode":"COM00042",
    "amount":2499.00,"currency":"VES","description":"Pedido a domicilio","externalOrderId":"PED-8841",
    "despacho":{"terminalId":"TERM0042","ttlSegundos":300}}'

    La respuesta trae la reference (FNR…) y el bloque despacho en PENDIENTE. Si se cae la red, repite con la misma idempotencyKey: recibes la misma venta.

  3. Síguela mientras llega el webhook (opcional):

    Ventana de terminal
    curl -s /api/v1/pagos/intenciones/FNRLZ3K9Q1/despacho -H "X-API-Key: $FINORA_API_KEY"

    El despacho pasa por PENDIENTE → ENTREGADA → ACEPTADA → COMPLETADA (o EN_DUDA, CANCELADA, EXPIRADA).

  4. Cancela si el cliente se arrepiente, solo antes de que el datáfono la acepte:

    Ventana de terminal
    curl -s -X POST /api/v1/pagos/intenciones/FNRLZ3K9Q1/despacho/cancelar \
    -H "X-API-Key: $FINORA_API_KEY"
  5. Recibe el webhook y verifica la firma. Llega un POST con X-Finora-Evento: intencion.resultado, X-Finora-Entrega y X-Finora-Signature: t=…,v1=…. Verifica sobre el cuerpo crudo:

    import crypto from 'node:crypto';
    function firmaValida(cuerpoCrudo, cabecera, secreto) {
    const p = Object.fromEntries(cabecera.split(',').map((x) => x.split('=')));
    if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false; // ventana de 5 minutos
    const esperado = crypto.createHmac('sha256', secreto).update(`${p.t}.${cuerpoCrudo}`).digest('hex');
    return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(p.v1 ?? ''));
    }

    Responde 2xx rápido y deduplica por X-Finora-Entrega: si no respondes, se reintenta a 1, 5, 25 y 125 minutos.

{
"evento": "intencion.resultado",
"externalId": "PED-8841",
"sucursal": { "storeCode": "SUC001" },
"referenciaFinora": "FNR8Q2LM4K",
"estado": "APROBADA",
"monto": 2499.00, "moneda": "VES", "riel": "TARJETA",
"autorizacion": "A1B2C3", "rrn": "622916125320", "terminalId": "TERM0042",
"tarjeta": { "marca": "VISA", "ultimos4": "0015" }
}

estado puede ser APROBADA, RECHAZADA, EN_DUDA, ANULADA, EN_CURSO o PENDIENTE. Como en el SDK, EN_DUDA no es un rechazo.

ReglaConsecuencia
Un cobro a la vez por datáfonoUn segundo envío responde 409 TERMINAL_OCUPADO, pero la venta queda creada: reenvíala más tarde con POST …/despacho
Vencer no es rechazarEXPIRADA libera el datáfono; reenvía la misma venta a otro motorizado
La clave tiene alcanceUn comercio principal actúa sobre sí y sus agencias; fuera de eso, 403 DESPACHO_NO_PERMITIDO
Un error tuyo no deshace el cobroLos avisos salen después de registrar el veredicto
  1. La suscripción aparece en GET /api/v1/pagos/webhooks/suscripciones?merchantCode=COM00042 (sin el secreto).
  2. Tras crear la venta, GET …/despacho muestra PENDIENTE y luego ENTREGADA cuando el datáfono la recoge.
  3. Con el cobro aprobado, tu servidor recibe intencion.resultado con estado = APROBADA, la firma verifica y el externalId es tu pedido.
  4. Repetir el paso 2 con la misma idempotencyKey devuelve la misma reference.

Siguiente: Lección 4 · Sucursal de origen, conciliación y portal en vivo