> ## 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.

# Outgoing webhooks

> Receive a signed JSON POST in your system every time an order is paid or shipped, a charge fails, a refund is recorded or a dispute comes in.

Outgoing webhooks notify your systems (ERP, Make, n8n, Zapier or your own server) about what happens in Hurry without you having to poll for anything. You register a URL, choose which events you want, and Hurry sends it a `POST` with the full JSON of the order, payment, customer, store and gateway every time something happens.

They're managed from the CRM, under **Account → Webhooks**. Only the main account can see and configure them: collaborators don't have access to this section.

## How it works

**1. Create an endpoint**

Click **New endpoint**, give it a name, enter your receiver's `https` URL and check the events you want to receive. If needed, limit the endpoint to specific stores and add your own headers.

**2. Store the secret**

When you create it, Hurry shows you a secret in the format `whsec_...`. Store it in your receiver: you'll use it to verify the signature of each delivery. You can view it again or rotate it from the endpoint card.

**3. Verify the signature and respond 2xx**

On each delivery, check the `X-Hurry-Signature` header against the raw body and respond with a `2xx` code within 10 seconds. If you don't respond `2xx`, Hurry retries automatically.

**4. Test it**

Click **Test** on the endpoint. Hurry sends a signed `ping` event and shows you the HTTP code and your server's response.

## Events

Each endpoint subscribes to the events you choose. The event code travels in the JSON `type` field and in the `X-Hurry-Event` header.

| Event | When it's sent | On by default |
| - | - | :-: |
| `order.created` | A buyer reaches checkout and leaves their email. The order exists but isn't paid yet. | No |
| `order.paid` | Successful checkout charge. Includes the full order: status, lines, amounts, customer, payment, store and gateway. | Yes |
| `order.shipped` | The order is marked as shipped or given a tracking number. | Yes |
| `cart.abandoned` | The recovery scanner detects an abandoned checkout. | No |
| `payment.succeeded` | Any successful charge, including recurring charges without an order (subscription rebills). | No |
| `payment.failed` | The card, 3DS or gateway declines the charge. Includes the error code and message. | Yes |
| `refund.created` | A refund is recorded (from the CRM, the AI or the gateway dashboard), with its status and amount. | Yes |
| `dispute.opened` | A dispute, chargeback, inquiry or early fraud warning comes in. | Yes |
| `dispute.closed` | A dispute reaches its final state: won, lost or closed. | Yes |
| `ping` | Test sent from the screen or from the MCP. Can't be turned off. | — |

> [!TIP]
>
> A checkout sale triggers `order.paid` **and** `payment.succeeded` at the same time. If you only want sales, subscribe to `order.paid`. If you also want subscription recurring charges, use `payment.succeeded`.

### Store scope

An endpoint can receive events from **all stores** (including recurring charges, which have no store) or **only some**. If you choose specific stores, charges without a store aren't sent to that endpoint.

### Custom headers

You can add up to **5 custom headers** per endpoint, for example `Authorization: Bearer ...` if your receiver requires a token. Names can contain letters, numbers and hyphens. You can't set the headers Hurry defines: `Host`, `Content-Type`, `Content-Length`, `Transfer-Encoding`, `User-Agent` or any starting with `X-Hurry-`.

## Event format

All events share the same envelope. What changes is the content of `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`

Event identifier (`evt_` + 24 characters). It's **the same** across all endpoints, all retries and manual resends of the same occurrence. Use it to ignore duplicates.

**type** `string`

Event code: `order.paid`, `payment.failed`, `refund.created`...

**apiVersion** `string`

JSON format version. Currently `2026-11-01`. Each endpoint is pinned to the version it was created with.

**createdAt** `string`

Event creation date in ISO 8601 with time zone.

**account.id** `string`

Your Hurry account identifier.

**endpoint** `object`

`id` and `name` of the endpoint receiving the event.

**data** `object`

Everything known about the occurrence. Which nodes are filled in depends on the event; those that don't apply come as `null`.

> [!NOTE]
>
> **Amounts and dates.** All amounts are integers in the currency's minor unit (cents: `4980` is 49.80 EUR), with the currency alongside and a readable version in the `*Formatted` fields. Dates are in ISO 8601 with time offset.

### What `data` contains for each event

| Event | `order` | `payment` | `customer` | `shop` | `gateway` | Extra |
| - | :-: | :-: | :-: | :-: | :-: | - |
| `order.created`, `cart.abandoned`, `order.shipped` | Yes | If it exists | If it exists | Yes | Yes | — |
| `order.paid`, `payment.succeeded`, `payment.failed` | If there's an order | Yes | Yes | If there's an order | Yes | — |
| `refund.created` | If there's an order | Yes | Yes | If there's an order | Yes | `refund` |
| `dispute.opened`, `dispute.closed` | If matched | If matched | If matched | If matched | Yes | `dispute` |
| `ping` | — | — | — | — | — | `message`, `endpoint` |

> [accordion] data.order: the order
>
> **id** `string`
>
> Order identifier in Hurry.
>
> **externalId** `string`
>
> External reference of the cart or source platform.
>
> **number** `integer | null`
>
> Sequential order number, if already assigned.
>
> **name** `string`
>
> Readable order name, for example `#1001`.
>
> **status** `string`
>
> Order status in the CRM: `PENDING`, `IN_PROGRESS`, `REQUIRES_ACTION`, `PAID`, `PARTIALLY_REFUNDED`, `REFUNDED` or `CANCELED`. Read the note below before using it to tell whether the order has been paid.
>
> **paid** `boolean`
>
> `true` if the order has been paid. Derived from the payment, it works for all gateways.
>
> **paidAt** `string | null`
>
> Time of the charge.
>
> **paymentStatus** `string | null`
>
> Status of the associated payment: `CREATED`, `REQUIRES_ACTION`, `PROCESSING`, `SUCCEEDED`, `CANCELED` or `FAILED`.
>
> **currency** `string`
>
> Charge currency (ISO 4217).
>
> **amounts** `object`
>
> Charge amounts in minor units: `currency`, `subtotal`, `tax`, `shipping`, `discount`, `upsells`, `total` and `totalFormatted`.
>
> **displayAmounts** `object`
>
> The same amounts in the currency the buyer saw at checkout, plus `fxRate` when there was a conversion.
>
> **discountCode** `string | null`
>
> Discount code applied.
>
> **shippingOption** `string | null`
>
> Name of the chosen shipping option.
>
> **items** `array`
>
> Order lines. Each with `id`, `productId`, `sku`, `name`, `imageUrl`, `quantity`, `unitAmount`, `taxAmount` and `totalAmount`.
>
> **shipping** `object`
>
> Tracking: `shipped`, `shippedAt`, `trackingNumber` and `trackingUrl`.
>
> **checkoutUrl** `string | null`
>
> Hurry checkout URL for this order.
>
> **sourceUrl** `string | null`
>
> Store page where the purchase started.
>
> **crmUrl** `string`
>
> Direct link to the order in the Hurry CRM.
>
> **createdAt / updatedAt** `string`
>
> Creation and last modification dates.
>
> > [!WARNING]
> >
> > `order.status` is the order status in the CRM and **only Shopify stores set it to `PAID`**; on all other gateways it stays at `IN_PROGRESS` even when the charge has gone through. To know whether an order has been paid, use `order.paid`, `order.paidAt` and `order.paymentStatus`, which come from the payment.

> [accordion] data.payment: the payment
>
> **id** `string`
>
> Payment identifier in Hurry.
>
> **status** `string`
>
> `CREATED`, `REQUIRES_ACTION`, `PROCESSING`, `SUCCEEDED`, `CANCELED` or `FAILED`.
>
> **amount / currency / amountFormatted** `integer / string / string`
>
> Amount charged in minor units, its currency and the readable version.
>
> **amountUsd** `integer | null`
>
> Amount converted to USD in minor units, if available.
>
> **psp** `string`
>
> Gateway that processed the charge: `STRIPE`, `STRIPE_EVERVAULT`, `WHOP`, `ONE`, `SUMUP`, `SQUARE`, `MAMO`, `AIRWALLEX`, `SHOPIFY`, `MOR`, `SLASH`, `NEXI`, `NEXI_ALIAS`, `REDSYS` or `REVOLUT`.
>
> **providerPaymentId** `string | null`
>
> Charge identifier in the gateway (for example Stripe's `pi_...`).
>
> **recurring** `boolean`
>
> `true` if it's a recurring charge without an order (rebill).
>
> **batchJobId** `string | null`
>
> Recurring charge batch it belongs to, if applicable.
>
> **email / phone** `string | null`
>
> Buyer contact. The customer's is preferred over the one given by the gateway.
>
> **paymentMethod** `object`
>
> `type`, `brand`, `last4`, `expMonth`, `expYear`, `country` and `funding`. The full card number is never sent.
>
> **error** `object | null`
>
> `code` and `message` of the decline when the charge fails.
>
> **refunds** `array`
>
> Refunds on the payment, in the same format as `data.refund`.
>
> **refundedAmount** `integer`
>
> Sum of successful or pending refunds, in minor units.
>
> **createdAt / updatedAt** `string`
>
> Creation and last modification dates.

> [accordion] data.customer: the customer
>
> `id`, `email`, `name`, `phone`, `address` (`line1`, `line2`, `city`, `state`, `postalCode`, `country`) and `createdAt`. It doesn't include any card data.

> [accordion] data.shop and data.gateway: store and gateway
>
> **shop**: `id`, `name`, `url`, `slug`, `customDomain`, `type` (`EXTERNAL` or `NATIVE`) and `platform` (`SHOPIFY`, `WORDPRESS` or `API`).
>
> **gateway**: `id`, `name` and `psp` of the payment credential used for the charge.

> [accordion] data.refund: the refund (refund.created)
>
> `id`, `providerRefundId`, `amount`, `currency`, `amountFormatted`, `status` (`PENDING`, `SUCCEEDED`, `FAILED` or `CANCELED`), `reason`, `failureReason`, `createdAt` and `updatedAt`.

> [accordion] data.dispute: the dispute (dispute.opened and dispute.closed)
>
> **id / providerDisputeId** `string`
>
> Identifiers in Hurry and in the gateway.
>
> **psp** `string`
>
> Gateway where it was opened.
>
> **type** `string`
>
> `CHARGEBACK`, `INQUIRY` or `EARLY_FRAUD_WARNING`.
>
> **state** `string`
>
> `NEEDS_RESPONSE`, `UNDER_REVIEW`, `WON`, `LOST`, `CHARGE_REFUNDED` or `CLOSED`.
>
> **previousState** `string | null`
>
> Previous state, useful in `dispute.closed` to know where it came from.
>
> **providerStatus / reason / networkReasonCode** `string | null`
>
> Status and reason as reported by the gateway and the card network.
>
> **amount / currency / amountFormatted** `integer / string / string`
>
> Disputed amount.
>
> **customerEmail / providerChargeId** `string | null`
>
> Buyer's email and affected charge in the gateway.
>
> **openedAt / evidenceDueBy / closedAt** `string | null`
>
> Opening date, evidence deadline and closing date.
>
> **fundsWithdrawn** `boolean`
>
> Whether the gateway has already withdrawn the funds.
>
> **crmUrl** `string`
>
> Link to the risk section of the CRM.

> [accordion] ping: the test
>
> `message` with fixed text and `endpoint` with `id`, `name`, `url` and the list of `events` it's subscribed to. Use it to check that your receiver is reachable and that you verify the signature correctly.

### Full example: `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": "WELCOME10",
      "shippingOption": "Shipping 24-48h",
      "items": [
        {
          "id": "3c2b1a90-5d6e-4f70-8192-a3b4c5d6e7f8",
          "productId": "prod_123",
          "sku": "VELA-LAV-200",
          "name": "Lavender scented candle 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.mystore.com/checkout/detail/0b8d3f2a-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "sourceUrl": "https://mystore.com/products/lavender-candle",
      "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": "My store",
      "url": "https://mystore.com",
      "slug": "my-store",
      "customDomain": null,
      "type": "EXTERNAL",
      "platform": "SHOPIFY"
    },
    "gateway": {
      "id": "f1e2d3c4-6c41-4e9b-8f7d-2a1c3e5b7d9f",
      "name": "Main Stripe",
      "psp": "STRIPE"
    }
  }
}
```

## HTTP headers

Each delivery is a `POST` with `Content-Type: application/json` and `User-Agent: Hurry-Webhooks/1.0`. It also includes these headers:

| Header | Content |
| - | - |
| `X-Hurry-Event` | Event code (`order.paid`, `refund.created`...). |
| `X-Hurry-Event-Id` | Event ID. The same across all endpoints and retries of the same occurrence. |
| `X-Hurry-Delivery-Id` | ID of this specific delivery. Changes on each retry. |
| `X-Hurry-Timestamp` | Unix seconds of the send. Included in what's signed. |
| `X-Hurry-Signature` | `t=<timestamp>,v1=<hex HMAC-SHA256 of t + "." + body>`. |
| `X-Hurry-Attempt` | Attempt number, from 1 to 7. |
| `X-Hurry-Version` | JSON format version (`2026-11-01`). |

Any custom headers you've defined on the endpoint are added to these.

## Verify the signature

Each delivery is signed with **HMAC-SHA256** using the endpoint's secret. Verify the signature before trusting the content:

1. Read the **raw** request body, exactly as it arrives, before parsing the JSON.
2. Extract `t` and `v1` from the `X-Hurry-Signature` header.
3. Reject the request if `t` differs by more than **300 seconds** from your current time. This way a captured delivery can't be replayed later.
4. Compute `HMAC-SHA256(secret, t + "." + body)` and convert it to lowercase hexadecimal.
5. Compare the result with `v1` in **constant time**.

```javascript Node.js (Express) theme={null}
// The body must arrive RAW: express.raw, not 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: store event.id to ignore duplicates and process in the background
  console.log(event.type, event.data.order && event.data.order.name);
  res.sendStatus(200);
});
app.listen(3000);
```

```php PHP theme={null}
<?php
// Read the raw body before decoding it.
$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: store $event['id'] to ignore duplicates and process in the background
error_log($event['type'] . ' ' . ($event['data']['order']['name'] ?? ''));
http_response_code(200);
```

```python Python (Flask) theme={null}
# request.get_data() returns the raw body.
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: store event['id'] to ignore duplicates and process in the background
    print(event['type'], (event['data'].get('order') or {}).get('name'))
    return '', 200
```

> [!WARNING]
>
> Always verify against the **raw** body. If your framework parses the JSON and serializes it again, the key order or whitespace can change and the signature will no longer match.

## Responses, retries and duplicates

**Respond `2xx` within 10 seconds** and process the event afterwards, in the background. Any other response, a timeout or a network failure counts as a failed delivery. Hurry doesn't follow redirects: a `3xx` response also counts as a failure.

If a delivery fails, Hurry retries it with increasing waits: the first attempt plus **6 retries**, spread over about 9 hours.

| Attempt | Wait since the previous one |
| :-: | - |
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 6 hours |

When all attempts are used up, the delivery is marked **Failed** and you can resend it manually from **Deliveries**. Test deliveries (`ping`) and manual resends are attempted only once.

> [!NOTE]
>
> **The same event can arrive more than once.** If your server processes the request but the response is lost, Hurry will retry it. Store the `id` field (or the `X-Hurry-Event-Id` header) of each processed event and ignore repeats.

### Automatic shutdown

To avoid hammering a receiver that's down, Hurry turns off the endpoint in two cases:

* **10 consecutive permanent failures**: the endpoint switches to *Turned off due to failures* and we notify you via the CRM bell. A success at any point resets the streak.
* **`410 Gone` response**: your server says the URL no longer exists and the endpoint is turned off immediately.

When it's turned off, pending deliveries are marked as failed and nothing new is queued. Once you fix the receiver, turn it back on with the card toggle and resend from **Deliveries** whatever you want to recover.

## Deliveries

In **Deliveries** you see the history of everything Hurry has sent or tried to send, most recent first, with filters by endpoint and status.

| Status | Meaning |
| - | - |
| **Pending** | Queued or waiting for the next retry. |
| **Sending** | Your server is being called right now. |
| **Delivered** | Your server responded `2xx`. |
| **Failed** | Attempts were used up, the receiver responded `410`, or it was a manual delivery that failed. |

Clicking **View** opens the details with the exact JSON sent, the HTTP code, the first bytes of your server's response, the duration and the error if there was one. **Resend** sends the same JSON again with the same event `id` as a new delivery. History is kept for **30 days**.

## Secret and security

* **Secret per endpoint.** Each endpoint has its own `whsec_...` secret, encrypted in the database. The list only shows a hint with the last 4 characters. You can **view** or **rotate** it whenever you want.
* **Immediate rotation.** When you rotate, the previous secret stops working right away: update your receiver with the new one before rotating if you don't want to lose deliveries.
* **HTTPS and public hosts only.** The URL must be `https://`, point to a public internet domain and not include a username or password. Hurry rejects `localhost`, internal names, private or loopback addresses and the Hurry platform itself, and checks again before each send.
* **No card data.** The JSON never includes the full card number or CVC: only brand, last 4 digits, expiry, country and type.
* **Main account only.** Collaborators don't see the section, and it can't be changed while impersonating another user.

## Limits

| Item | Limit |
| - | - |
| Endpoints per account | 10 |
| Custom headers per endpoint | 5 |
| URL length | 1024 characters |
| Timeout per attempt | 10 seconds (5 seconds to connect) |
| Attempts per delivery | 7 (the first plus 6 retries) |
| Consecutive permanent failures before turning off the endpoint | 10 |
| Receiver response stored in the details | First 1,000 characters |
| Delivery history retention | 30 days |

## Integration with Make, Zapier and n8n

Create a *Webhook* module or node in the tool, copy the URL it gives you and paste it as the endpoint URL in Hurry. Choose the events and click **Test** so the tool receives a `ping` you can use to map the fields.

If the tool doesn't let you verify the signature, add at least one **custom header** with a token only you and the tool know, and check it in the first step of the flow.

## Management from the Hurry MCP

If you use the Hurry MCP connector with an AI assistant, webhooks can also be managed conversationally with these tools. The secret is never returned through the MCP; to view or rotate it you need to use the screen.

| Tool | What it does |
| - | - |
| `list_webhook_endpoints` | Lists endpoints with URL, events, scope, status and counters. |
| `create_webhook_endpoint` | Registers an endpoint with name, URL, events, stores and headers. |
| `update_webhook_endpoint` | Changes only what's specified on an existing endpoint. |
| `delete_webhook_endpoint` | Deletes an endpoint and its history, after confirmation. |
| `test_webhook_endpoint` | Sends a signed `ping` and returns the result. |
| `list_webhook_deliveries` | Delivery history with filters by endpoint and status. |
| `redeliver_webhook` | Resends a delivery with the same event ID. |

## Frequently asked questions

> [accordion] My receiver isn't getting anything
>
> Click **Test** on the endpoint: you'll see the HTTP code your server returns and its response. Check that the URL is `https`, public and that you respond `2xx`. If Hurry turned it off due to failures, turn it back on with the toggle.

> [accordion] What happens if my server goes down?
>
> Hurry retries up to 6 times over about 9 hours. If all attempts fail, the delivery is marked as failed and you can resend it from **Deliveries**. Ten consecutive permanent failures turn off the endpoint and we notify you via the bell.

> [accordion] Can I receive events in Make, Zapier or n8n?
>
> Yes: create a webhook in that tool, paste its URL into Hurry and choose the events. If you can't verify the signature, use at least one custom header with a token only you and the tool know.

> [accordion] What data is sent?
>
> Full order (status, lines, amounts, discounts, shipping and tracking), payment (status, amount, gateway, method with brand and last 4), customer (email, name, phone, address), store and gateway. Never full card data.

> [accordion] Why isn't order.status PAID if the order has been paid?
>
> `order.status` is the order status in the CRM and only Shopify stores set it to `PAID`. On other gateways, the source of truth for the charge is the payment. Use `order.paid`, `order.paidAt` and `order.paymentStatus`, which work for all of them.

> [accordion] Why do I get two events for one sale?
>
> A checkout sale triggers `order.paid` and `payment.succeeded` at the same time, with the same content in `data`. Subscribe to only one of them unless you also want subscription recurring charges, which only arrive as `payment.succeeded`.

> [accordion] My secret has leaked
>
> Click **Rotate secret**: the previous one stops working immediately. Update your receiver with the new one.

> [accordion] Can I pause an endpoint without deleting it?
>
> Yes, with the card toggle. While paused, no new events are queued; retries already in the queue continue on their normal schedule. When you turn it back on, new events are sent again.


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