Woldy uses webhooks to notify your server about events — for example, when a payment link is paid, a charge fails, or a subscription goes past due. You create an HTTPS endpoint that Woldy sends POST requests to.
Quick Start#
1.
Create an HTTPS endpoint to receive webhook requests
2.
Register a webhook in the dashboard under Settings → Branch → Webhooks, and pick the event types you want 3.
Save the Public Key shown in the dashboard — you need it to verify signatures
4.
Verify the signature of every incoming request before trusting its contents
5.
Return HTTP 2xx in response to each request
6.
Use Settings → Branch → Webhooks → Send test event (webhook.test) to confirm delivery and signature verification end-to-end before going live
Setup#
Webhook creation and management is available through the dashboard under Settings → Branch → Webhooks — this is not part of the API-key–authenticated REST surface documented elsewhere in this reference.When creating a webhook, specify:URL — your server's HTTPS endpoint (HTTP is not supported; private IPs, localhost, and cloud metadata addresses are rejected)
Events — the event types to subscribe to (see the full catalog below)
After creation, the dashboard shows a Public Key (Ed25519, base64). This key is the same for every webhook on your account — save it. It is shown again any time you view or edit a webhook, so losing it isn't fatal, but treat it as long-lived integration config rather than a one-time secret.Limits: max 10 webhooks per branch, HTTPS only.
Event Catalog#
Every event shares the same outer envelope (see below); only type and data change. data has one shape per group — the individual event types within a group only differ in status/timing fields, not structure. Each group below is documented on a single page — pick the matching example from the type field's examples to see that specific event's payload.| Event type |
|---|
payment_link.created |
payment_link.processing |
payment_link.attempt_failed |
payment_link.cancelled |
payment_link.completed |
payment_link.expired |
A charge is one payment attempt. A single payment link can produce several charges (e.g. one failed attempt followed by a successful one).| Event type |
|---|
charge.created |
charge.completed |
charge.failed |
| Event type |
|---|
subscription.created |
subscription.activated |
subscription.cancelled |
subscription.cancellation_scheduled |
subscription.cancellation_revoked |
subscription.expired |
subscription.past_due |
subscription.recovered |
subscription.payment_succeeded — only event type where data.charge is populated (with payment_failed) |
subscription.payment_failed — only event type where data.charge is populated (with payment_succeeded) |
subscription.paused |
subscription.resumed |
subscription.allowance_low |
subscription.allowance_revoked |
subscription.allowance_expiring |
| Event type |
|---|
subscription_plan.created |
subscription_plan.updated |
subscription_plan.paused |
subscription_plan.resumed |
subscription_plan.archived |
| Event type |
|---|
webhook.test — sent by "Send test event" in the dashboard, not a real business event |
Incoming Request#
Each webhook is a POST request with Content-Type: application/json.| Header | Value |
|---|
X-Woldy-Signature | Ed25519 signature, base64 Standard Encoding (RFC 4648, with = padding) |
X-Woldy-Signature-Version | Always ed25519-v1 |
X-Woldy-Event-ID | Event UID — wev_... (base64url-encoded UUID). Not a raw UUID — use this exact string, prefix included, when verifying the signature |
X-Woldy-Event-Type | Event type, e.g. charge.completed |
X-Woldy-Timestamp | Unix timestamp of the request (integer, seconds) |
Content-Type | application/json |
User-Agent | Woldy-Webhooks/1.0 |
Envelope#
Every request body has the same outer shape:{
"id": "wev_VQ6EA4np...",
"branch_id": "cb_VQ6EA4np...",
"webhook_id": "wh_VQ6EA4np...",
"type": "charge.completed",
"api_version": "2024-01-01",
"created_at": 1737720000,
"data": {
"...": "shape depends on the event group — see the Event Catalog above"
}
}
id — same value as X-Woldy-Event-ID (with the wev_ prefix)
branch_id — your branch UID (cb_...)
webhook_id — the webhook subscription that delivered this event (wh_...)
created_at — Unix timestamp, seconds
For the exact fields inside data for each event group, follow the links in the Event Catalog above.
Signature Verification#
Always verify the signature before processing event data.
How it works#
Woldy signs every request using Ed25519. The signed string is:{timestamp}.{eventID}.{bodyJSON}
timestamp — value from X-Woldy-Timestamp
eventID — value from X-Woldy-Event-ID (the full wev_... string, exactly as received — not a raw UUID)
bodyJSON — the exact raw bytes of the HTTP request body
The signature covers the entire request body. Always verify against the raw body bytes — never re-serialize parsed JSON.
Steps#
1.
Extract X-Woldy-Timestamp, X-Woldy-Event-ID, X-Woldy-Signature from headers
2.
Check freshness: |now - timestamp| ≤ 300 seconds
3.
Build: "{timestamp}.{eventID}.{bodyJSON}"
4.
Decode X-Woldy-Signature from base64 Standard Encoding
5.
Verify the Ed25519 signature with the Public Key from the dashboard
Node.js
Python
Go
Retry Policy#
If your server returns an error (4xx, 5xx) or does not respond within the configured request timeout:| Attempt | Delay after previous attempt |
|---|
| 1 | Immediately |
| 2 | ~1 minute |
| 3 | ~5 minutes |
| 4 | ~30 minutes |
| 5+ | ~2 hours, repeating (±20% jitter applied to every delay) |
Delivery is retried up to a server-configured attempt limit, after which it's marked FAILED. You can inspect delivery history and manually retry a specific delivery from the dashboard.
Best Practices#
Respond quickly — return 200 immediately, process asynchronously.Handle duplicates — the same event may be delivered more than once (e.g. after a retry that actually succeeded but the ack was lost). Use the event ID (X-Woldy-Event-ID / body id) for deduplication.Always verify — never process a webhook without verifying the signature.Don't rely on delivery order — deliveries for different events can arrive out of order. If ordering matters, use the resource's own status/timestamp fields (e.g. re-fetch the current state via the REST API) rather than assuming event arrival order reflects it.
Troubleshooting#
Not receiving events — check the webhook's status in the dashboard, confirm you subscribed to the correct event type, and check the webhook's delivery history for failed attempts and their error messages.Use the Public Key currently shown in the dashboard for this webhook
Sign only the exact raw request body bytes — do not re-serialize parsed JSON
Timestamp must be within ±5 minutes
Decode the signature from base64 Standard Encoding (with = padding)
The Public Key is raw 32-byte Ed25519, not X.509/SPKI
Timeout — respond 200 immediately, process asynchronously; don't do slow work (DB writes, outbound calls) before acknowledging. Modified at 2026-08-19 09:43:07