> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ringup.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Ringup posts a signed event to your server when a payment completes, so your system can mark the order paid.

Ringup posts an event to your server when a Checkout Session is paid. This is how your system
learns, out of band, that an order was paid, without waiting on the call. It is the same across
every platform, so you write one handler.

## The envelope

Every event has the same shape:

```jsonc theme={null}
{
  "id": "evt_9f2",               // unique per event; use it to dedupe
  "type": "checkout.completed",  // see the catalog below
  "created": "2026-07-07T18:05:12Z",
  "data": { ... }                // shape depends on type
}
```

## Event catalog

| Type                 | When it fires                                                                                   |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| `checkout.completed` | A Checkout Session was paid (on a saved card, or a first-time caller paid via the texted link). |
| `checkout.failed`    | The charge was declined or errored.                                                             |
| `merchant.connected` | A merchant finished connecting their payment processor to Ringup.                               |

The catalog is deliberately small. A first-time link payment is a `checkout.completed` with
`method: "link"`, not a separate event.

## `checkout.completed`

The one you build against. Its `data` is the completed Checkout Session with its **Payment**,
and it carries everything you need to mark an order paid:

```jsonc theme={null}
"data": {
  "checkout_id": "cs_x9f2",      // the Checkout Session
  "payment_id": "pay_abc",       // the Payment
  "method": "saved_card",        // or "link"
  "amount_cents": 1900,
  "currency": "USD",
  "order_id": "sq_abc",          // the order id you passed, echoed; null if you passed none
  "confirmation": "2PJJZY",
  "caller": "+14155551234",      // the recognized caller's number
  "merchant": "mch_x",
  "card": { "brand": "VISA", "last_4": "5858" },
  "call_id": "..."               // the platform call id, to match back to the call
}
```

Match on the `order_id` you passed (or the `call_id`), store the `payment_id`, and flip your
order to paid. `checkout.failed` carries the same fields plus a `reason`.

## Statuses

A Payment moves through a small set of states:

* **`succeeded`**: the charge settled. This is the only status that means money moved.
* **`failed`**: the charge was declined or errored; nothing was charged.
* **`pending`**: a first-time caller was texted a link and has not paid yet. You see this only
  if you poll a Checkout Session during a call; a pending Payment does not send a
  `checkout.completed` webhook until it settles.

## Getting it right

Three rules cover almost every webhook bug:

1. **Verify the signature.** Each request carries a `Ringup-Signature` header, an HMAC of the
   raw body plus a timestamp. Recompute it with your endpoint's signing secret and reject a
   mismatch or an old timestamp, so nobody can forge an event.
2. **Dedupe on `id`.** Delivery is at-least-once: Ringup retries on any non-2xx response, so
   the same event can arrive more than once. Treat a repeated `id` as a no-op.
3. **Acknowledge fast.** Return `2xx` immediately and do your work asynchronously. Ringup
   retries with backoff and parks a persistently failing endpoint after several attempts.

## Registering an endpoint

Add your endpoint URL in the Ringup dashboard (or via the API). Ringup shows the signing secret
once at creation. You can register more than one endpoint, and test mode delivers to its own.

<Note>
  **Signature scheme.** `Ringup-Signature` is an HMAC-SHA256 over the timestamp and the raw request
  body, signed with your endpoint's secret. When you register an endpoint the dashboard shows the
  exact recipe and a copy-paste verification snippet next to your signing secret.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Testing" icon="flask" href="/concepts/testing">
    Trigger real events against the sandbox and watch them arrive.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Every field of every tool, with a live playground.
  </Card>
</CardGroup>
