> ## 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.

# Events

> The webhook event envelope and the exact payload of every event Ringup sends, generated from the API's own types.

Payment outcomes reach you twice: as the response to your call, and as a signed event posted to
your endpoint. The event is the one that survives the caller hanging up, so it is the record you
reconcile against.

Register an endpoint with [`POST /v1/webhook_endpoints`](/api-reference/create-a-webhook-endpoint).
For signature verification, retries and idempotent handling, see
[Webhooks](/concepts/webhooks).

## The envelope

Every event has the same outer shape. `data` carries the payload for that event type.

<ResponseField name="event_id" type="string">
  Unique per event. Dedupe on this: delivery is at-least-once.
</ResponseField>

<ResponseField name="type" type="checkout.succeeded | checkout.failed | checkout.step_up_required | merchant.connected">
  Which event this is. Branch on it to know what `data` holds.
</ResponseField>

<ResponseField name="environment" type="test | live">
  Which environment produced it, so one endpoint can serve both safely.
</ResponseField>

<ResponseField name="created_at" type="string">
  When Ringup created the event, ISO 8601. Not when it was delivered.
</ResponseField>

<ResponseField name="data" type="object">
  The payload for this event type.
</ResponseField>

<Note>
  Dedupe on `event_id`. Delivery is at-least-once, so the same event can arrive more than once,
  and an endpoint that is not idempotent will double-fulfil an order eventually rather than never.
</Note>

## Event types

| Event                                                     | Fires when                                                                                                                                                                                                                          |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`checkout.succeeded`](#checkout-succeeded)               | A charge against a Checkout Session succeeded. The money moved.                                                                                                                                                                     |
| [`checkout.failed`](#checkout-failed)                     | A charge was declined, or the session expired without payment. `reason` tells you which, and `last_payment_error` carries the same error object the 402 returned.                                                                   |
| [`checkout.step_up_required`](#checkout-step_up_required) | Screening found a positive risk signal on the caller's number, so their saved card was not offered on this call and they were texted a payment link instead. `reasons` says why. Fires once per session, whichever signal found it. |
| [`merchant.connected`](#merchant-connected)               | A merchant finished connecting their payment processor, so their calls can now take payment.                                                                                                                                        |

## checkout.succeeded

A charge against a Checkout Session succeeded. The money moved.

<ResponseField name="checkout_session_id" type="string">
  The session that was paid.
</ResponseField>

<ResponseField name="charge_id" type="string">
  The charge that paid it.
</ResponseField>

<ResponseField name="status" type="pending | succeeded | failed">
  Always `succeeded` on this event; present so one parser handles both outcomes.
</ResponseField>

<ResponseField name="amount_cents" type="number">
  What was actually charged, in the smallest currency unit.
</ResponseField>

<ResponseField name="currency" type="string">
  ISO 4217, uppercase. Matches the session.
</ResponseField>

<ResponseField name="processor_payment_id" type="string | null">
  The processor's own id for the payment, for reconciliation against their dashboard.
</ResponseField>

<ResponseField name="merchant_id" type="string">
  The merchant who was paid.
</ResponseField>

<ResponseField name="order_id" type="string | null">
  The merchant's order number, and the confirmation a caller can quote back to them.
</ResponseField>

<ResponseField name="platform_call_id" type="string | null">
  Your platform's call id, to tie this payment to your own call logs.
</ResponseField>

<ResponseField name="card" type="object | null">
  The card that paid, for a receipt line. Never more than brand and last four.
</ResponseField>

<ResponseField name="wallet" type="apple_pay | google_pay | ">
  Which wallet paid (`apple_pay` | `google_pay`), when the pay page's wallet button did. Null on a card payment. Wallet payments carry `card: null`: a wallet never becomes a saved card, so there is no brand or last four to report.
</ResponseField>

<ResponseField name="metadata" type="object | null">
  Whatever you attached at create\_checkout, echoed back untouched.
</ResponseField>

## checkout.failed

A charge was declined, or the session expired without payment. `reason` tells you which, and `last_payment_error` carries the same error object the 402 returned.

<ResponseField name="checkout_session_id" type="string">
  The session that was not paid.
</ResponseField>

<ResponseField name="charge_id" type="string | null">
  Null when the session expired without any attempt.
</ResponseField>

<ResponseField name="reason" type="declined | expired">
  `declined` fires on a real decline; `expired` when the session lapsed unpaid.
</ResponseField>

<ResponseField name="amount_cents" type="number">
  What we tried to charge, in the smallest currency unit.
</ResponseField>

<ResponseField name="currency" type="string">
  ISO 4217, uppercase. Matches the session.
</ResponseField>

<ResponseField name="merchant_id" type="string">
  The merchant the payment was for.
</ResponseField>

<ResponseField name="order_id" type="string | null">
  The merchant's order number, so you can mark the right order unpaid.
</ResponseField>

<ResponseField name="platform_call_id" type="string | null">
  Your platform's call id, to tie this failure to your own call logs.
</ResponseField>

<ResponseField name="wallet" type="apple_pay | google_pay | ">
  Which wallet the failed attempt came from, mirroring the succeeded payload. Null on a card attempt or an expiry.
</ResponseField>

<ResponseField name="metadata" type="object | null">
  Whatever you attached at create\_checkout, echoed back untouched.
</ResponseField>

<ResponseField name="last_payment_error" type="object | null">
  The SAME error object the 402 returned, minus `request_id`, which belongs to a request and not to an event. Stripe embeds the error this way on `last_payment_error` rather than flattening it, so one parser handles the synchronous and asynchronous paths. Null when the session merely expired: nothing was attempted, so there is no error to report.
</ResponseField>

## checkout.step\_up\_required

Screening found a positive risk signal on the caller's number, so their saved card was not offered on this call and they were texted a payment link instead. `reasons` says why. Fires once per session, whichever signal found it.

<ResponseField name="checkout_session_id" type="string">
  The session whose caller must pay by link instead of with their saved card.
</ResponseField>

<ResponseField name="merchant_id" type="string">
  The merchant the payment was for.
</ResponseField>

<ResponseField name="decision" type="string">
  Always `step_up` on this event. Present so one parser reads it and the session field alike.
</ResponseField>

<ResponseField name="reasons" type="array">
  Why, as stable codes: `ported_recently` when the caller's number changed hands inside the last seven days, `attestation_C` or `attestation_F` when the carrier's signature on the original call was poor or failed. More than one can apply. Codes only: the underlying carrier records stay ours.
</ResponseField>

<ResponseField name="attestation" type="A | B | C | F | ">
  The carrier attestation grade for the original call, when it resolved.
</ResponseField>

<ResponseField name="ported_recently" type="boolean | null">
  True when a recent port was the cause. Null when the lookup did not resolve.
</ResponseField>

<ResponseField name="platform_call_id" type="string | null">
  Your platform's call id, to tie this to your own call logs.
</ResponseField>

<ResponseField name="metadata" type="object | null">
  Whatever you attached at create\_checkout, echoed back untouched.
</ResponseField>

## merchant.connected

A merchant finished connecting their payment processor, so their calls can now take payment.

<ResponseField name="merchant_id" type="string">
  Ringup's id for the merchant that just connected.
</ResponseField>

<ResponseField name="reference_id" type="string | null">
  YOUR id for this merchant, echoed back, so you can join it to your own record (R15d).
</ResponseField>

<ResponseField name="processor" type="string">
  Which processor they connected.
</ResponseField>

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/concepts/webhooks">
    Signatures, retries, and handling events idempotently.
  </Card>

  <Card title="Error codes" icon="list" href="/api-reference/error-codes">
    What `last_payment_error` can carry on a failed checkout.
  </Card>
</CardGroup>
