> For the complete documentation index, see [llms.txt](https://savefee.gitbook.io/savefee/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://savefee.gitbook.io/savefee/webhooks/events-and-payloads.md).

# Events and payloads

## Headers on every delivery

```bash
X-SaveFee-Event         order.created
X-SaveFee-Delivery-Id   65f1a2b3c4d5e6f708192a3b
X-SaveFee-Delivery-Sig  fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210
X-SaveFee-Signature     t=1730000000,v1=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Content-Type            application/json
```

*(The values above are illustrative. Real deliveries carry a 24-character hexadecimal delivery id and 64-character hexadecimal digests.)*

| Header                   | Use                                                                                                     |
| ------------------------ | ------------------------------------------------------------------------------------------------------- |
| `X-SaveFee-Event`        | The event type. Branch on this.                                                                         |
| `X-SaveFee-Delivery-Id`  | Unique id for this delivery. Deduplicate on it — a retry reuses the same value.                         |
| `X-SaveFee-Delivery-Sig` | A digest of the delivery id. Like the delivery id, it stays the same across retries.                    |
| `X-SaveFee-Signature`    | The HMAC you verify. It is recomputed for every attempt, so it changes between retries of one delivery. |

{% hint style="info" %}
Read the delivery id from **`X-SaveFee-Delivery-Id`**, including the `-Id` suffix, so your deduplication sees a value on every request. Deduplicate on that header rather than on the signature, which is different on each attempt.
{% endhint %}

Deliveries may carry additional `X-SaveFee-*` headers. Use the ones above and ignore the rest.

## Events

| `X-SaveFee-Event`     | Fired when                                                   |
| --------------------- | ------------------------------------------------------------ |
| `order.created`       | An energy order has been accepted and charged.               |
| `order.fulfilled`     | The energy has been delegated. This is the one that matters. |
| `broadcast.accepted`  | A signed transaction was accepted for processing.            |
| `broadcast.confirmed` | The transaction confirmed on-chain.                          |
| `broadcast.failed`    | The transaction was not delivered. The fee is refunded.      |

## `order.created`

```json
{
  "event": "order.created",
  "orderId": "65f1a2b3c4d5e6f708192a3b",
  "status": "pending",
  "receiver": "T…",
  "resource": "ENERGY",
  "resourceAmount": 50000,
  "remainAmount": 50000,
  "isMatching": true,
  "durationSec": 3600,
  "totalCostSun": 1400000,
  "createdAt": "2026-08-07T03:47:44.439Z"
}
```

This fires at order creation. `status` is `pending`, so it is not yet confirmation that energy was delivered — wait for `order.fulfilled` before you release goods or credit a customer.

## `order.fulfilled`

Same shape, with `status` set to `fulfilled` and `remainAmount` at `0`. **This is the event that means the energy has landed.**

It normally follows `order.created` in well under a minute, but the gap depends on how quickly the order is matched against available energy. Drive your flow off the event itself, not off an expected delay.

## `broadcast.accepted`

```json
{
  "broadcastId": "65f1a2b3c4d5e6f708192a3b",
  "txId": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "pending_energy"
}
```

## `broadcast.confirmed`

Same shape, with `status` set to `confirmed`. **This is the event that means the transfer is on-chain.**

## `broadcast.failed`

```json
{
  "broadcastId": "65f1a2b3c4d5e6f708192a3b",
  "txId": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "status": "expired",
  "failureReason": "…"
}
```

{% hint style="warning" %}
This one event covers both terminal failures. Read `status` to tell them apart: `failed` means the transaction was not delivered, and `expired` means it did not confirm before its expiration. The fee is refunded either way, and neither is a state to keep polling.
{% endhint %}

{% hint style="info" %}
Broadcast payloads identify the event through the `X-SaveFee-Event` header rather than a field in the body. Reading the header works for every event type, so use it consistently.
{% endhint %}

## Ordering

Deliveries are not ordered. Make your handler idempotent and state-based: apply what the payload says rather than assuming a sequence.
