webhook-endpoints
Stable. Describes behaviour that is built and covered by tests in this repository.
GET /v1/webhook-endpoints #
The tenant's webhook endpoints
Not paged: the response is the complete list.
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | ListWebhookEndpointsResponse |
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
| Field | Type | Description |
|---|---|---|
url required | string · uri | https only, a public address, no credentials |
description | string | |
event_types | array of WebhookEventType | Empty means every type |
Example
{
"url": "string"
}Responses
| Status | Meaning | Body |
|---|---|---|
201 | Created | WebhookEndpoint |
400 | Error envelope | Error |
409 | Error envelope | Error |
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.tgzimport { 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/pythonfrom 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/goev, 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
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Request body
| Field | Type | Description |
|---|---|---|
url required | string · uri | https only, a public address, no credentials |
description | string | |
event_types | array of WebhookEventType | Empty means every type |
active required | boolean | A PUT replaces the endpoint: leaving active out deactivates it |
Example
{
"url": "string",
"active": false
}Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | WebhookEndpoint |
400 | Error envelope | Error |
404 | Error envelope | Error |
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
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
204 | Deleted | |
404 | Error envelope | Error |
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
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | OK | WebhookEndpoint |
404 | Error envelope | Error |
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
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string · uuid |
Responses
| Status | Meaning | Body |
|---|---|---|
202 | Queued | WebhookDelivery |
404 | Error envelope | Error |
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
| Field | Type | Description |
|---|---|---|
url required | string · uri | https only, a public address, no credentials |
description | string | |
event_types | array of WebhookEventType | Empty means every type |
Error
| Field | Type | Description |
|---|---|---|
error required | object |
ListWebhookEndpointsResponse
| Field | Type | Description |
|---|---|---|
endpoints required | array of WebhookEndpoint |
WebhookDelivery
| Field | Type | Description |
|---|---|---|
id required | string · uuid | |
event_id required | string · uuid | The id inside the delivered body and the Payrudder-Event-Id header |
event_type required | string | A WebhookEventType, or ping for the test event |
endpoint_id required | string · uuid | |
state required | WEBHOOK_DELIVERY_STATE_PENDING | WEBHOOK_DELIVERY_STATE_DELIVERED | WEBHOOK_DELIVERY_STATE_FAILED | |
attempts required | integer | |
next_attempt_time required | string · date-time | Set while the delivery is pending |
last_status required | integer | HTTP status of the last attempt; 0 when no attempt got an answer |
last_error required | string | |
create_time required | string · date-time | |
delivered_time required | string · date-time | Set once the endpoint answered 2xx |
WebhookEndpoint
| Field | Type | Description |
|---|---|---|
id required | string · uuid | |
url required | string | |
description required | string | |
event_types required | array of string | |
active required | boolean | |
secret required | string | whsec_…; only in the response of create and rotate-secret |
create_time required | string · date-time |
WebhookEndpointInput
| Field | Type | Description |
|---|---|---|
url required | string · uri | https only, a public address, no credentials |
description | string | |
event_types | array of WebhookEventType | Empty means every type |
active required | boolean | A 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