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

The response includes 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 typeFires when
checkout.session.completedA checkout session moved to COMPLETE. This is the primary signal to fulfill an order.
checkout.session.expiredA session moved to EXPIRED — either auto-expiry or a manual /expire call.
payment.succeededA payment row transitioned to SUCCEEDED. Fires for checkout, invoice, installment, and payment link payments.
payment.failedA charge was declined. Useful for retrying installments or notifying your customer.
payment.refundedFull or partial refund processed.
invoice.paidAn invoice reached amount_due_cents = 0.
invoice.sentInvoice 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

Return 200 quickly. Do the real fulfillment work asynchronously if it takes longer than a second. Deduplicate by 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