DevelopersWebhooks

Webhooks

How Fianto tells your server that an order is paid or a subscription changed, and what every delivery looks like.

3 min read

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
The steps of a one-time checkout, from your server to Solana and backYour serverFianto APICheckout pagePayer's walletSolanacreate session1send payer to url2payer signs3signed transaction4broadcast5confirmedpayer sees success6finalizedorder PAIDwebhooks7

Scroll sideways to see the whole diagram →

Show as text
  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

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:

HeaderValue
webhook-idThe event id, such as evt_…. The same on every retry and redelivery of that event
webhook-timestampWhen this attempt was sent, in Unix seconds
webhook-signaturev1, 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": { "…": "…" }
}
  • 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.

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

There are 14 event types, plus endpoint.verification:

GroupTypes
Checkout sessionscheckout.session.completed, checkout.session.expired, checkout.session.canceled
Ordersorder.paid, order.expired, order.duplicate_payment
Subscriptionssubscription.created, subscription.renewed, subscription.payment_failed, subscription.past_due, subscription.cancel_scheduled, subscription.cancel_withdrawn, subscription.ended
Testingtest.event

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.

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

On this page