Skip to content

API reference

Webhooks

Outgoing webhook subscriptions and delivery attempts.

List webhooks for the authenticated account

GET /webhooks

Bearer JWT (Sign-In With Ethereum)

Query parameters

  • page integer

    1-based page number.

  • per_page integer

    Items per page (capped at 100).

  • sort string

    Comma-separated sort fields; prefix with - for descending (e.g. -created_at,status). An unsupported field is rejected with 422 invalid_sort, whose detail names the allowed set — it is never silently dropped.

  • topic string

    Filter to subscriptions that include this topic.

    One of: payments.created, payments.signed, payments.authorized, payments.charged, payments.captured, payments.voided, payments.released, payments.refunded, payments.authorization_expiring, payments.expired, payments.failed, payments.disputed, payments.dispute_closed
  • active boolean

    Filter by active status.

  • circuit_state string

    Filter by circuit state.

    One of: closed, open

Responses

  • 200 Webhooks.
  • 401 Authentication failed.
Terminal window
curl "https://api.rail0.xyz/webhooks" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
[
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}
]

Create a webhook

POST /webhooks

Bearer JWT (Sign-In With Ethereum)

Request body

  • name string required

    Human-readable identifier.

  • callback_url string required

    HTTPS destination URL.

  • topics array of string required

    Event topics for this subscription — one secret and one circuit breaker for all of them. Repeated values are collapsed.

    One of: payments.created, payments.signed, payments.authorized, payments.charged, payments.captured, payments.voided, payments.released, payments.refunded, payments.authorization_expiring, payments.expired, payments.failed, payments.disputed, payments.dispute_closed

Responses

  • 201 Webhook created. shared_secret is returned only on this response and on rotate.
  • 401 Authentication failed.
  • 422 validation_failed (a malformed callback_url or topic), or webhook_limit_reached: the account already holds 20 subscriptions (WEBHOOKS_PER_ACCOUNT; disabled ones count, deleted ones do not). Delete one, or add topics to an existing subscription.
Terminal window
curl -X POST "https://api.rail0.xyz/webhooks" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
]
}'
Response 201
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z",
"shared_secret": "string"
}

Get a webhook

GET /webhooks/{id}

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Responses

  • 200 Webhook.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Update a webhook (name, callback_url, topics)

PATCH /webhooks/{id}

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Request body (optional)

  • name string
  • callback_url string
  • topics array of string

    REPLACES the whole set, so this is also how a topic is removed. The shared secret is untouched.

    One of: payments.created, payments.signed, payments.authorized, payments.charged, payments.captured, payments.voided, payments.released, payments.refunded, payments.authorization_expiring, payments.expired, payments.failed, payments.disputed, payments.dispute_closed

Responses

  • 200 Webhook updated.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 Validation error.
Terminal window
curl -X PATCH "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Delete a webhook

DELETE /webhooks/{id}

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Responses

  • 204 Webhook deleted.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl -X DELETE "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 204 · No content

Enable a webhook

PUT /webhooks/{id}/enable

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Responses

  • 200 Webhook enabled.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl -X PUT "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/enable" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Disable a webhook

PUT /webhooks/{id}/disable

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Responses

  • 200 Webhook disabled.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl -X PUT "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/disable" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

Rotate the shared secret — returns the new secret once

PUT /webhooks/{id}/rotate_secret

Bearer JWT (Sign-In With Ethereum)

Replaces the shared secret and returns the new one once. The replaced key is kept for a grace window (one hour): inside it every delivery carries X-Rail0-Signature under the new key AND X-Rail0-Signature-Previous under the old one, so an endpoint verifies with whichever key it has deployed and no delivery in flight at rotation becomes unverifiable. The old key signs only, is never honoured past the window, and a second rotation inside it drops the oldest key. Two concurrent rotations cannot both succeed: the one that loses the race answers 409.

Path parameters

  • id string (uuid) required

Responses

  • 200 Secret rotated; the new shared_secret is returned once.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 409 conflict: another rotation of this webhook replaced the secret while this request was in flight. No secret is returned; the winning request's secret is the current one.
Terminal window
curl -X PUT "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/rotate_secret" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z",
"shared_secret": "string"
}

Reset the circuit breaker and re-enable the webhook

PUT /webhooks/{id}/reset_circuit

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Responses

  • 200 Circuit reset.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl -X PUT "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/reset_circuit" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"name": "Acme Store",
"callback_url": "https://acme.example/webhooks/rail0",
"topics": [
"payments.captured"
],
"active": false,
"circuit_state": "closed",
"circuit_failure_count": 1,
"created_at": "2026-10-01T12:00:00Z",
"updated_at": "2026-10-01T12:00:00Z"
}

List delivery attempts for a webhook

GET /webhooks/{id}/event_callbacks

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • id string (uuid) required

Query parameters

  • page integer

    1-based page number.

  • per_page integer

    Items per page (capped at 100).

  • sort string

    Comma-separated sort fields; prefix with - for descending (e.g. -created_at,status). An unsupported field is rejected with 422 invalid_sort, whose detail names the allowed set — it is never silently dropped.

  • status string

    Filter by delivery status.

    One of: delivered, failed
  • topic string

    Filter by event topic.

    One of: payments.created, payments.signed, payments.authorized, payments.charged, payments.captured, payments.voided, payments.released, payments.refunded, payments.authorization_expiring, payments.expired, payments.failed, payments.disputed, payments.dispute_closed
  • payment_id string

    Filter by the payment the delivery is for: its id (UUID) or its rail0_id (0x…).

  • response_code string

    Filter by the subscriber's exact HTTP response code (e.g. 500).

  • since string (date-time)

    Only deliveries at/after this time (ISO-8601).

  • until string (date-time)

    Only deliveries at/before this time (ISO-8601).

Responses

  • 200 Event callbacks.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/event_callbacks?status=failed" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
[
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"webhook_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
"payment_id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"topic": "payments.captured",
"callback_url": "https://acme.example/webhooks/rail0",
"response_code": null,
"response_message": null,
"error_reason": null,
"status": "delivered",
"created_at": "2026-10-01T12:00:00Z",
"payload": null
}
]

Re-deliver a recorded event callback's exact payload

POST /webhooks/{id}/event_callbacks/{callback_id}/redeliver

Bearer JWT (Sign-In With Ethereum)

Re-sends the stored delivery payload verbatim (same embedded event id, so a consumer that already processed it deduplicates) under a fresh timestamped signature, through the standard async delivery job. A disabled or circuit-open webhook drops the replay like any delivery - reset the circuit first.

Path parameters

  • id string (uuid) required
  • callback_id string (uuid) required

    The event_callback row whose recorded payload to re-send.

Responses

  • 202 Replay queued.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 503 queue_unavailable: the job queue could not be reached, so nothing was enqueued and no replay will happen. Nothing is written by this endpoint; retry in a moment.
Terminal window
curl -X POST "https://api.rail0.xyz/webhooks/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/event_callbacks/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/redeliver" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 202
{
"status": "queued"
}