> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://docs.bloy.io/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# Event Delivery

## Request format
BLOY sends a `POST` request with a JSON body and these headers:
| Header | Description |
| ---- |
| `X-Bloy-Topic` | The event topic, for example `points/earned` |
| `X-Bloy-Event-Id` | Unique event id, for example `9f2c7a1b4e8d3c6f0a5b2e91`. Stable across retries |
| `X-Bloy-Shop-Domain` | The `.`myshopify.com domain the event belongs to |
| `X-Bloy-Api-Version` | Webhook API version, currently `2026-01` |
| `X-Bloy-Hmac-Sha256` | HMAC-SHA256 of the raw body, hex encoded, signed with the current secret |
| `X-Bloy-Hmac-Sha256-Previous` | Only present during a secret rotation grace period |
| `X-Bloy-Attempt` | Delivery attempt number, starting at `1` |
| `Content-Type` | `application/json` |

Routing metadata lives in headers, not in the body — the body is not padded with values you already have in the headers.
---
## Body structure
Every payload has the same four top-level fields, whatever the topic:
```
{
  "id": "9f2c7a1b4e8d3c6f0a5b2e91",
  "occurredAt": "2026-08-25T08:14:22.913Z",
  "customer": {
    "id": "66f2a1c9e4b0d8a12f3c9a71",
    "shopifyCustomerId": "7284619203841",
    "email": "mai.nguyen@example.com",
    "pointBalance": 1500,
    "currentTier": { "id": "66d1b4e7c2a9f80315de7742", "name": "Gold" },
    "referralCode": "MAI-4F2A",
    "birthday": "1994-03-18T00:00:00.000Z",
    "createdAt": "2025-11-02T09:41:07.220Z"
  },
  "data": {}
}
```
| Field | Type | Description |
| ---- |
| `id` | string | Unique event id, identical to `X-Bloy-Event-Id` |
| `occurredAt` | string | ISO 8601 timestamp of when the event happened |
| `customer` | object | The customer the event is about. Same shape for every topic |
| `data` | object | Topic-specific fields — see [Event payloads](https://file+.vscode-resource.vscode-cdn.net/home/cuongcx/Desktop/project/bloy/bloy_loyalty/shopify-app-loyalty-api/docs/webhook-api/events.md#event-payloads) |

The `customer` object:
| Field | Type | Description |
| ---- |
| `id` | string | BLOY customer id |
| `shopifyCustomerId` | string | Shopify customer id |
| `email` | string | Customer email |
| `pointBalance` | number | Point balance **after** the event |
| `currentTier` | object | `{ id, name }` of the current VIP tier, or `null` if none |
| `referralCode` | string | The customer's referral code |
| `birthday` | string | ISO 8601 date |
| `createdAt` | string | ISO 8601 date the customer joined the programme |


---
## Responding
Return any `2xx` status code to acknowledge the delivery. Any other status, a connection error, or no answer at all counts as a failure and starts the retry sequence. Redirects are followed automatically, and the status at the end of the redirect chain is what decides success.
BLOY waits **5 seconds** for a response. Return quickly and defer the real work; a slow handler will time out and be retried even though it succeeded.
---
## Automatic retries
A failed delivery is retried up to **6 attempts in total** over roughly 31 minutes:
| Attempt | Sent |
| ---- |
| 1 | Immediately |
| 2 | 1 minute after attempt 1 fails |
| 3 | 2 minutes after attempt 2 fails |
| 4 | 4 minutes after attempt 3 fails |
| 5 | 8 minutes after attempt 4 fails |
| 6 | 16 minutes after attempt 5 fails |

Retries target **only the endpoint that failed**. If you have several subscriptions matching one event and just one endpoint is down, the healthy endpoints are not re-sent to.
After attempt 6 the delivery is marked `failed`
---
## Handle duplicate events
BLOY will not re-send an event to an endpoint that has already acknowledged it, but you should still make your handler **idempotent**. Network partitions and infrastructure failures can, in rare cases, produce a repeat delivery.
Record the `id` of every event you process and ignore ids you have seen before. The `id` is stable across all attempts and replays of the same event.