# Webhooks

Fluxis usa webhooks a **nivel de cuenta** para notificar a tu aplicación en tiempo real cuando ocurren eventos, como cambios de estado de pagos. Esta guía cubre registro, comportamiento de entrega y un resumen de verificación de firmas.

Para el recorrido completo de verificación HMAC (JSON canónico, ejemplo trabajado, endpoint de prueba), consulta [Verificación de firma de webhook](/es/docs/developers/webhook-signature-verification).

## Resumen

Cuando registras un webhook para una **cuenta**, Fluxis envía solicitudes HTTP POST a tu URL cuando ocurren eventos coincidentes. Cada solicitud incluye:

- Un payload JSON con datos del evento
- Encabezados de seguridad para verificación de firma

::::warning
Siempre verifica las firmas de webhook antes de procesar el payload. Consulta [Verificación de firma de webhook](/es/docs/developers/webhook-signature-verification).
::::

## Encabezados de webhook

Cada solicitud de webhook incluye los siguientes encabezados:

| Encabezado                | Descripción                                             |
| --------------------- | ------------------------------------------------------- |
| `X-Request-Signature` | Firma HMAC-SHA256 del payload                    |
| `X-Request-Timestamp` | Marca de tiempo Unix (segundos) cuando se generó la solicitud |
| `Content-Type`        | Siempre `application/json`                               |

## Registrar webhooks

Los webhooks se gestionan a **nivel de cuenta** (no por Punto de Venta). Usa las credenciales de tu organización para registrar un endpoint HTTPS para la cuenta vinculada a tu POS.

**Crear webhook:** `POST /v1/account/{accountID}/webhook`

```json
{
  "url": "https://your-server.com/webhooks/fluxis",
  "event_type": "payment_request",
  "description": "Order status updates"
}
```

La respuesta incluye un `secret` — **guárdalo de inmediato**; solo se devuelve al crear.

| `event_type`        | Cuándo se dispara                                      |
| ------------------- | -------------------------------------------------- |
| `payment_request`   | Cambios de estado de solicitud de pago (ver abajo)         |
| `incoming_transfer` | Eventos de transferencia entrante (reservado / uso futuro) |
| `refund`            | Eventos de reembolso                                      |

**Otros endpoints de gestión:**

| Acción            | Endpoint                                                          |
| ----------------- | ----------------------------------------------------------------- |
| List              | `GET /v1/account/{accountID}/webhook/list`                        |
| Update URL        | `PUT /v1/account/{accountID}/webhook/{webhookID}/url`             |
| Activate          | `PATCH /v1/account/{accountID}/webhook/{webhookID}/activate`      |
| Deactivate        | `PATCH /v1/account/{accountID}/webhook/{webhookID}/deactivate`    |
| Delete            | `DELETE /v1/account/{accountID}/webhook/{webhookID}/delete`       |
| Test delivery     | `POST /v1/account/{accountID}/webhook/{webhookID}/test`           |
| Delivery logs     | `GET /v1/account/{accountID}/webhook/logs?page=1&page_size=20`    |

Consulta la [Referencia API — Webhooks](/api/webhooks#create-webhook).

::::note
La gestión de webhooks requiere credenciales API a **nivel de organización**. La cuenta debe pertenecer a tu organización.
::::

---

## Verificar firmas de webhook (resumen)

Fluxis firma el **cuerpo JSON parseado completo** — por ejemplo `{ "data": { … } }` — no los bytes crudos de la solicitud ni solo el objeto interno `data`.

**Algoritmo:**

1. Serializa el cuerpo parseado a **JSON canónico** (compacto, claves ordenadas alfabéticamente en cada nivel de objeto)
2. Construye la cadena firmada: `<unix_timestamp>.<canonical_json>`
3. Calcula `HMAC-SHA256(webhook_secret, signed_string)` y codifica el resultado en hexadecimal
4. Compara con `X-Request-Signature` usando una función de tiempo constante
5. Rechaza si la marca de tiempo tiene más de **300 segundos** (5 minutos)

**Ejemplo de cadena firmada** (marca de tiempo `1712345678`, payload de muestra `payment_request`):

```
1712345678.{"data":{"account_id":"c87464d1-c54a-48c3-80c9-bee4fddf593d","external_reference_id":"order-123","notification_type":"payment_request_status_update","payment_request_id":"aa5d6c80-0b0b-4a2a-bbc8-14a41e6245bf","payment_request_type":"fixed","point_of_sale_id":"3cc1f7ca-c017-4e7e-b00d-aca0da6e464e","status":"completed"}}
```

::::note Campos opcionales
Campos como `external_reference_id` y `account_id` usan `omitempty` en la API — se omiten del JSON cuando no están definidos. Tu verificador debe canonicalizar el **cuerpo parseado tal como se recibió**, no una plantilla de esquema fija.
::::

**Verificación rápida en Node.js** (consulta la [guía completa](/es/docs/developers/webhook-signature-verification) para Python, Go y un ejemplo trabajado):

```typescript
import crypto from "crypto";

function sortKeys(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(sortKeys);
  if (value !== null && typeof value === "object") {
    return Object.keys(value as object)
      .sort()
      .reduce((acc, key) => {
        acc[key] = sortKeys((value as Record<string, unknown>)[key]);
        return acc;
      }, {} as Record<string, unknown>);
  }
  return value;
}

function verifyWebhookSignature(
  payload: unknown,
  signature: string,
  timestamp: string,
  secret: string,
): boolean {
  const requestTimestamp = parseInt(timestamp, 10);
  if (Math.floor(Date.now() / 1000) - requestTimestamp > 300) return false;

  const canonicalJson = JSON.stringify(sortKeys(payload));
  const signedString = `${timestamp}.${canonicalJson}`;
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(signedString)
    .digest("hex");

  if (signature.length !== expectedSignature.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(signature, "utf8"),
    Buffer.from(expectedSignature, "utf8"),
  );
}
```

---

## Qué estados disparan webhooks

Fluxis entrega webhooks `payment_request` cuando una solicitud de pago entra en **`processing`** (pago detectado) o un estado terminal de confirmación (**`completed`**, **`underpaid`** o **`overpaid`**).

Cambios de estado como **`expired`** o **`failed`** **no** se envían vía webhook — haz polling del endpoint de solicitud de pago o concilia vía transacciones.

## Estructura del payload de webhook

Los webhooks de actualización de estado de solicitud de pago tienen la siguiente estructura:

```json
{
  "data": {
    "notification_type": "payment_request_status_update",
    "payment_request_id": "aa5d6c80-0b0b-4a2a-bbc8-14a41e6245bf",
    "payment_request_type": "fixed",
    "external_reference_id": "your-order-id-123",
    "status": "completed",
    "point_of_sale_id": "3cc1f7ca-c017-4e7e-b00d-aca0da6e464e",
    "account_id": "c87464d1-c54a-48c3-80c9-bee4fddf593d"
  }
}
```

| Campo                   | Descripción                                                  |
| ----------------------- | ------------------------------------------------------------ |
| `notification_type`     | Tipo de notificación (p. ej., `payment_request_status_update`) |
| `payment_request_id`    | Identificador único de la solicitud de pago                     |
| `payment_request_type`  | Tipo de solicitud de pago (p. ej., `fixed`)                      |
| `external_reference_id` | Tu ID de referencia interna (opcional; omitido si no está definido)      |
| `status`                | Nuevo estado de la solicitud de pago                            |
| `point_of_sale_id`      | ID del Punto de Venta                                      |
| `account_id`            | ID de la cuenta asociada (opcional; omitido si no está definido)    |

---

## Mejores prácticas

### Responder rápidamente

Tu endpoint debe devolver HTTP `200 OK` en **2 segundos**. Si necesitas procesamiento pesado, confirma el webhook de inmediato y encola el trabajo para ejecución en segundo plano.

```typescript
app.post("/webhooks/fluxis", async (req, res) => {
  if (!verifyWebhookSignature(req.body, /* ... */)) {
    return res.status(401).send();
  }
  res.status(200).json({ received: true });
  processWebhookAsync(req.body).catch(console.error);
});
```

### Manejar duplicados (idempotencia)

Puedes recibir el mismo evento de webhook varias veces. Usa `payment_request_id` y `status` para deduplicar.

### Conciencia de lógica de reintentos

Si tu endpoint no responde con HTTP `200`, Fluxis puede reintentar el webhook. Asegúrate de que tu manejador sea idempotente.

---

## Probar webhooks

1. Expón tu servidor local con [ngrok](https://ngrok.com) o similar durante el desarrollo.
2. Registra la URL pública vía `POST /v1/account/{accountID}/webhook`.
3. Dispara una entrega de prueba: `POST /v1/account/{accountID}/webhook/{webhookID}/test` — usa la misma ruta de firma que producción.
4. Confirma que tu manejador verifica la firma y devuelve HTTP `200`.

Consulta [Verificación de firma de webhook](/es/docs/developers/webhook-signature-verification) para verificación paso a paso y modos de fallo comunes.

---

## Solución de problemas

| Problema | Qué verificar |
| ----- | ------------- |
| La firma falla | Secreto correcto de la respuesta de creación; firmar el cuerpo parseado `{data:…}`; reglas de JSON canónico — [guía completa](/es/docs/developers/webhook-signature-verification) |
| No se reciben eventos | URL HTTPS pública; HTTP `200` en 2 s; webhook habilitado; `event_type` correcto |
| Faltan webhooks `expired` / `failed` | Esperado — haz polling de `GET /v1/pos/{posID}/payment-request/{paymentRequestID}` |

---

## ¿Necesitas ayuda?

Contacta [support@fluxis.us](mailto:support@fluxis.us) con tu ID de cuenta, ID de webhook y marca de tiempo de la entrega problemática.
