webhook-endpoints

Stable. Describes behaviour that is built and covered by tests in this repository.

API reference

GET /v1/webhook-endpoints #

The tenant's webhook endpoints

Not paged: the response is the complete list.

Responses

StatusMeaningBody
200OKListWebhookEndpointsResponse
Example response
{
  "endpoints": [
    {
      "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
      "url": "string",
      "description": "string",
      "event_types": [
        "string"
      ],
      "active": false,
      "secret": "string",
      "create_time": "2026-11-03T10:15:00Z"
    }
  ]
}
curl
curl -X GET https://YOUR-API-ADDRESS/v1/webhook-endpoints \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY"

POST /v1/webhook-endpoints #

Register an HTTPS endpoint that receives Payrudder's events

Payrudder sends a signed JSON POST to the endpoint for each event of the types it asks for (all types if none). The response carries the signing secret, once; it cannot be read again, only rotated. Verify each delivery with the Payrudder-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with the secret> (two v1 while a rotated secret is still accepted); reject a timestamp more than five minutes from your clock. Answer 2xx within 10 seconds; anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then given up (a delivery can be sent again by hand). Events are delivered at least once and not in order: use the event id to ignore repeats. The URL must be https and public; at most 20 endpoints.

Request body

FieldTypeDescription
url requiredstring · urihttps only, a public address, no credentials
descriptionstring
event_typesarray of WebhookEventTypeEmpty means every type
Example
{
  "url": "string"
}

Responses

StatusMeaningBody
201CreatedWebhookEndpoint
400Error envelopeError
409Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "url": "string",
  "description": "string",
  "event_types": [
    "string"
  ],
  "active": false,
  "secret": "string",
  "create_time": "2026-11-03T10:15:00Z"
}

Verify a delivery

# Nothing to install: any HTTP client works.
export PAYRUDDER_API_KEY=qk_test_
# A delivery carries Payrudder-Signature: t=<unix time>,v1=<hex>. The hex is HMAC-SHA256 of "<t>.<raw body>" under your endpoint's secret.
# Check it on the raw body before parsing; the SDKs do it for you (see the other tabs).
# Not on npm yet: build the package from a checkout of the repository, then install the tarball it makes.
(cd /path/to/payrudder/sdk/typescript && npm ci && npm run build && npm pack)   # writes payrudder-sdk-0.0.0.tgz
npm install /path/to/payrudder/sdk/typescript/payrudder-sdk-0.0.0.tgz
import { constructEvent, WebhookVerificationError } from "@payrudder/sdk";

// Express: app.post("/webhooks/payrudder", express.raw({ type: "application/json" }), handler)
async function handler(req, res) {
  try {
    const event = await constructEvent(req.body, req.header("Payrudder-Signature") ?? "", process.env.PAYRUDDER_WEBHOOK_SECRET!);
    console.log(event.id, event.type);
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.sendStatus(400);
    throw err;
  }
}
# Not on PyPI yet: install from a checkout of the repository.
pip install /path/to/payrudder/sdk/python
from payrudder import WebhookSignatureError, construct_event

@app.post("/webhooks/payrudder")  # Flask
def hook():
    try:
        event = construct_event(request.get_data(), request.headers.get("Payrudder-Signature"), WEBHOOK_SECRET)
    except WebhookSignatureError:
        return "", 400
    print(event["id"], event["type"])
    return "", 200
# Not on a module proxy yet: point your module at a checkout of the repository.
go mod edit -require=github.com/nafrucom/payrudder-sdks/go@v0.0.0
go mod edit -replace=github.com/nafrucom/payrudder-sdks/go=/path/to/payrudder/sdk/go
ev, err := payrudder.ConstructEvent(body, r.Header.Get(payrudder.WebhookSignatureHeader), os.Getenv("PAYRUDDER_WEBHOOK_SECRET"), 0)
if err != nil {
	http.Error(w, "bad signature", http.StatusBadRequest)
	return
}
fmt.Println(ev.ID, ev.Type)
# Not on Packagist yet: use a checkout of the repository as a path repository.
composer config repositories.payrudder path /path/to/payrudder/sdk/php
composer require payrudder/payrudder-php:"*@dev"
use Payrudder\Webhook\Webhook;

$payload = file_get_contents('php://input');
try {
    $event = Webhook::constructEvent($payload, $_SERVER['HTTP_PAYRUDDER_SIGNATURE'] ?? '', getenv('PAYRUDDER_WEBHOOK_SECRET'));
} catch (\Payrudder\Exception\WebhookException $e) {
    http_response_code(400);
    exit;
}
echo $event->id;

PUT /v1/webhook-endpoints/{id} #

Replace an endpoint's url, description, event types and active flag

Parameters

NameInTypeDescription
id requiredpathstring · uuid

Request body

FieldTypeDescription
url requiredstring · urihttps only, a public address, no credentials
descriptionstring
event_typesarray of WebhookEventTypeEmpty means every type
active requiredbooleanA PUT replaces the endpoint: leaving active out deactivates it
Example
{
  "url": "string",
  "active": false
}

Responses

StatusMeaningBody
200OKWebhookEndpoint
400Error envelopeError
404Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "url": "string",
  "description": "string",
  "event_types": [
    "string"
  ],
  "active": false,
  "secret": "string",
  "create_time": "2026-11-03T10:15:00Z"
}
curl
curl -X PUT https://YOUR-API-ADDRESS/v1/webhook-endpoints/<id> \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"string","active":false}'

DELETE /v1/webhook-endpoints/{id} #

Delete an endpoint and its delivery history

Parameters

NameInTypeDescription
id requiredpathstring · uuid

Responses

StatusMeaningBody
204Deleted
404Error envelopeError
curl
curl -X DELETE https://YOUR-API-ADDRESS/v1/webhook-endpoints/<id> \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY"

POST /v1/webhook-endpoints/{id}/rotate-secret #

Make a new signing secret (returned once); the old one keeps signing for 24 hours

Parameters

NameInTypeDescription
id requiredpathstring · uuid

Responses

StatusMeaningBody
200OKWebhookEndpoint
404Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "url": "string",
  "description": "string",
  "event_types": [
    "string"
  ],
  "active": false,
  "secret": "string",
  "create_time": "2026-11-03T10:15:00Z"
}
curl
curl -X POST https://YOUR-API-ADDRESS/v1/webhook-endpoints/<id>/rotate-secret \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY"

POST /v1/webhook-endpoints/{id}/test #

Send a ping event to one endpoint

Parameters

NameInTypeDescription
id requiredpathstring · uuid

Responses

StatusMeaningBody
202QueuedWebhookDelivery
404Error envelopeError
Example response
{
  "id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "event_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "event_type": "string",
  "endpoint_id": "0192f0b4-6f6e-7c3a-9a3e-6a1f2b3c4d5e",
  "state": "WEBHOOK_DELIVERY_STATE_PENDING",
  "attempts": 1,
  "next_attempt_time": "2026-11-03T10:15:00Z",
  "last_status": 1,
  "last_error": "string",
  "create_time": "2026-11-03T10:15:00Z",
  "delivered_time": "2026-11-03T10:15:00Z"
}
curl
curl -X POST https://YOUR-API-ADDRESS/v1/webhook-endpoints/<id>/test \
  -H "Authorization: Bearer $PAYRUDDER_API_KEY"

Models

CreateWebhookEndpointRequest

FieldTypeDescription
url requiredstring · urihttps only, a public address, no credentials
descriptionstring
event_typesarray of WebhookEventTypeEmpty means every type

Error

FieldTypeDescription
error requiredobject

ListWebhookEndpointsResponse

FieldTypeDescription
endpoints requiredarray of WebhookEndpoint

WebhookDelivery

FieldTypeDescription
id requiredstring · uuid
event_id requiredstring · uuidThe id inside the delivered body and the Payrudder-Event-Id header
event_type requiredstringA WebhookEventType, or ping for the test event
endpoint_id requiredstring · uuid
state requiredWEBHOOK_DELIVERY_STATE_PENDING | WEBHOOK_DELIVERY_STATE_DELIVERED | WEBHOOK_DELIVERY_STATE_FAILED
attempts requiredinteger
next_attempt_time requiredstring · date-timeSet while the delivery is pending
last_status requiredintegerHTTP status of the last attempt; 0 when no attempt got an answer
last_error requiredstring
create_time requiredstring · date-time
delivered_time requiredstring · date-timeSet once the endpoint answered 2xx

WebhookEndpoint

FieldTypeDescription
id requiredstring · uuid
url requiredstring
description requiredstring
event_types requiredarray of string
active requiredboolean
secret requiredstringwhsec_…; only in the response of create and rotate-secret
create_time requiredstring · date-time

WebhookEndpointInput

FieldTypeDescription
url requiredstring · urihttps only, a public address, no credentials
descriptionstring
event_typesarray of WebhookEventTypeEmpty means every type
active requiredbooleanA PUT replaces the endpoint: leaving active out deactivates it

WebhookEventType

invoice.finalized | invoice.paid | invoice.voided | invoice.uncollectible | credit_note.created | credit_note.refunded | credit_note.refund_failed | subscription.created | subscription.changed | subscription.change_scheduled | subscription.cancelled | subscription.cancellation_scheduled | subscription.cancellation_withdrawn | subscription.paused | subscription.resumed | subscription.moved_to_provider | subscription.trial_will_end | payment_link.created | payment_link.paid | payment_link.expired | payment.disputed | credits.purchased | credits.low | customer.blocked | customer.unblocked | price_change.scheduled | contract.accepted | contract.version_published | contract.withdrawal_requested | contract.withdrawal_decided