Webhooks

Receive real-time HTTP notifications when billing events happen in Paid

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

Supported events

Event nameWhen it firesPrimary payload key
billing-invoice-createdA new invoice is createdinvoice
billing-invoice-paidAn invoice becomes fully paidinvoice
billing-checkout-createdA checkout session is createdcheckout
billing-checkout-completedA checkout payment completescheckout
billing-checkout-expiredA checkout session expirescheckout
billing-payment-succeededA payment succeedspayment
billing-payment-failedA payment attempt failspayment
billing-credits-depletedA customer runs out of prepaid creditscredits
billing-overage-incurredUsage exceeds the included threshold and creates an overage conditionoverage

Envelope format

Every webhook uses the same top-level envelope:

1{
2 "event": "billing-payment-succeeded",
3 "timestamp": "2026-04-16T14:05:00.000Z",
4 "isTest": false,
5 "data": {
6 "payment": {
7 "id": "pay_123",
8 "invoiceId": "inv_123",
9 "customerId": "cus_123"
10 }
11 }
12}
  • event identifies which webhook fired
  • timestamp is the time Paid created the delivery in RFC 3339 format
  • isTest is true for test deliveries sent from the UI
  • data contains the event-specific payload, keyed by the primary payload key listed above

How delivery works

Each webhook event is configured independently. In the Paid UI you can:

  1. Open Settings > Webhooks
  2. Choose the event you want to receive
  3. Enter the receiver URL in your system
  4. Enable the webhook
  5. 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 2xx response 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

$curl https://api.agentpaid.io/api/v2/webhooks \
> -H "Authorization: Bearer YOUR_API_KEY"

Configure a webhook

$curl -X PATCH https://api.agentpaid.io/api/v2/webhooks/billing-payment-succeeded \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://example.com/paid-webhooks",
> "enabled": true
> }'

Send a test delivery

$curl -X POST https://api.agentpaid.io/api/v2/webhooks/billing-payment-succeeded/test \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{}'

Test deliveries only work after the webhook has a valid URL configured and enabled is set to true.

Event payload examples

Payment succeeded

1{
2 "event": "billing-payment-succeeded",
3 "timestamp": "2026-04-16T14:05:00.000Z",
4 "isTest": false,
5 "data": {
6 "payment": {
7 "id": "pay_123",
8 "invoiceId": "inv_123",
9 "invoiceNumber": "INV-000123",
10 "customerId": "cus_123",
11 "customerName": "Acme",
12 "amount": 10800,
13 "currency": "USD",
14 "status": "posted",
15 "paymentDate": "2026-04-16T14:05:00.000Z",
16 "paymentType": "creditCard",
17 "externalPaymentId": "pi_123"
18 }
19 }
20}

Field notes:

  • payment.status is currently posted
  • payment.invoiceId and payment.invoiceNumber can be null when the payment is not linked to an invoice
  • payment.externalPaymentId can be null when there is no upstream processor reference
  • paymentDate is the effective payment timestamp in RFC 3339 format

Payment failed

1{
2 "event": "billing-payment-failed",
3 "timestamp": "2026-04-16T14:05:00.000Z",
4 "isTest": false,
5 "data": {
6 "payment": {
7 "id": "pay_456",
8 "invoiceId": "inv_456",
9 "invoiceNumber": "INV-000456",
10 "customerId": "cus_456",
11 "customerName": "Acme",
12 "amount": 10800,
13 "currency": "USD",
14 "status": "failed",
15 "failureReason": "Your card was declined.",
16 "failureDate": "2026-04-16T14:05:00.000Z",
17 "paymentType": "creditCard",
18 "externalPaymentId": "pi_456"
19 }
20 }
21}

Field notes:

  • payment.status is currently failed
  • failureReason is a human-readable failure message when one is available
  • failureDate is when Paid recorded the failed attempt in RFC 3339 format
  • invoiceId, invoiceNumber, and externalPaymentId can be null

Credits depleted

1{
2 "event": "billing-credits-depleted",
3 "timestamp": "2026-04-16T14:05:00.000Z",
4 "isTest": false,
5 "data": {
6 "credits": {
7 "customerId": "cus_123",
8 "customerName": "Acme",
9 "orderLineAttributeId": "ola_123",
10 "creditsCurrencyId": "cur_123",
11 "totalCredits": 1000,
12 "remainingCredits": 0,
13 "usedCredits": 1000,
14 "eventName": "api_call.completed",
15 "signalId": "sig_123",
16 "depletedAt": "2026-04-16T14:05:00.000Z"
17 }
18 }
19}

Field notes:

  • remainingCredits is 0 when this event fires
  • creditsCurrencyId can be null if the depleted balance is not scoped to a credits currency
  • signalId is the Paid signal that pushed the balance to zero
  • eventName is the original signal event name that consumed the final credits

Overage incurred

1{
2 "event": "billing-overage-incurred",
3 "timestamp": "2026-04-16T14:05:00.000Z",
4 "isTest": false,
5 "data": {
6 "overage": {
7 "customerId": "cus_123",
8 "customerName": "Acme",
9 "planId": "plan_123",
10 "planName": "Growth",
11 "orderLineAttributeId": "ola_123",
12 "eventType": "OverageUsage",
13 "threshold": 1000,
14 "currentUsage": 1200,
15 "occurredAt": "2026-04-16T14:05:00.000Z"
16 }
17 }
18}

Field notes:

  • eventType is one of OverageUsage or OverageCredit
  • threshold is the included usage limit that was crossed
  • currentUsage is the usage value observed when the overage condition was detected
  • customerId, customerName, planName, threshold, and currentUsage can be null in edge cases

Implementation checklist

Before you enable a webhook in Paid, make sure your receiver:

  • Accepts unauthenticated HTTPS POST requests from Paid at a stable public URL
  • Parses the top-level envelope first, then branches on event
  • Handles null values for optional fields like invoiceId, invoiceNumber, externalPaymentId, customerName, planName, threshold, and currentUsage
  • Returns a 2xx after persisting or queueing the event
  • Ignores or separately labels isTest: true deliveries in your downstream systems
  • Safely ignores unknown event values 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
  • isTest is set to true
  • IDs in the nested payload are synthetic test IDs such as pay_test_*, cust_test_*, ola_test_*, or plan_test_*
  • Test payloads are intended to validate parsing and routing, not to represent real billing records in your system