Webhooks
Paid can send HTTP POST requests to your system whenever important billing events happen. You configure webhooks in the Paid app under Settings > Webhooks, or manage them programmatically through the API.
This page covers:
- Which webhook events Paid supports today
- What the webhook payloads look like
- How to configure and manage webhooks
- How to test deliveries
Prerequisites
- Admin access to Settings > Webhooks or a Paid API key for programmatic webhook management.
- A publicly reachable HTTPS endpoint that can accept
POSTrequests from Paid. - Access to the raw request body in your web server so you can verify
x-webhook-signaturebefore parsing JSON. - A place to store the webhook signing secret securely, such as an environment variable or secret manager.
Supported events
Envelope format
Every webhook uses the same top-level envelope:
eventidentifies which webhook firedtimestampis the time Paid created the delivery in RFC 3339 formatisTestistruefor test deliveries sent from the UIdatacontains the event-specific payload, keyed by the primary payload key listed above
Verifying webhook signatures
Every delivery includes an HMAC-SHA256 signature in the x-webhook-signature header so you can verify the request came from Paid. Your organization has one signing secret, used by every webhook delivery regardless of event type. Generate or rotate it from Settings > Webhooks.
Header format
tis the delivery timestamp in milliseconds since the Unix epoch.sis the base64-encoded HMAC-SHA256 of the signed payload using your webhook’s signing secret as the key. (The trailing=is base64 padding. Do not strip it.)
The signed payload is the timestamp, a literal ., and the raw JSON request body, joined as one byte string:
Verify by recomputing the HMAC on your side and comparing in constant time. Reject deliveries where the timestamp is older than five minutes. This prevents replay of leaked deliveries.
Node.js example
Make sure your framework gives you the raw request body, not a re-serialized JSON object. Even whitespace differences break the signature.
Getting the raw body
Most web frameworks auto-parse JSON request bodies and discard the original bytes. The HMAC is computed over the literal bytes Paid sent, so a parsed-and-re-serialized object will not match. Below are minimal recipes for opting into raw-body access on the most common frameworks.
Express (Node): use express.raw() on the webhook route instead of express.json().
Next.js (App Router): use request.text(), not request.json().
Next.js (Pages Router): disable Next’s built-in body parser for the route.
Flask (Python): call request.get_data(), not request.get_json().
FastAPI (Python): await request.body() directly.
Django (Python): request.body is already the raw bytes. Exempt the route from CSRF since Paid is not the user’s browser.
Go (net/http): read the request body before decoding.
The general rule: if your framework auto-parses JSON, find the option to disable it on this route, or read the body before any parser runs.
Rotating the secret
There is one signing secret per organization. Every webhook delivery from your account is signed with the same key. Rotate from the UI (Settings > Webhooks > Rotate signing secret) or the API:
The response includes signingSecret exactly once. Store it before closing the response. Paid does not store it in a way you can read back.
Rotation invalidates the old secret on the next delivery. Update your receiver before rotating in production, or accept a short verification gap.
How delivery works
Each webhook event is configured independently. In the Paid UI you can:
- Open Settings > Webhooks
- Choose the event you want to receive
- Enter the receiver URL in your system
- Enable the webhook
- Use the Test button to send a sample payload
Paid sends webhook requests as HTTP POST calls. Your endpoint should:
- Accept JSON request bodies with
Content-Type: application/json - Return a
2xxresponse quickly after validating and enqueueing work - Treat deliveries as retriable and idempotent on your side
- Deduplicate using the business identifiers in the payload, not the delivery timestamp
Managing webhooks through the API
You can manage webhooks programmatically through the v2 API with an organization API key.
List all webhooks
Configure a webhook
Send a test delivery
Test deliveries only work after the webhook has a valid URL configured and
enabled is set to true.
Event payload examples
Payment succeeded
Field notes:
payment.statusis currentlypostedpayment.invoiceIdandpayment.invoiceNumbercan benullwhen the payment is not linked to an invoicepayment.externalPaymentIdcan benullwhen there is no upstream processor referencepaymentDateis the effective payment timestamp in RFC 3339 format
Payment failed
Field notes:
payment.statusis currentlyfailedfailureReasonis a human-readable failure message when one is availablefailureDateis when Paid recorded the failed attempt in RFC 3339 formatinvoiceId,invoiceNumber, andexternalPaymentIdcan benull
Credits depleted
The sample above is a 100.5-credit pool with 0.5 credits left, and a
1-credit call that consumes them and overshoots by 0.5.
Field notes:
- Credit quantities are decimal, not whole numbers.
totalCredits,remainingCredits, andusedCreditscarry up to six decimal places, so a parser must accept values such as10.5or0.000001. They are JSON numbers; ±2^53 is the exactness boundary for a consumer that reads them into a double, not an enforced payload limit — the publisher emits any storable balance. A parser typed to integers will break on this payload. - The
*Decimalsiblings are the lossless read.totalCreditsDecimal,remainingCreditsDecimal, andusedCreditsDecimalcarry the same values as decimal strings, exact at any magnitude. Prefer them wherever a balance can exceed ±2^53; parse them with a decimal type, not a double. remainingCreditscan be negative. It is the real remainder in the pool at depletion:0for an exact depletion, and negative when the depleting spend overshot into overage — with sub-unit credit costs, a fraction such as-0.5.totalCreditsis the pool’s total, andusedCreditsis what the depleting signal alone consumed — not the cumulative amount used. The two are equal only when a single signal consumes a whole pool.creditsCurrencyIdcan benullif the depleted balance is not scoped to a credits currencysignalIdis the Paid signal that took the balance to zero or beloweventNameis the original signal event name that consumed the final credits
Overage incurred
Field notes:
eventTypeis one ofOverageUsageorOverageCreditthresholdis the included usage limit that was crossedcurrentUsageis the usage value observed when the overage condition was detectedcustomerId,customerName,planName,threshold, andcurrentUsagecan benullin edge cases
Credit cap reached
Fires once per cap and period, when the credits of one currency spent by a customer unit and everything under it reach the cap set on that unit. Caps are advisory: spend is not blocked, and the event is your signal to act.
Field notes:
breachedUnitis the unit whose cap was reached, by its Paid id (cu_…) and your ownexternalId;externalTypeis your vocabulary for it and can benull. The spend may have come from that unit or from any unit beneath it.capidentifies the cap: the credits currency it limits, theamountper period, thefrequency, and theeffectiveFromits periods are anchored on. Caps have no separate id; address one by unit and credits currency.usedis the subtree’s spend of that currency inperiodat detection, a decimal that can exceedcap.amount.amountDecimalandusedDecimalare always present: the lossless string twins, exact at any magnitude. Prefer them wherever a total may exceed ±2^53.periodis the cap period the breach was found in (startinclusive,endexclusive, UTC).customer.externalIdcan benullwhen the customer has no external id.
Implementation checklist
Before you enable a webhook in Paid, make sure your receiver:
- Accepts unauthenticated HTTPS
POSTrequests from Paid at a stable public URL - Parses the top-level envelope first, then branches on
event - Handles
nullvalues for optional fields likeinvoiceId,invoiceNumber,externalPaymentId,customerName,planName,threshold, andcurrentUsage - Returns a
2xxafter persisting or queueing the event - Ignores or separately labels
isTest: truedeliveries in your downstream systems - Safely ignores unknown
eventvalues for forward compatibility as new events are added
Testing from the Paid UI
Use the Test button in Settings > Webhooks to send a synthetic event to your receiver.
- Test deliveries use the same envelope shape as production deliveries
isTestis set totrue- IDs in the nested payload are synthetic test IDs such as
pay_test_*,cust_test_*,cus_test_*,cu_test_*,ola_test_*, orplan_test_* - Test payloads are intended to validate parsing and routing, not to represent real billing records in your system