Skip to content
ripper docs Open the console

Webhooks

Webhooks tell your server what happened without it polling. Register an endpoint, and read the deliveries when you need to see what was sent and what your endpoint answered.

An endpoint names the events it wants. The vocabulary below is closed: a filter naming anything else is refused when you register it, rather than accepted and never delivered to. Matching is exact, so a capitalised spelling is refused too.

The events we send #

Every delivery carries an event id beginning evt_ and declares API version 2026-09-01.

event when it is sent what it means
payment.captured Every capture, whether it was an immediate sale or the later capture of a payment you had held. The money has been taken.
payment.held A payment is authorised without being captured. The card was approved and the amount is reserved. Nothing is taken until the hold is decided — by you, or by ripper on your rules.
payment.voided A held payment's authorisation is released instead of captured. A hold was released, and the shopper's reserved amount goes back.
payment.declined An attempt ends in a decline. The card was refused. Nothing was taken.
payment.failed An attempt ends in an error rather than a decline. Something went wrong and the payment did not complete. Nothing was taken.

payment.captured. One word covers both, so you do not have to reconcile two events that mean the same thing.

payment.declined. A decline is never folded into a failure. Filtering on declines asks a commercial question; filtering on failures asks an operational one.

Retired event types #

payment.succeeded is retired. A capture became one word whether it was immediate or followed a hold. Registering a filter for it now fails, and the refusal names payment.captured as the word that replaced it. Historical delivery rows keep the word they were sent with, so a delivery log may show payment.succeeded on older rows.

What your endpoint receives #

Every delivery is a POST with a JSON body in this envelope. Answer 2xx once you have stored it; anything else is retried. Treat a delivery you have already seen as a duplicate rather than an error — the same event may arrive more than once.

field type always sent what it is
id string yes The event's id. The same event to the same endpoint always has the same id.
type string yes Which event this is. The same five names your endpoint filter uses.
created string yes When ripper made this event, not when it was delivered. A retry of the same event keeps the original time.
api_version string yes The shape of this body. It changes only when the shape does, so you can branch on it if you keep old handlers running.
merchant_id string yes Your merchant account id (m_ followed by 24 hexadecimal characters).
sequence integer yes 1 for a payment's outcome; 2 for the later decision on a held payment.
data object yes The payment this event is about.

The data block #

What the event is about.

field type always sent what it is
payment_id string yes The payment. Use it to read the payment back, and as the key to make your handler idempotent.
checkout_session_id string yes The session this payment came from.
amount integer yes What was charged, in the currency’s minor unit.
currency string yes A three-letter ISO 4217 code, for example GBP.
status string yes The payment’s status at the moment this event was made.
mode string yes test or live. A test payment moved no money.
decline object or null yes Why the payment was declined, or null when it was not.
metadata object yes The metadata you sent when you created the session, keys exactly as you sent them.
amount_verified boolean yes Whether ripper could check the amount against your price list before charging it. When it could not, an approved payment is held for you to review rather than captured.
capture_state string or null yes Where the money is: captured, authorised and waiting, or voided.
items array or null yes The basket, or null when the session carried no lines.
basket_total integer or null yes What the lines add up to, in minor units, tax inclusive. null when there are no lines.
tax_total integer or null yes The tax across the lines, in minor units. null when no line carries a tax_amount.
customer object yes The shopper, as you or the checkout supplied them.
hold object or null yes Why this payment is on hold and what was decided, or null when it was never held.
order_reference string or null yes Your own reference for this order, or null when none was sent.

Checking a delivery really came from ripper #

parameter in required what it is
Rip-Signature header yes t=<unix seconds>,v1=<hex HMAC-SHA256>. To verify: take the raw request body exactly as received, compute HMAC-SHA256 over <t>.<raw body> with the endpoint's signing secret (the whole string, prefix included, as UTF-8), hex-encode it in lower case, and compare it with v1 in constant time. Reject a t that is too far from your clock (five minutes is a sensible tolerance) so a captured request cannot be replayed later.
Rip-Idempotency-Key header yes The same value for every attempt and replay of one event to one endpoint, and the row's event_id in the delivery list. Use it to ignore an event you have already handled.

An example payment.captured delivery #

{
  "id": "evt_fedcba9876543210fedcba98",
  "type": "payment.captured",
  "created": "2026-09-16T10:17:42.0000000+00:00",
  "api_version": "2026-09-01",
  "merchant_id": "m_your_merchant_id",
  "sequence": 1,
  "data": {
    "payment_id": "pay_89abcdef0123456789abcdef",
    "checkout_session_id": "cs_0123456789abcdef01234567",
    "amount": 2500,
    "currency": "GBP",
    "status": "captured",
    "mode": "test",
    "decline": null,
    "metadata": {
      "order_id": "1042"
    },
    "amount_verified": true,
    "capture_state": "captured",
    "customer": {
      "email": "shopper@example.com",
      "name": "Sam Shopper",
      "account_created": null,
      "account_id": "your-customer-4821",
      "metadata": {
        "tier": "gold"
      }
    },
    "hold": null,
    "order_reference": "ORDER-1042",
    "items": [
      {
        "kind": "product",
        "sku": "SKU-1",
        "name": "Merino scarf",
        "quantity": 1,
        "unit_amount": 2000,
        "tax_amount": 333,
        "tax_rate": 2000,
        "type": "apparel"
      },
      {
        "kind": "shipping",
        "sku": null,
        "name": "Next-day delivery",
        "quantity": 1,
        "unit_amount": 500,
        "tax_amount": 83,
        "tax_rate": 2000,
        "type": null
      }
    ],
    "basket_total": 2500,
    "tax_total": 416
  }
}

An example payment.held delivery #

{
  "id": "evt_fedcba9876543210fedcba98",
  "type": "payment.held",
  "created": "2026-09-16T10:17:42.0000000+00:00",
  "api_version": "2026-09-01",
  "merchant_id": "m_your_merchant_id",
  "sequence": 1,
  "data": {
    "payment_id": "pay_89abcdef0123456789abcdef",
    "checkout_session_id": "cs_0123456789abcdef01234567",
    "amount": 2500,
    "currency": "GBP",
    "status": "held",
    "mode": "test",
    "decline": null,
    "metadata": {},
    "amount_verified": false,
    "capture_state": "authorised",
    "customer": {
      "email": "shopper@example.com",
      "name": null,
      "account_created": null
    },
    "hold": {
      "status": "pending_review",
      "reason": "no_price_list",
      "detail": null,
      "deadline": "2026-09-21T10:17:42.0000000+00:00",
      "decided_by": null,
      "decided_at": null,
      "operation_refusal": null
    },
    "order_reference": null,
    "items": null,
    "basket_total": null,
    "tax_total": null
  }
}

An example payment.voided delivery #

{
  "id": "evt_ab1234567890abcdef123456",
  "type": "payment.voided",
  "created": "2026-09-16T10:19:03.0000000+00:00",
  "api_version": "2026-09-01",
  "merchant_id": "m_your_merchant_id",
  "sequence": 2,
  "data": {
    "payment_id": "pay_89abcdef0123456789abcdef",
    "checkout_session_id": "cs_0123456789abcdef01234567",
    "amount": 2500,
    "currency": "GBP",
    "status": "voided",
    "mode": "test",
    "decline": null,
    "metadata": {
      "order_id": "1042"
    },
    "amount_verified": true,
    "capture_state": "released",
    "customer": {
      "email": "shopper@example.com",
      "name": "Sam Shopper",
      "account_created": null,
      "account_id": "your-customer-4821",
      "metadata": {
        "tier": "gold"
      }
    },
    "hold": {
      "status": "released",
      "reason": "no_price_list",
      "detail": null,
      "deadline": "2026-09-21T10:17:42.0000000+00:00",
      "decided_by": "merchant",
      "decided_at": "2026-09-16T10:19:03.0000000+00:00",
      "operation_refusal": null
    },
    "order_reference": "ORDER-1042",
    "items": null,
    "basket_total": null,
    "tax_total": null
  }
}

An example payment.declined delivery #

{
  "id": "evt_cd1234567890abcdef123456",
  "type": "payment.declined",
  "created": "2026-09-16T10:19:03.0000000+00:00",
  "api_version": "2026-09-01",
  "merchant_id": "m_your_merchant_id",
  "sequence": 1,
  "data": {
    "payment_id": "pay_89abcdef0123456789abcdef",
    "checkout_session_id": "cs_0123456789abcdef01234567",
    "amount": 2500,
    "currency": "GBP",
    "status": "declined",
    "mode": "test",
    "decline": {
      "code": "card_declined",
      "category": "hard",
      "reason": "Do not honour"
    },
    "metadata": {
      "order_id": "1042"
    },
    "amount_verified": true,
    "capture_state": null,
    "customer": {
      "email": "shopper@example.com",
      "name": "Sam Shopper",
      "account_created": null,
      "account_id": "your-customer-4821",
      "metadata": {
        "tier": "gold"
      }
    },
    "hold": null,
    "order_reference": "ORDER-1042",
    "items": null,
    "basket_total": null,
    "tax_total": null
  }
}

An example payment.failed delivery #

{
  "id": "evt_ef1234567890abcdef123456",
  "type": "payment.failed",
  "created": "2026-09-16T10:19:03.0000000+00:00",
  "api_version": "2026-09-01",
  "merchant_id": "m_your_merchant_id",
  "sequence": 1,
  "data": {
    "payment_id": "pay_89abcdef0123456789abcdef",
    "checkout_session_id": "cs_0123456789abcdef01234567",
    "amount": 2500,
    "currency": "GBP",
    "status": "failed",
    "mode": "test",
    "decline": {
      "code": "provider_error",
      "category": "provider_error",
      "reason": "The acquirer did not answer in time"
    },
    "metadata": {
      "order_id": "1042"
    },
    "amount_verified": true,
    "capture_state": null,
    "customer": {
      "email": "shopper@example.com",
      "name": "Sam Shopper",
      "account_created": null,
      "account_id": "your-customer-4821",
      "metadata": {
        "tier": "gold"
      }
    },
    "hold": null,
    "order_reference": "ORDER-1042",
    "items": null,
    "basket_total": null,
    "tax_total": null
  }
}

Webhook deliveries #

GET /v1/webhooks/deliveries #

List webhook deliveries

Authentication. Your secret key, in X-Ripper-Api-Key. Server only.

Returns delivery attempts to your endpoints, newest first, a page at a time. Pass next_cursor back as after for the next page, with the same filters. Each row is one attempt; a delivery that was retried has one row per attempt. Rows are kept for 30 days.

Requires a secret key with the manage_webhooks permission.

parameter in required what it is
limit query no Rows per page, 1 to 100. Defaults to 20.
after query no The next_cursor from the previous page.
status query no Only rows with this delivery status.
event_type query no Only rows for this delivery type, spelled as the row's event_type (for example webhook.payment.captured).

200 — One page of delivery rows.

{
  "data": [
    {
      "delivery_id": "0123456789abcdef0123456789abcdef",
      "replay_of": null,
      "merchant_id": "m_your_merchant_id",
      "event_id": "evt:pay_89abcdef0123456789abcdef#payment.captured#whep_0123456789abcdef01234567",
      "event_type": "webhook.payment.captured",
      "attempt_number": 1,
      "status": "succeeded",
      "occurred_at": "2026-09-16T10:17:43.0000000+00:00",
      "endpoint_url": "https://shop.example.com/webhooks/ripper",
      "response_status_code": 200,
      "response_body": "ok",
      "next_retry_at": null,
      "replayable": false,
      "replay_expires_at": "2026-10-16T10:17:42.0000000+00:00"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Fields in the response

field type always sent what it is
data array yes This page of deliveries, newest first.
has_more boolean yes Whether there are older deliveries beyond this page.
next_cursor string or null yes Pass this as cursor to get the next page, or null when this is the last.

data fields

field type always sent what it is
delivery_id string yes The delivery this attempt belongs to. Replay by this id.
replay_of string or null yes The delivery this one replays.
merchant_id string yes Your merchant account.
event_id string yes The value sent in the delivery's Rip-Idempotency-Key header.
event_type string yes webhook. followed by the event type, for example webhook.payment.captured.
attempt_number integer yes 1 for the first attempt. 0 marks a replay that was accepted and not yet attempted.
status string yes How this attempt ended.
occurred_at string yes When this attempt was made.
endpoint_url string or null yes Where it was sent.
response_status_code integer or null yes What your endpoint answered, or null if it never answered.
response_body string or null yes The first 1,024 characters of your endpoint's answer.
next_retry_at string or null yes When ripper will try again, or null when it will not.
replayable boolean yes Whether you can ask for this delivery to be sent again.
replay_expires_at string or null yes After this, the delivery can no longer be replayed.

When it refuses

Operation id listWebhookDeliveries, for code generators.

POST /v1/webhooks/deliveries/{delivery_id}/replay #

Replay a webhook delivery

Authentication. Your secret key, in X-Ripper-Api-Key. Server only.

Queues a failed delivery to be sent again. Only a delivery whose status is dead_lettered can be replayed (its row shows replayable: true), within 30 days of the original delivery. The replay is a new delivery with its own delivery_id. Send no body.

Replays are limited to 10 a minute per account, whatever your plan.

Requires a secret key with the manage_webhooks permission.

parameter in required what it is
delivery_id path yes The delivery_id from a delivery row.
Rip-Idempotency-Key header no Optional but recommended. A retry with the same key returns the first response. Without a key the response carries Rip-Idempotency-Warning. Idempotency-Key is accepted as well; sending two different keys is refused.

202 — The replay was queued.

{
  "delivery_id": "89abcdef0123456789abcdef01234567",
  "replay_of": "0123456789abcdef0123456789abcdef"
}

Fields in the response

field type always sent what it is
delivery_id string yes The new delivery.
replay_of string yes The delivery you replayed.

When it refuses

Operation id replayWebhookDelivery, for code generators.

Webhook endpoints #

POST /v1/webhooks/endpoints #

Create a webhook endpoint

Authentication. Your secret key, in X-Ripper-Api-Key. Server only.

Registers an https URL that receives payment webhooks. The response is the only time the signing_secret is shown: store it where your webhook handler can read it.

Leave event_filter out to receive every payment event. A filter names the event types to receive, from the list this route accepts.

Requires a secret key with the manage_webhooks permission.

parameter in required what it is
Rip-Idempotency-Key header no Optional but recommended. A retry with the same key returns the first response. Without a key the response carries Rip-Idempotency-Warning. Idempotency-Key is accepted as well; sending two different keys is refused.

The body you send

{
  "url": "https://shop.example.com/webhooks/ripper"
}

Fields in the body you send

field type always sent what it is
url string yes An absolute https URL that is publicly reachable.
event_filter array no The event types to receive, from the five under webhooks. Leave it out, or send an empty list, to receive every payment event.

201 — The endpoint was created, or a retry with the same idempotency key replayed it.

{
  "id": "whep_0123456789abcdef01234567",
  "url": "https://shop.example.com/webhooks/ripper",
  "event_filter": [],
  "status": "active",
  "signing_secret": "rip_whsec_EXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXAMPLEEXA",
  "created_at": "2026-09-16T10:00:00.0000000+00:00"
}

Fields in the response

field type always sent what it is
id string yes The endpoint. Use it to remove the endpoint or to list its deliveries.
url string yes Where deliveries are sent.
event_filter array yes The event types this endpoint receives. Empty means every payment event.
status string yes Whether this endpoint is still receiving deliveries. Removing an endpoint disables it rather than deleting it.
created_at string yes When the endpoint was registered.
signing_secret string yes The secret that signs this endpoint's webhooks. Shown only in this response.

When it refuses

Operation id createWebhookEndpoint, for code generators.

DELETE /v1/webhooks/endpoints/{endpoint_id} #

Delete a webhook endpoint

Authentication. Your secret key, in X-Ripper-Api-Key. Server only.

Stops deliveries to an endpoint. An unknown id, another account's id and an endpoint already deleted all answer the same 404.

Requires a secret key with the manage_webhooks permission.

parameter in required what it is
endpoint_id path yes The endpoint's id.
Rip-Idempotency-Key header no Optional but recommended. A retry with the same key returns the first response. Without a key the response carries Rip-Idempotency-Warning. Idempotency-Key is accepted as well; sending two different keys is refused.

204 — The endpoint was deleted. There is no body.

When it refuses

Operation id deleteWebhookEndpoint, for code generators.

GET /v1/webhooks/endpoints #

List webhook endpoints

Authentication. Your secret key, in X-Ripper-Api-Key. Server only.

Returns your active endpoints. With none registered the list is empty. Signing secrets are never returned here.

Requires a secret key with the manage_webhooks permission.

200 — Your active endpoints.

{
  "endpoints": [
    {
      "id": "whep_0123456789abcdef01234567",
      "url": "https://shop.example.com/webhooks/ripper",
      "event_filter": [],
      "status": "active",
      "created_at": "2026-09-16T10:00:00.0000000+00:00"
    }
  ]
}

Fields in the response

field type always sent what it is
endpoints array yes Your endpoints. An empty array when you have none — never a 404.

endpoints fields

field type always sent what it is
id string yes The endpoint. Use it to remove the endpoint or to list its deliveries.
url string yes Where deliveries are sent.
event_filter array yes The event types this endpoint receives. Empty means every payment event.
status string yes Whether this endpoint is still receiving deliveries. Removing an endpoint disables it rather than deleting it.
created_at string yes When the endpoint was registered.

When it refuses

Operation id listWebhookEndpoints, for code generators.