Webhooks
Webhook overview
LuniPay pushes signed, durable events to URLs you register. Webhooks are the way your backend learns that a payment succeeded, a dispute opened, or an invoice was paid.
Why webhooks
Payments are asynchronous. A customer might authenticate with 3DS, leave their phone face-down for a minute, come back, and complete. Your browser cannot reliably wait that long. Webhooks decouple fulfillment from the checkout redirect: LuniPay calls you when the state actually changes, and your order-shipping logic runs against that signal.
Registering an endpoint
In Settings → Developer, add a webhook endpoint pointing at your HTTPS URL and pick which events it subscribes to. Endpoints are scoped per livemode — the same URL can be registered once for test and once for live with different signing secrets.
You can also manage endpoints from the API:
curl https://lunipay.io/api/v1/webhook-endpoints \
-H "Authorization: Bearer sk_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/lunipay",
"enabled_events": ["checkout.session.completed", "payment.succeeded"],
"description": "Production order fulfillment"
}'Save the signing secret
signing_secret exactly once. Store it in your environment before you close the terminal. Subsequent GETs never return it.Delivery shape
LuniPay POSTs a JSON body to your URL. Every delivery includes the event object in full — never just an id.
{
"id": "evt_01JRZKD1KMN8RZJX4QV5W7YEFH",
"object": "event",
"livemode": false,
"type": "checkout.session.completed",
"api_version": "2026-04-14",
"data": {
"object": {
"id": "cs_01JRZK8WV3FYMPEQRXJ9HA5NTB",
"object": "checkout_session",
"status": "COMPLETE",
"payment_status": "paid",
"amount_cents": 5000,
"currency": "usd"
}
},
"created": 1713100900
}Event catalog
The types below are the most important events for building a payment integration. The full event list is documented on the Events API page.
| Event type | Fires when |
|---|---|
| checkout.session.completed | A checkout session moved to COMPLETE. This is the primary signal to fulfill an order. |
| checkout.session.expired | A session moved to EXPIRED — either auto-expiry or a manual /expire call. |
| payment.succeeded | A payment row transitioned to SUCCEEDED. Fires for checkout, invoice, installment, and payment link payments. |
| payment.failed | A charge was declined. Useful for retrying installments or notifying your customer. |
| payment.refunded | Full or partial refund processed. |
| invoice.paid | An invoice reached amount_due_cents = 0. |
| invoice.sent | Invoice was delivered to the customer (email + hosted URL active). |
Retries
LuniPay retries failed deliveries with exponential backoff. A delivery is considered successful when your endpoint returns any 2xx within 10 seconds. Non-2xx responses or timeouts trigger the next retry.
- Attempt 1 — immediately, inline with the triggering request.
- Attempts 2–7 — hourly-ish with exponential backoff, spanning ~10.5 hours total.
- After attempt 7 — the delivery is marked
FAILED.
Handler rules of thumb
event.id. Never block on a downstream service that can itself fail.Replaying events
Every delivery attempt is stored in the dashboard. If your handler misbehaves you can re-send any past event from the Settings → Developer → Webhooks page, or re-fetch an event via the events API and POST it yourself.
Next steps
- Verify signatures — the actual crypto, in three languages.
- Accept a payment — end-to-end tutorial with webhook code.