DevelopersWebhooks
Webhooks
How Fianto tells your server that an order is paid or a subscription changed, and what every delivery looks like.
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.
Scroll sideways to see the whole diagram →
Show as text
- 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.
- Send the payer to that `url`: Fianto's hosted checkout page.
- The payer's wallet signs the transaction. The wallet only signs; it does not send.
- The checkout page hands the signed transaction to Fianto.
- Fianto broadcasts it to Solana, and rebroadcasts it when needed.
- When the transaction is confirmed, Fianto tells the checkout page, which shows the payer success. The order is not `PAID` yet.
- 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
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
A new URL starts as pending. Fianto sends it a signed endpoint.verification request:
{
"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.
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. Read them with GET /v1/events, which keeps events for 90 days.
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
Every event has the same outer shape:
{
"id": "evt_4f6c2a9e1b7d4c3a8e5f0a1b2c3d4e5f",
"type": "order.paid",
"timestamp": "2026-09-28T10:00:00.000Z",
"data": { "…": "…" }
}idisevt_followed by 32 hexadecimal characters, the same value as thewebhook-idheader.timestampis when the event was created. It stays the same on every retry.datadepends ontype. See Event types.
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. The same event can reach you more than once. Record each event
idyou 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
2xxwithin 10 seconds counts as delivered. Anything else is retried, up to 8 attempts over about 28 hours. See 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
Was this page helpful? Tell us