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

# Webhooks

Tendr sends signed webhooks for checkout, refund, gas and hook events. Add endpoints in the dashboard or
with `POST /v1/webhooks/endpoints`. Omit `events` to receive all of them. See
[Webhook signatures](https://docs.usetendr.tech/security/webhook-signatures.md) for verification.

The body is `{ id, type, created, livemode, data }`. `data.external_ref` is your order id. A retry or a
resend keeps the event `id` (also sent as `Tendr-Event-Id`), so deduplicate on it. Failed deliveries are
retried with exponential backoff for 3 days, and you can resend any delivery from the dashboard.

## Headers

Every delivery is a `POST` with these headers:

| Header | Value |
| - | - |
| `Tendr-Signature` | `t=<unix seconds>,v1=<hex>`: HMAC-SHA256 of `<t>.<raw body>` with the endpoint's signing secret. For 24 hours after a secret rotation it carries a second `v1=` signed with the old secret; accept a match on any `v1`. The timestamp is the `t` in this header (there is no separate timestamp header). |
| `Tendr-Event-Id` | The event `id`, the same on every retry and resend. |
| `Content-Type` | `application/json` |
| `User-Agent` | `Tendr-Webhooks/1.0` |

Verify the raw body before parsing it, and reject a `t` more than 5 minutes old. See
[Webhook signatures](https://docs.usetendr.tech/security/webhook-signatures.md).

## Checkout, payment and refund events

| Event | When |
| - | - |
| `checkout.session.expired` | A session expired without a payment. |
| `payment.detected` | A deposit for the session was seen on chain. |
| `payment.processing` | The deposit is being swapped and settled. |
| `payment.succeeded` | The payment settled to your payout destination. Mark the order paid on this event. |
| `payment.refunded` | The payment was refunded (see the `refund.*` events for each return). |
| `payment.late` | A deposit arrived after the session ended. |
| `payment.held` | The payment is held for review. |
| `refund.pending` | A refund was created and waits to be sent. |
| `refund.sent` | A refund was sent on chain. |
| `refund.needs_address` | A refund needs a refund address before it can be sent. |
| `refund.cancelled` | A refund's proposal was rejected by the signers or expired. No money moved. |
| `refund.failed` | A refund failed when it executed on chain. No money moved. |

## Gasless and hook events

| Event | When |
| - | - |
| `gas.budget_low` | Sponsored gas reserved plus spent crossed 80% of the month's budget. Once per calendar month (UTC) per mode. |
| `gas.budget_exhausted` | A payment was not sponsored because the month's budget is used up. Once per month per mode. |
| `hook.succeeded` | Your contract accepted the `onTendrDeposit` or `on_tendr_deposit` call. |
| `hook.failed` | Tendr gave up calling your contract. Sent once per hook call. |

`gas.budget_low` and `gas.budget_exhausted` carry `mode` (`test` or `live`), `month` (`YYYY-MM`),
`budget_usd`, `reserved_usd`, `spent_usd` and `remaining_usd`. `gas.budget_exhausted` also carries
`requested_usd`. See [gas policy](https://docs.usetendr.tech/gasless/gas-policy.md).

`hook.succeeded` and `hook.failed` carry `payment_id`, `session_id`, `external_ref`, `hook_call_id`,
`chain`, `contract`, `attempts` and `tx_hash`. `hook.failed` also carries `error`, the last error. See
[integrator hooks](https://docs.usetendr.tech/integrations/hooks.md).

## Payment link fields

Every checkout, payment and refund event carries `data.payment_link_id` and `data.payer_note` next to
`external_ref`. They are always present:

| Key | Value |
| - | - |
| `payment_link_id` | The id of the [payment link](https://docs.usetendr.tech/payment-links/overview.md) the session was created from, or `null`. |
| `payer_note` | The note the payer left on the link page (plain text, up to 280 characters), or `null`. |

Sessions made from a link's public page have no `external_ref` (it is `null`): match them by
`payment_link_id` instead. Treat `payer_note` as untrusted text and escape it wherever you show it.

## `network_fee` on `payment.succeeded`

`payment.succeeded` includes `data.network_fee` for payments quoted with gasless payments. It is null
for older payments.

```json theme={null}
{
  "paid_by": "shopper",
  "fee_mode": "token",
  "amount": "80000",
  "asset": "nep141:<1Click asset id>",
  "usd": "0.08",
  "method": "eip3009",
  "reason": "shopper_choice"
}
```

* `paid_by`: `merchant` (you sponsored) or `shopper`.
* `fee_mode`: `token` (the shopper paid in the payment token), `native` (the shopper's wallet paid gas
  itself), or null when you sponsored.
* `amount`, `asset`: the token fee in base units and its 1Click asset id. Null unless `fee_mode` is
  `token`.
* `usd`: the quoted fee in USD, a decimal string. When Tendr collects the fee it includes Tendr's
  safety margin.
* `method`: the gasless method (`eip3009`, `permit`, `paymaster`, `kora`, `meta_tx`, `forwarder`), or
  null when the shopper's wallet paid gas itself.
* `reason`: why the shopper paid, when you were expected to sponsor. See
  [why a payment was not sponsored](https://docs.usetendr.tech/gasless/gas-policy.md#why-a-payment-was-not-sponsored).


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