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

# Webhook deliveries

> Receive outbound POST events from UGiftMe — headers, payloads, signature verification, retries, and testing.

# Webhook deliveries

When you [register a webhook subscription](/docs/business-api#webhooks-b2b), UGiftMe **POSTs JSON** to your HTTPS URL whenever a subscribed event occurs. This page documents those **inbound deliveries to your server** — not the subscription CRUD endpoints.

## Delivery contract

| Item        | Detail                                                                        |
| ----------- | ----------------------------------------------------------------------------- |
| **Method**  | `POST`                                                                        |
| **URL**     | The `url` you registered on the subscription                                  |
| **Body**    | `Content-Type: application/json` — event-specific object (see below)          |
| **Success** | Your endpoint returns **HTTP 2xx** within **5 seconds** (production)          |
| **Failure** | Non-2xx, timeout, or network error triggers retries (see [Retries](#retries)) |

Production deliveries use a **5 s** timeout. Manual test deliveries (`POST /webhooks/test`) use **10 s** and include `X-Webhook-Delivery: test`.

## Headers

| Header                | Always    | Description                                                                           |
| --------------------- | --------- | ------------------------------------------------------------------------------------- |
| `Content-Type`        | Yes       | `application/json`                                                                    |
| `X-Webhook-Event`     | Yes       | Event name, e.g. `order.succeeded`                                                    |
| `X-Webhook-Signature` | Yes       | HMAC-SHA256 hex digest of the JSON body (see [Verify signatures](#verify-signatures)) |
| `X-Webhook-Delivery`  | Test only | `test` when sent via `POST /webhooks/test`                                            |

Match **`X-Webhook-Event`** to the payload shape you parse. The event name is **not** repeated inside most order payloads (except `webhook.test`, which includes `type`).

## Verify signatures

When you create a webhook, the API returns a **secret once**. Store it securely. UGiftMe signs each delivery as:

```text theme={null}
HMAC-SHA256(secret, JSON.stringify(payload)) → hex → X-Webhook-Signature
```

**Important:** Verification must use the **exact JSON bytes** UGiftMe sent. Prefer reading the **raw request body** before parsing JSON. Re-serializing a parsed object can change key order or whitespace and break verification.

### Node.js example

```javascript theme={null}
import crypto from 'crypto';

function verifyWebhookSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signatureHeader, 'utf8'),
      Buffer.from(expected, 'utf8')
    );
  } catch {
    return false;
  }
}

// Express: use express.raw({ type: 'application/json' }) on your webhook route,
// or middleware that captures rawBody before JSON parsing.
```

Reject requests with invalid or missing signatures before processing the event.

## Retries

Failed deliveries (non-2xx, timeout, or connection error) are retried up to **5 attempts** total, with backoff approximately:

| Attempt | Delay before retry |
| ------- | ------------------ |
| 1 → 2   | 1 minute           |
| 2 → 3   | 5 minutes          |
| 3 → 4   | 15 minutes         |
| 4 → 5   | 1 hour             |
| 5 → 6   | 4 hours            |

After the final attempt, the delivery is marked failed. **`deliveryStats`** on your subscription (visible via `GET /webhooks`) tracks success and failure counts.

Design your handler to be **idempotent**: the same event may be delivered more than once if a retry occurs after your server already processed the first attempt but returned a non-2xx or timed out.

## Order lifecycle (async)

When async order processing is enabled, subscribed **`order.*`** events typically fire in sequence:

```text theme={null}
order.queued → order.processing → order.succeeded | order.failed
```

**Synchronous** order creation (`201 Created`) does **not** emit `order.queued` / `order.processing` / `order.succeeded` / `order.failed`. It may still emit **`wallet.updated`** when the wallet balance changes.

Poll **`GET /orders/order-requests/{id}`** if you need full request state in addition to webhooks.

## Event payloads

### `order.queued`

Emitted when an async order request is accepted (`202`).

**Single order:**

```json theme={null}
{
  "orderRequestId": "65e4f5b6c7d8e90123456789",
  "status": "queued",
  "type": "single",
  "timestamp": "2025-04-11T08:00:00.000Z"
}
```

**Bulk order** — includes `orderCount`:

```json theme={null}
{
  "orderRequestId": "65e4f5b6c7d8e90123456789",
  "status": "queued",
  "type": "bulk",
  "orderCount": 25,
  "timestamp": "2025-04-11T08:00:00.000Z"
}
```

### `order.processing`

Worker picked up the order request:

```json theme={null}
{
  "orderRequestId": "65e4f5b6c7d8e90123456789",
  "status": "processing",
  "type": "single",
  "timestamp": "2025-04-11T08:00:05.000Z"
}
```

`type` is `"single"` or `"bulk"`.

### `order.succeeded`

Fulfillment completed. **`result`** shape depends on `type`:

**Single** — one created order id:

```json theme={null}
{
  "orderRequestId": "65e4f5b6c7d8e90123456789",
  "status": "succeeded",
  "type": "single",
  "result": {
    "orderId": "65f5a6b7c8d9e01234567890"
  },
  "timestamp": "2025-04-11T08:00:12.000Z"
}
```

**Bulk** — summary of created orders:

```json theme={null}
{
  "orderRequestId": "65e4f5b6c7d8e90123456789",
  "status": "succeeded",
  "type": "bulk",
  "result": {
    "orderIds": ["65f5a6b7c8d9e01234567890", "65f5a6b7c8d9e01234567891"],
    "totalOrders": 2,
    "totalValue": 100,
    "currency": "GBP"
  },
  "timestamp": "2025-04-11T08:00:30.000Z"
}
```

Use **`GET /orders/{orderId}`** for full order details; the webhook carries ids and summary only.

### `order.failed`

Processing failed after retries exhausted:

```json theme={null}
{
  "orderRequestId": "65e4f5b6c7d8e90123456789",
  "status": "failed",
  "error": "Insufficient available balance. Required: 50, Available: 10 GBP",
  "timestamp": "2025-04-11T08:01:00.000Z"
}
```

### `wallet.updated`

Balance changed on your business wallet (orders, top-ups, holds, etc.).

```json theme={null}
{
  "userId": "507f1f77bcf86cd799439011",
  "currency": "GBP",
  "balance": 950.5,
  "availableBalance": 900.5,
  "source": "api_order_async",
  "timestamp": "2025-04-11T08:00:12.000Z",
  "transaction": {
    "type": "debit",
    "amount": 49.5,
    "orderId": "65f5a6b7c8d9e01234567890",
    "details": "Purchase of TESCO-GBP-50 from Tesco"
  }
}
```

| Field              | Notes                                                               |
| ------------------ | ------------------------------------------------------------------- |
| `userId`           | Business user id (same as your integration account)                 |
| `availableBalance` | `balance` minus active wallet holds                                 |
| `source`           | e.g. `api_order_sync`, `api_order_async`, or other internal sources |
| `transaction`      | Optional; present when the update is tied to a wallet transaction   |

### `product.updated`

Full **product catalog document** as stored internally (same fields you see from the products API). Shape varies by product; treat as a catalog record update notification and refresh via **`GET /products`** or search as needed.

### `webhook.test` (test deliveries only)

Not subscribable via `events`. Sent only through **`POST /webhooks/test`**:

```json theme={null}
{
  "type": "webhook.test",
  "sentAt": "2025-04-11T08:15:00.000Z",
  "data": {
    "message": "This is a test delivery from UGiftMe. Your endpoint should return HTTP 2xx within 10 seconds.",
    "exampleOrderId": "ord_example_123"
  }
}
```

Header **`X-Webhook-Event`** is `webhook.test`. Header **`X-Webhook-Delivery`** is `test`.

## Test your endpoint

Use the Business API (with **`X-API-Key`**) after registering a webhook:

| Method   | Path                                    | Purpose                                                                                    |
| -------- | --------------------------------------- | ------------------------------------------------------------------------------------------ |
| **GET**  | `/api/v1/business/webhooks/test-sample` | Returns `{ event, payload }` sample for `webhook.test` (fresh `sentAt` when actually sent) |
| **POST** | `/api/v1/business/webhooks/test`        | Body `{ "webhookId": "<id>" }` — POSTs a signed test payload to that subscription's URL    |

**POST /test** response:

```json theme={null}
{
  "ok": true,
  "statusCode": 200,
  "payload": { "type": "webhook.test", "sentAt": "...", "data": { ... } }
}
```

On failure, `ok` is `false` and `errorMessage` describes the problem (e.g. non-2xx from your URL).

## Related

* [API Guide — webhook subscriptions](/docs/business-api#webhooks-b2b)
* **UGiftMe API Reference** — subscription CRUD, test endpoints, and outbound event schemas
