> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hurrypayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks salientes

> Recibe en tu sistema un POST JSON firmado cada vez que un pedido se paga, se envía, un cobro falla, se registra un reembolso o llega una disputa.

Los webhooks salientes avisan a tus sistemas (ERP, Make, n8n, Zapier o tu propio servidor) de lo que pasa en Hurry sin que tengas que consultar nada. Das de alta una URL, eliges qué eventos quieres y Hurry le hace un `POST` con el JSON completo del pedido, el pago, el cliente, la tienda y la pasarela cada vez que ocurre algo.

Se gestionan desde el CRM, en **Cuenta → Webhooks**. Solo la cuenta principal puede verlos y configurarlos: los colaboradores no tienen acceso a esta sección.

## Cómo funciona

**1. Crea un endpoint**

Pulsa **Nuevo endpoint**, ponle un nombre, escribe la URL `https` de tu receptor y marca los eventos que quieres recibir. Si lo necesitas, acota el endpoint a unas tiendas concretas y añade cabeceras propias.

**2. Guarda el secreto**

Al crearlo, Hurry te muestra un secreto con el formato `whsec_...`. Guárdalo en tu receptor: es lo que usarás para verificar la firma de cada envío. Puedes volver a verlo o rotarlo desde la tarjeta del endpoint.

**3. Verifica la firma y responde 2xx**

En cada entrega, comprueba la cabecera `X-Hurry-Signature` sobre el cuerpo crudo y responde un código `2xx` en menos de 10 segundos. Si no respondes `2xx`, Hurry reintenta solo.

**4. Pruébalo**

Pulsa **Probar** en el endpoint. Hurry manda un evento `ping` firmado y te enseña el código HTTP y la respuesta de tu servidor.

## Eventos

Cada endpoint se suscribe a los eventos que quieras. El código del evento viaja en el campo `type` del JSON y en la cabecera `X-Hurry-Event`.

| Evento | Cuándo se envía | Activo por defecto |
| - | - | :-: |
| `order.created` | Un comprador llega al checkout y deja su email. El pedido existe pero aún no está pagado. | No |
| `order.paid` | Cobro correcto de un checkout. Lleva el pedido completo: estado, líneas, importes, cliente, pago, tienda y pasarela. | Sí |
| `order.shipped` | El pedido se marca como enviado o se le pone un número de seguimiento. | Sí |
| `cart.abandoned` | El escáner de recuperación detecta un checkout abandonado. | No |
| `payment.succeeded` | Cualquier cobro correcto, incluidos los recurrentes sin pedido (rebills de suscripciones). | No |
| `payment.failed` | La tarjeta, el 3DS o la pasarela rechazan el cobro. Incluye el código y el mensaje de error. | Sí |
| `refund.created` | Se registra un reembolso (desde el CRM, la IA o el panel de la pasarela), con su estado e importe. | Sí |
| `dispute.opened` | Llega una disputa, un contracargo, una consulta o un aviso temprano de fraude. | Sí |
| `dispute.closed` | Una disputa llega a su estado final: ganada, perdida o cerrada. | Sí |
| `ping` | Prueba lanzada desde la pantalla o desde el MCP. No se puede desactivar. | — |

> [!TIP]
>
> Una venta de checkout dispara `order.paid` **y** `payment.succeeded` a la vez. Si solo quieres ventas, suscríbete a `order.paid`. Si además quieres los cobros recurrentes de las suscripciones, usa `payment.succeeded`.

### Alcance por tienda

Un endpoint puede recibir los eventos de **todas las tiendas** (incluidos los cobros recurrentes, que no tienen tienda) o **solo de algunas**. Si eliges tiendas concretas, los cobros sin tienda no se envían a ese endpoint.

### Cabeceras personalizadas

Puedes añadir hasta **5 cabeceras propias** por endpoint, por ejemplo `Authorization: Bearer ...` si tu receptor exige un token. Los nombres admiten letras, números y guiones. No se pueden definir las cabeceras que fija Hurry: `Host`, `Content-Type`, `Content-Length`, `Transfer-Encoding`, `User-Agent` ni ninguna que empiece por `X-Hurry-`.

## Formato del evento

Todos los eventos comparten el mismo sobre. Lo que cambia es el contenido de `data`.

```json theme={null}
{
  "id": "evt_7Qp9kS2mB1xZcV4nT8wL3aRd",
  "type": "order.paid",
  "apiVersion": "2026-11-01",
  "createdAt": "2026-11-02T18:42:11.512+01:00",
  "account": { "id": "5f1c0d6e-8a2b-4c3d-9e0f-112233445566" },
  "endpoint": { "id": "c9e1a2b3-4d5e-6f70-8192-a3b4c5d6e7f8", "name": "ERP" },
  "data": {
    "order": { "...": "..." },
    "payment": { "...": "..." },
    "customer": { "...": "..." },
    "shop": { "...": "..." },
    "gateway": { "...": "..." }
  }
}
```

**id** `string`

Identificador del evento (`evt_` + 24 caracteres). Es **el mismo** en todos los endpoints, en todos los reintentos y en los reenvíos manuales del mismo hecho. Úsalo para ignorar duplicados.

**type** `string`

Código del evento: `order.paid`, `payment.failed`, `refund.created`...

**apiVersion** `string`

Versión del formato del JSON. Actualmente `2026-11-01`. Cada endpoint queda fijado a la versión con la que se creó.

**createdAt** `string`

Fecha de creación del evento en ISO 8601 con zona horaria.

**account.id** `string`

Identificador de tu cuenta en Hurry.

**endpoint** `object`

`id` y `name` del endpoint que recibe el evento.

**data** `object`

Todo lo que se sabe del hecho. Qué nodos vienen rellenos depende del evento; los que no aplican llegan como `null`.

> [!NOTE]
>
> **Importes y fechas.** Todos los importes son enteros en la unidad menor de la moneda (céntimos: `4980` son 49,80 EUR), con la moneda al lado y una versión legible en los campos `*Formatted`. Las fechas van en ISO 8601 con desplazamiento horario.

### Qué lleva `data` en cada evento

| Evento | `order` | `payment` | `customer` | `shop` | `gateway` | Extra |
| - | :-: | :-: | :-: | :-: | :-: | - |
| `order.created`, `cart.abandoned`, `order.shipped` | Sí | Si existe | Si existe | Sí | Sí | — |
| `order.paid`, `payment.succeeded`, `payment.failed` | Si hay pedido | Sí | Sí | Si hay pedido | Sí | — |
| `refund.created` | Si hay pedido | Sí | Sí | Si hay pedido | Sí | `refund` |
| `dispute.opened`, `dispute.closed` | Si se cruzó | Si se cruzó | Si se cruzó | Si se cruzó | Sí | `dispute` |
| `ping` | — | — | — | — | — | `message`, `endpoint` |

> [accordion] data.order: el pedido
>
> **id** `string`
>
> Identificador del pedido en Hurry.
>
> **externalId** `string`
>
> Referencia externa del carrito o de la plataforma de origen.
>
> **number** `integer | null`
>
> Número de pedido secuencial, si ya se ha asignado.
>
> **name** `string`
>
> Nombre legible del pedido, por ejemplo `#1001`.
>
> **status** `string`
>
> Estado del pedido en el CRM: `PENDING`, `IN_PROGRESS`, `REQUIRES_ACTION`, `PAID`, `PARTIALLY_REFUNDED`, `REFUNDED` o `CANCELED`. Lee la nota de abajo antes de usarlo para saber si está cobrado.
>
> **paid** `boolean`
>
> `true` si el pedido está cobrado. Derivado del pago, vale para todas las pasarelas.
>
> **paidAt** `string | null`
>
> Momento del cobro.
>
> **paymentStatus** `string | null`
>
> Estado del pago asociado: `CREATED`, `REQUIRES_ACTION`, `PROCESSING`, `SUCCEEDED`, `CANCELED` o `FAILED`.
>
> **currency** `string`
>
> Moneda de cobro (ISO 4217).
>
> **amounts** `object`
>
> Importes de cobro en unidad menor: `currency`, `subtotal`, `tax`, `shipping`, `discount`, `upsells`, `total` y `totalFormatted`.
>
> **displayAmounts** `object`
>
> Los mismos importes en la moneda que vio el comprador en el checkout, más `fxRate` cuando hubo conversión.
>
> **discountCode** `string | null`
>
> Código de descuento aplicado.
>
> **shippingOption** `string | null`
>
> Nombre de la opción de envío elegida.
>
> **items** `array`
>
> Líneas del pedido. Cada una con `id`, `productId`, `sku`, `name`, `imageUrl`, `quantity`, `unitAmount`, `taxAmount` y `totalAmount`.
>
> **shipping** `object`
>
> Seguimiento: `shipped`, `shippedAt`, `trackingNumber` y `trackingUrl`.
>
> **checkoutUrl** `string | null`
>
> URL del checkout de Hurry para este pedido.
>
> **sourceUrl** `string | null`
>
> Página de la tienda desde la que se inició la compra.
>
> **crmUrl** `string`
>
> Enlace directo al pedido en el CRM de Hurry.
>
> **createdAt / updatedAt** `string`
>
> Fechas de creación y última modificación.
>
> > [!WARNING]
> >
> > `order.status` es el estado del pedido en el CRM y **solo las tiendas Shopify lo pasan a `PAID`**; en el resto de pasarelas se queda en `IN_PROGRESS` aunque el cobro esté hecho. Para saber si un pedido está cobrado usa `order.paid`, `order.paidAt` y `order.paymentStatus`, que salen del pago.

> [accordion] data.payment: el pago
>
> **id** `string`
>
> Identificador del pago en Hurry.
>
> **status** `string`
>
> `CREATED`, `REQUIRES_ACTION`, `PROCESSING`, `SUCCEEDED`, `CANCELED` o `FAILED`.
>
> **amount / currency / amountFormatted** `integer / string / string`
>
> Importe cobrado en unidad menor, su moneda y la versión legible.
>
> **amountUsd** `integer | null`
>
> Importe convertido a USD en unidad menor, si está disponible.
>
> **psp** `string`
>
> Pasarela que procesó el cobro: `STRIPE`, `STRIPE_EVERVAULT`, `WHOP`, `ONE`, `SUMUP`, `SQUARE`, `MAMO`, `AIRWALLEX`, `SHOPIFY`, `MOR`, `SLASH`, `NEXI`, `NEXI_ALIAS`, `REDSYS` o `REVOLUT`.
>
> **providerPaymentId** `string | null`
>
> Identificador del cobro en la pasarela (por ejemplo el `pi_...` de Stripe).
>
> **recurring** `boolean`
>
> `true` si es un cobro recurrente sin pedido (rebill).
>
> **batchJobId** `string | null`
>
> Lote de cobros recurrentes al que pertenece, si aplica.
>
> **email / phone** `string | null`
>
> Contacto del comprador. Se prefiere el del cliente al que dio la pasarela.
>
> **paymentMethod** `object`
>
> `type`, `brand`, `last4`, `expMonth`, `expYear`, `country` y `funding`. Nunca viaja el número completo de la tarjeta.
>
> **error** `object | null`
>
> `code` y `message` del rechazo cuando el cobro falla.
>
> **refunds** `array`
>
> Reembolsos del pago, con el mismo formato que `data.refund`.
>
> **refundedAmount** `integer`
>
> Suma de los reembolsos correctos o pendientes, en unidad menor.
>
> **createdAt / updatedAt** `string`
>
> Fechas de creación y última modificación.

> [accordion] data.customer: el cliente
>
> `id`, `email`, `name`, `phone`, `address` (`line1`, `line2`, `city`, `state`, `postalCode`, `country`) y `createdAt`. No incluye ningún dato de tarjeta.

> [accordion] data.shop y data.gateway: tienda y pasarela
>
> **shop**: `id`, `name`, `url`, `slug`, `customDomain`, `type` (`EXTERNAL` o `NATIVE`) y `platform` (`SHOPIFY`, `WORDPRESS` o `API`).
>
> **gateway**: `id`, `name` y `psp` de la credencial de pago con la que se cobró.

> [accordion] data.refund: el reembolso (refund.created)
>
> `id`, `providerRefundId`, `amount`, `currency`, `amountFormatted`, `status` (`PENDING`, `SUCCEEDED`, `FAILED` o `CANCELED`), `reason`, `failureReason`, `createdAt` y `updatedAt`.

> [accordion] data.dispute: la disputa (dispute.opened y dispute.closed)
>
> **id / providerDisputeId** `string`
>
> Identificadores en Hurry y en la pasarela.
>
> **psp** `string`
>
> Pasarela donde se abrió.
>
> **type** `string`
>
> `CHARGEBACK`, `INQUIRY` o `EARLY_FRAUD_WARNING`.
>
> **state** `string`
>
> `NEEDS_RESPONSE`, `UNDER_REVIEW`, `WON`, `LOST`, `CHARGE_REFUNDED` o `CLOSED`.
>
> **previousState** `string | null`
>
> Estado anterior, útil en `dispute.closed` para saber de dónde viene.
>
> **providerStatus / reason / networkReasonCode** `string | null`
>
> Estado y motivo tal como los reporta la pasarela y la red de tarjetas.
>
> **amount / currency / amountFormatted** `integer / string / string`
>
> Importe en disputa.
>
> **customerEmail / providerChargeId** `string | null`
>
> Email del comprador y cargo afectado en la pasarela.
>
> **openedAt / evidenceDueBy / closedAt** `string | null`
>
> Fechas de apertura, límite para aportar pruebas y cierre.
>
> **fundsWithdrawn** `boolean`
>
> Si la pasarela ya ha retirado los fondos.
>
> **crmUrl** `string`
>
> Enlace a la sección de riesgo del CRM.

> [accordion] ping: la prueba
>
> `message` con un texto fijo y `endpoint` con `id`, `name`, `url` y la lista `events` a la que está suscrito. Sirve para comprobar que tu receptor está accesible y que verificas bien la firma.

### Ejemplo completo: `order.paid`

```json theme={null}
{
  "id": "evt_7Qp9kS2mB1xZcV4nT8wL3aRd",
  "type": "order.paid",
  "apiVersion": "2026-11-01",
  "createdAt": "2026-11-02T18:42:11.512+01:00",
  "account": { "id": "5f1c0d6e-8a2b-4c3d-9e0f-112233445566" },
  "endpoint": { "id": "c9e1a2b3-4d5e-6f70-8192-a3b4c5d6e7f8", "name": "ERP" },
  "data": {
    "order": {
      "id": "0b8d3f2a-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "externalId": "CART-1762100131-ab12",
      "number": 1001,
      "name": "#1001",
      "status": "IN_PROGRESS",
      "paid": true,
      "paidAt": "2026-11-02T18:42:11+01:00",
      "paymentStatus": "SUCCEEDED",
      "currency": "EUR",
      "amounts": {
        "currency": "EUR",
        "subtotal": 4990,
        "tax": 0,
        "shipping": 490,
        "discount": 500,
        "upsells": 0,
        "total": 4980,
        "totalFormatted": "49,80 EUR"
      },
      "displayAmounts": {
        "currency": "EUR",
        "subtotal": 4990,
        "tax": 0,
        "shipping": 490,
        "discount": 500,
        "upsells": 0,
        "total": 4980,
        "totalFormatted": "49,80 EUR"
      },
      "discountCode": "BIENVENIDA10",
      "shippingOption": "Envio 24-48h",
      "items": [
        {
          "id": "3c2b1a90-5d6e-4f70-8192-a3b4c5d6e7f8",
          "productId": "prod_123",
          "sku": "VELA-LAV-200",
          "name": "Vela aromatica Lavanda 200 g",
          "imageUrl": "https://cdn.example.com/vela.jpg",
          "quantity": 2,
          "unitAmount": 2495,
          "taxAmount": 0,
          "totalAmount": 4990
        }
      ],
      "shipping": {
        "shipped": false,
        "shippedAt": null,
        "trackingNumber": null,
        "trackingUrl": null
      },
      "checkoutUrl": "https://pay.mitienda.com/checkout/detail/0b8d3f2a-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "sourceUrl": "https://mitienda.com/products/vela-lavanda",
      "crmUrl": "https://app.hurrypayments.com/private/payments?order=0b8d3f2a-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "createdAt": "2026-11-02T18:40:02+01:00",
      "updatedAt": "2026-11-02T18:42:11+01:00"
    },
    "payment": {
      "id": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
      "status": "SUCCEEDED",
      "amount": 4980,
      "currency": "EUR",
      "amountFormatted": "49,80 EUR",
      "amountUsd": 5411,
      "psp": "STRIPE",
      "providerPaymentId": "pi_3QxYz0000000000000000000",
      "recurring": false,
      "batchJobId": null,
      "email": "ana@example.com",
      "phone": "+34600111222",
      "paymentMethod": {
        "type": "CARD",
        "brand": "visa",
        "last4": "4242",
        "expMonth": 12,
        "expYear": 2029,
        "country": "ES",
        "funding": "credit"
      },
      "error": null,
      "refunds": [],
      "refundedAmount": 0,
      "createdAt": "2026-11-02T18:41:30+01:00",
      "updatedAt": "2026-11-02T18:42:11+01:00"
    },
    "customer": {
      "id": "d4c3b2a1-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "email": "ana@example.com",
      "name": "Ana Garcia",
      "phone": "+34600111222",
      "address": {
        "line1": "Calle Mayor 1",
        "line2": null,
        "city": "Madrid",
        "state": "Madrid",
        "postalCode": "28013",
        "country": "ES"
      },
      "createdAt": "2026-10-12T10:05:00+02:00"
    },
    "shop": {
      "id": "e5f6a7b8-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "name": "Mi tienda",
      "url": "https://mitienda.com",
      "slug": "mi-tienda",
      "customDomain": null,
      "type": "EXTERNAL",
      "platform": "SHOPIFY"
    },
    "gateway": {
      "id": "f1e2d3c4-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "name": "Stripe principal",
      "psp": "STRIPE"
    }
  }
}
```

## Cabeceras HTTP

Cada entrega es un `POST` con `Content-Type: application/json` y `User-Agent: Hurry-Webhooks/1.0`. Además lleva estas cabeceras:

| Cabecera | Contenido |
| - | - |
| `X-Hurry-Event` | Código del evento (`order.paid`, `refund.created`...). |
| `X-Hurry-Event-Id` | Id del evento. El mismo en todos los endpoints y reintentos del mismo hecho. |
| `X-Hurry-Delivery-Id` | Id de esta entrega concreta. Cambia en cada reintento. |
| `X-Hurry-Timestamp` | Segundos Unix del envío. Va dentro de lo firmado. |
| `X-Hurry-Signature` | `t=<timestamp>,v1=<HMAC-SHA256 en hexadecimal de t + "." + cuerpo>`. |
| `X-Hurry-Attempt` | Número de intento, de 1 a 7. |
| `X-Hurry-Version` | Versión del formato del JSON (`2026-11-01`). |

A estas se suman las cabeceras personalizadas que hayas definido en el endpoint.

## Verificar la firma

Cada envío va firmado con **HMAC-SHA256** usando el secreto del endpoint. Verifica la firma antes de confiar en el contenido:

1. Lee el cuerpo **crudo** de la petición, tal como llega, antes de parsear el JSON.
2. Saca `t` y `v1` de la cabecera `X-Hurry-Signature`.
3. Rechaza la petición si `t` difiere más de **300 segundos** de tu hora actual. Así una entrega capturada no se puede reproducir más tarde.
4. Calcula `HMAC-SHA256(secreto, t + "." + cuerpo)` y pásalo a hexadecimal en minúsculas.
5. Compara el resultado con `v1` en **tiempo constante**.

```javascript Node.js (Express) theme={null}
// El cuerpo tiene que llegar CRUDO: express.raw, no express.json.
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.HURRY_WEBHOOK_SECRET; // whsec_...

app.post('/webhooks/hurry', express.raw({ type: '*/*' }), (req, res) => {
  const header = req.get('X-Hurry-Signature') || '';
  const t = (header.match(/t=(\d+)/) || [])[1];
  const v1 = (header.match(/v1=([a-f0-9]+)/) || [])[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
  const expected = crypto.createHmac('sha256', SECRET).update(t + '.' + req.body.toString('utf8')).digest('hex');
  if (expected.length !== v1.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // TODO: guarda event.id para ignorar duplicados y procesa en segundo plano
  console.log(event.type, event.data.order && event.data.order.name);
  res.sendStatus(200);
});
app.listen(3000);
```

```php PHP theme={null}
<?php
// Lee el cuerpo crudo antes de decodificarlo.
$secret = getenv('HURRY_WEBHOOK_SECRET'); // whsec_...
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_HURRY_SIGNATURE'] ?? '';
preg_match('/t=(\d+)/', $header, $t);
preg_match('/v1=([a-f0-9]+)/', $header, $v1);
if (!$t || !$v1 || abs(time() - (int)$t[1]) > 300) { http_response_code(400); exit; }

$expected = hash_hmac('sha256', $t[1] . '.' . $body, $secret);
if (!hash_equals($expected, $v1[1])) { http_response_code(401); exit; }

$event = json_decode($body, true);
// TODO: guarda $event['id'] para ignorar duplicados y procesa en segundo plano
error_log($event['type'] . ' ' . ($event['data']['order']['name'] ?? ''));
http_response_code(200);
```

```python Python (Flask) theme={null}
# request.get_data() devuelve el cuerpo crudo.
import hmac, hashlib, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['HURRY_WEBHOOK_SECRET'].encode()  # whsec_...

@app.post('/webhooks/hurry')
def hurry():
    header = request.headers.get('X-Hurry-Signature', '')
    parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p)
    t, v1 = parts.get('t'), parts.get('v1')
    if not t or not v1 or abs(time.time() - int(t)) > 300:
        abort(400)
    expected = hmac.new(SECRET, t.encode() + b'.' + request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, v1):
        abort(401)
    event = request.get_json(force=True)
    # TODO: guarda event['id'] para ignorar duplicados y procesa en segundo plano
    print(event['type'], (event['data'].get('order') or {}).get('name'))
    return '', 200
```

> [!WARNING]
>
> Verifica siempre sobre el cuerpo **crudo**. Si tu framework parsea el JSON y lo vuelve a serializar, el orden de las claves o los espacios pueden cambiar y la firma dejará de coincidir.

## Responder, reintentos y duplicados

**Responde `2xx` en menos de 10 segundos** y procesa el evento después, en segundo plano. Cualquier otra respuesta, un timeout o un fallo de red cuentan como entrega fallida. Hurry no sigue redirecciones: una respuesta `3xx` también cuenta como fallo.

Si una entrega falla, Hurry la reintenta con esperas crecientes: el primer intento más **6 reintentos**, repartidos a lo largo de unas 9 horas.

| Intento | Espera desde el anterior |
| :-: | - |
| 1 | Inmediato |
| 2 | 30 segundos |
| 3 | 2 minutos |
| 4 | 10 minutos |
| 5 | 30 minutos |
| 6 | 2 horas |
| 7 | 6 horas |

Cuando se agotan los intentos, la entrega queda como **Fallida** y puedes reenviarla a mano desde **Entregas**. Las entregas de prueba (`ping`) y los reenvíos manuales se intentan una sola vez.

> [!NOTE]
>
> **Un mismo evento puede llegar más de una vez.** Si tu servidor procesa la petición pero la respuesta se pierde, Hurry la reintentará. Guarda el campo `id` (o la cabecera `X-Hurry-Event-Id`) de cada evento procesado e ignora los repetidos.

### Apagado automático

Para no castigar a un receptor caído, Hurry apaga el endpoint en dos casos:

* **10 fallos definitivos seguidos**: el endpoint pasa a *Apagado por fallos* y te avisamos con una notificación en la campana del CRM. Un éxito en cualquier momento reinicia la racha.
* **Respuesta `410 Gone`**: tu servidor dice que la URL ya no existe y el endpoint se apaga al momento.

Al apagarse, las entregas que estaban pendientes se marcan como fallidas y no se encola nada nuevo. Cuando arregles el receptor, vuelve a activarlo con el interruptor de la tarjeta y reenvía desde **Entregas** lo que te interese recuperar.

## Entregas

En **Entregas** ves el historial de todo lo que Hurry ha enviado o intentado enviar, lo último primero, con filtros por endpoint y por estado.

| Estado | Significado |
| - | - |
| **Pendiente** | En cola o esperando el próximo reintento. |
| **Enviando** | Se está llamando a tu servidor ahora mismo. |
| **Entregado** | Tu servidor respondió `2xx`. |
| **Fallido** | Se agotaron los intentos, o el receptor respondió `410`, o era una entrega manual que falló. |

Al pulsar **Ver** se abre el detalle con el JSON exacto que se envió, el código HTTP, los primeros bytes de la respuesta de tu servidor, la duración y el error si lo hubo. **Reenviar** vuelve a mandar el mismo JSON con el mismo `id` de evento como una entrega nueva. El historial se conserva **30 días**.

## Secreto y seguridad

* **Secreto por endpoint.** Cada endpoint tiene su propio secreto `whsec_...`, cifrado en la base de datos. En la lista solo se enseña una pista con los últimos 4 caracteres. Puedes **verlo** o **rotarlo** cuando quieras.
* **Rotación inmediata.** Al rotar, el secreto anterior deja de valer al momento: actualiza tu receptor con el nuevo antes de rotarlo si no quieres perder entregas.
* **Solo HTTPS y hosts públicos.** La URL tiene que ser `https://`, apuntar a un dominio público de Internet y no llevar usuario ni contraseña. Hurry rechaza `localhost`, nombres internos, direcciones privadas o de loopback y la propia plataforma de Hurry, y vuelve a comprobarlo antes de cada envío.
* **Sin datos de tarjeta.** El JSON nunca incluye el número completo de la tarjeta ni el CVC: solo marca, últimos 4 dígitos, caducidad, país y tipo.
* **Solo la cuenta principal.** Los colaboradores no ven la sección, y no se puede modificar mientras se está suplantando a otro usuario.

## Límites

| Concepto | Límite |
| - | - |
| Endpoints por cuenta | 10 |
| Cabeceras personalizadas por endpoint | 5 |
| Longitud de la URL | 1024 caracteres |
| Tiempo de espera por intento | 10 segundos (5 segundos para conectar) |
| Intentos por entrega | 7 (el primero más 6 reintentos) |
| Fallos definitivos seguidos antes de apagar el endpoint | 10 |
| Respuesta del receptor guardada en el detalle | Primeros 1.000 caracteres |
| Conservación del historial de entregas | 30 días |

## Integración con Make, Zapier y n8n

Crea un módulo o nodo de tipo *Webhook* en la herramienta, copia la URL que te da y pégala como URL del endpoint en Hurry. Elige los eventos y pulsa **Probar** para que la herramienta reciba un `ping` con el que mapear los campos.

Si la herramienta no te deja verificar la firma, añade al menos una **cabecera personalizada** con un token que solo conozcáis tú y la herramienta, y compruébala en el primer paso del flujo.

## Gestión desde el MCP de Hurry

Si usas el conector MCP de Hurry con un asistente de IA, los webhooks también se pueden gestionar por conversación con estas herramientas. El secreto nunca se devuelve por el MCP; para verlo o rotarlo hay que usar la pantalla.

| Herramienta | Qué hace |
| - | - |
| `list_webhook_endpoints` | Lista los endpoints con URL, eventos, alcance, estado y contadores. |
| `create_webhook_endpoint` | Da de alta un endpoint con nombre, URL, eventos, tiendas y cabeceras. |
| `update_webhook_endpoint` | Cambia solo lo que se indique de un endpoint existente. |
| `delete_webhook_endpoint` | Borra un endpoint y su historial, previa confirmación. |
| `test_webhook_endpoint` | Manda un `ping` firmado y devuelve el resultado. |
| `list_webhook_deliveries` | Historial de entregas con filtros por endpoint y estado. |
| `redeliver_webhook` | Reenvía una entrega con el mismo id de evento. |

## Preguntas frecuentes

> [accordion] Mi receptor no recibe nada
>
> Pulsa **Probar** en el endpoint: verás el código HTTP que devuelve tu servidor y su respuesta. Comprueba que la URL es `https`, pública y que respondes `2xx`. Si Hurry lo apagó por fallos, vuelve a activarlo con el interruptor.

> [accordion] ¿Qué pasa si mi servidor se cae?
>
> Hurry reintenta hasta 6 veces durante unas 9 horas. Si todos los intentos fallan, la entrega queda como fallida y puedes reenviarla desde **Entregas**. Diez fallos definitivos seguidos apagan el endpoint y te avisamos en la campana.

> [accordion] ¿Puedo recibir los eventos en Make, Zapier o n8n?
>
> Sí: crea un webhook en esa herramienta, pega su URL en Hurry y elige los eventos. Si no puedes verificar la firma, usa al menos una cabecera personalizada con un token que solo conozcáis tú y la herramienta.

> [accordion] ¿Qué datos viajan?
>
> Pedido completo (estado, líneas, importes, descuentos, envío y seguimiento), pago (estado, importe, pasarela, método con marca y últimos 4), cliente (email, nombre, teléfono, dirección), tienda y pasarela. Nunca datos completos de tarjeta.

> [accordion] ¿Por qué order.status no es PAID si el pedido está cobrado?
>
> `order.status` es el estado del pedido en el CRM y solo las tiendas Shopify lo pasan a `PAID`. En el resto de pasarelas la verdad del cobro está en el pago. Usa `order.paid`, `order.paidAt` y `order.paymentStatus`, que valen para todas.

> [accordion] ¿Por qué recibo dos eventos por una venta?
>
> Una venta de checkout dispara `order.paid` y `payment.succeeded` a la vez, con el mismo contenido en `data`. Suscríbete solo a uno de los dos salvo que también quieras los cobros recurrentes, que solo llegan como `payment.succeeded`.

> [accordion] Se me ha filtrado el secreto
>
> Pulsa **Rotar secreto**: el anterior deja de valer al momento. Actualiza tu receptor con el nuevo.

> [accordion] ¿Puedo pausar un endpoint sin borrarlo?
>
> Sí, con el interruptor de la tarjeta. En pausa no se encola ningún evento nuevo; los reintentos que ya estaban en cola se siguen intentando con su calendario normal. Al reactivarlo, los eventos nuevos vuelven a enviarse.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.