Ir al contenido

Webhooks de eventos (POST)

Crowder te notifica las transiciones del lifecycle vía webhooks. Cada evento dispara una transición concreta; no hay eventos puramente informativos.

POST {endpoint}/{interaction}/events

Headers:

Authorization: Bearer <api_key>
Content-Type: application/json
statusTransiciónTriggerCampos relevantes
purchaseReservedvalid → reservedCrowder te pide reservar stock real. Vos devolvés expiresAt en el ack — el TTL arranca acá.currency, partnerItems
purchasePaidreserved → confirmedPago capturado en Crowder.currency, purchase, partnerItems
purchaseExpiredreserved → expiredEl TTL de la reserva venció sin pago. Crowder notifica para que el partner libere el hold de forma sincronizada (no solo por su cuenta).status
purchaseRefundedconfirmed → confirmed (parcial) o confirmed → refunded (total)Devolución de uno o más items. Crowder envía un array items con qué devolver. Si abarca todos los items vigentes → terminal refunded. Si es subset → la interacción queda en confirmed con partnerItems reducido.reason, items

Secuencia happy path: purchaseReservedpurchasePaid.

{
"status": "purchaseReserved",
"currency": "ARS",
"partnerItems": [
{
"uuid": "HOTEL-3N-PREMIUM",
"type": "HOTEL",
"description": "Hotel 3 noches · Hilton Buenos Aires",
"price": 120000.00,
"quantity": 1
}
]
}

No hay envelope acknowledged. El código HTTP carga el significado y el body lleva el status resultante del lifecycle como discriminador, más los campos que el partner aporta como output.

HTTPBodySignificado
200{ "status": "<lifecycle>", ... } con el lifecycle alcanzado: reserved (lleva expiresAt), confirmed, expired, refundedProcesado correctamente.
202{ "status": "deferred", "notes"?: "..." }Recibido, procesamiento async.
409{ "status": "error", "error": { "code": "invalid_transition", "message": "..." } }El estado actual no admite el evento. No se mutó nada.
400{ "status": "error", "error": { "code": "invalid_payload" | "unsupported_event", ... } }Payload mal formado o evento desconocido.
401 / 403{ "status": "error", "error": { "code": "auth_invalid", ... } }Bearer ausente o inválido.
404{ "status": "error", "error": { "code": "not_found", ... } }interaction desconocida.
5xx(libre)Error interno. Entra a reintentos.
200 OK · ack del purchaseReserved (TTL arranca acá)
{ "status": "reserved", "expiresAt": "2026-04-28T15:48:00Z" }
200 OK · ack de purchasePaid
{ "status": "confirmed" }
200 OK · ack de purchaseExpired
{ "status": "expired" }
200 OK · ack de purchaseRefunded (parcial — quedan items vigentes)
{ "status": "confirmed" }
200 OK · ack de purchaseRefunded (total)
{ "status": "refunded" }
202 Accepted · partner difiere el procesamiento
{
"status": "deferred",
"notes": "Encolado en queue de fulfillment. Ref: voucher-q-4528."
}
409 Conflict · transición inválida
{
"status": "error",
"error": {
"code": "invalid_transition",
"message": "Evento purchasePaid requiere lifecycle=\"reserved\", actual=\"valid\"."
}
}

Backoff exponencial: 1m → 5m → 30m → 2h → 12h → 24h. Después: failed en backoffice.

  • 4xx salen del ciclo de reintentos (no se vuelve a intentar).
  • 5xx entra al backoff exponencial.

El partner valida que su lifecycle actual coincida con el “from” de la tabla. Si no encaja (ej: purchasePaid sobre valid), respondé 409 invalid_transition sin mutar nada. Crowder consulta el GET de estado antes de disparar, así que un 409 indica una divergencia que requiere resolución bilateral.