# Webhooks (/developers/webhooks/overview)

Fianto sends your server a signed HTTP `POST` when something happens to your orders, checkout
sessions and subscriptions. Webhooks are how you learn that an order is paid: the checkout page and
your `success_url` never prove it.

**The steps of a one-time checkout**

1. From your server, create a session with `POST /v1/checkout-sessions`. The first create returns its `url`; creating again for the same open order returns `url: null`, so reissue the link instead.
2. Send the payer to that `url`: Fianto's hosted checkout page.
3. The payer's wallet signs the transaction. The wallet only signs; it does not send.
4. The checkout page hands the signed transaction to Fianto.
5. Fianto broadcasts it to Solana, and rebroadcasts it when needed.
6. When the transaction is confirmed, Fianto tells the checkout page, which shows the payer success. The order is not `PAID` yet.
7. When it is finalized, the order becomes `PAID`, the payment `SUCCEEDED`, and Fianto sends `order.paid` and `checkout.session.completed` to your webhook endpoint — only at finalized.

## One endpoint per application [#one-endpoint-per-application]

Each application has one webhook endpoint, and it receives the events of that application's
checkout sessions, orders and subscriptions. You set its URL in the dashboard, in the application's
webhook settings. Setting it needs your password and a two-factor code.

The URL must:

* use `https`;
* carry no username, password or `#fragment`;
* use port 443, or a port of 1024 or above;
* point to a public IP address. Fianto checks this again on every attempt;
* be at most 2,048 characters.

## The URL must pass a verification challenge [#the-url-must-pass-a-verification-challenge]

A new URL starts as pending. Fianto sends it a signed `endpoint.verification` request:

```json
{
  "type": "endpoint.verification",
  "timestamp": "2026-09-28T10:00:00.000Z",
  "data": { "challenge": "…" }
}
```

Your endpoint must answer within 5 seconds with HTTP `200` and the JSON body
`{"challenge":"<the value you received>"}`. Any other status fails, and redirects are not followed.
A challenge lives 15 minutes. If the check fails, the dashboard shows why and a "Verify again"
button. You can start 10 checks per hour per endpoint and 30 per hour across your account; past
that, Fianto refuses with `too_many_probes`.

While a new URL is pending, the URL verified before it keeps receiving events until the new one
passes. The SDK's webhook handlers answer the challenge for you, once they have the right signing
secret. See [Verify webhooks](/developers/webhooks/verify).

> **Events sent before a URL is first verified are never delivered:**
>
> Events published before your application's URL is first verified are stored but never delivered,
> even after you verify one later. A URL that was verified and then suspended holds its events
> instead; see [Retries and delivery](/developers/webhooks/retries-and-delivery). Read them with `GET /v1/events`, which keeps events for 90 days.

## Standard Webhooks signatures [#standard-webhooks-signatures]

Deliveries follow the Standard Webhooks format, so any Standard Webhooks library can verify them.
Every request carries three headers:

| Header              | Value                                                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `webhook-id`        | The event id, such as `evt_…`. The same on every retry and redelivery of that event                                                                                            |
| `webhook-timestamp` | When this attempt was sent, in Unix seconds                                                                                                                                    |
| `webhook-signature` | `v1,` then the base64 HMAC-SHA256 of `id.timestamp.body`. The key is the base64-decoded part of your signing secret after `whsec_`. Several signatures are separated by spaces |

While a secret roll's grace period runs, `webhook-signature` carries two signatures, one for each
valid secret. Rejecting stale timestamps is your receiver's job: the SDK refuses a timestamp more
than 300 seconds from your clock by default, and Fianto's servers apply no tolerance of their own.

## The event envelope [#the-event-envelope]

Every event has the same outer shape:

```json
{
  "id": "evt_4f6c2a9e1b7d4c3a8e5f0a1b2c3d4e5f",
  "type": "order.paid",
  "timestamp": "2026-09-28T10:00:00.000Z",
  "data": { "…": "…" }
}
```

* `id` is `evt_` followed by 32 hexadecimal characters, the same value as the `webhook-id` header.
* `timestamp` is when the event was created. It stays the same on every retry.
* `data` depends on `type`. See [Event types](/developers/webhooks/events).

`GET /v1/events` and `GET /v1/events/{id}` return the same envelope with `object: "event"` added.

There are 14 event types, plus `endpoint.verification`:

| Group             | Types                                                                                                                                                                                          |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Checkout sessions | `checkout.session.completed`, `checkout.session.expired`, `checkout.session.canceled`                                                                                                          |
| Orders            | `order.paid`, `order.expired`, `order.duplicate_payment`                                                                                                                                       |
| Subscriptions     | `subscription.created`, `subscription.renewed`, `subscription.payment_failed`, `subscription.past_due`, `subscription.cancel_scheduled`, `subscription.cancel_withdrawn`, `subscription.ended` |
| Testing           | `test.event`                                                                                                                                                                                   |

## At least once, in no particular order [#at-least-once-in-no-particular-order]

* **At least once.** The same event can reach you more than once. Record each event `id` you have
  handled in the same database transaction as the work it triggers, and skip ids you have already
  seen.
* **Unordered.** A later event can arrive before an earlier one. When the current state matters,
  read the order or subscription back from the API instead of trusting arrival order.
* **Answer fast.** Any `2xx` within 10 seconds counts as delivered. Anything else is retried, up to
  8 attempts over about 28 hours. See [Retries and delivery](/developers/webhooks/retries-and-delivery).

> **Fulfil from a verified webhook, not from the browser:**
>
> The checkout page shows the payer success once the payment is confirmed, but Fianto sends
> `order.paid` only when it is finalized. A payer reaching your `success_url` does not prove that you
> were paid.

## See also [#see-also]

- [Verify webhooks](/developers/webhooks/verify): Check signatures with the SDK or by hand.

- [Event types](/developers/webhooks/events): When each event fires, with an example payload.

- [Retries and delivery](/developers/webhooks/retries-and-delivery): Retry waits, suspension, held deliveries and redelivery.

- [Testing](/developers/testing): Send your route signed samples and a real test.event.