Skip to content

Webhook integration

Webhooks notify your backend when accounting balances change. The configuration contract comes from the public API specification; the delivery body and signature below come directly from the proxybuild/accountant sender.

Terminal window
curl 'https://api.proxyrequest.com/api/v1/webhooks' \
--request POST \
--header 'Authorization: Static YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"type": "reseller",
"endpoint": "https://integrator.example/webhooks/proxy-usage",
"read_timeout": 5,
"write_timeout": 5,
"retries": 3,
"retry_timeout": 10
}'
FieldAllowed by the public schemaDefault
typeuser, reseller, or systemRequired
endpointPublic URI, maximum 500 charactersRequired
read_timeout1–60 seconds5
write_timeout1–60 seconds5
retries0–103
retry_timeout1–300 seconds10

The create response includes a signing secret. Store it immediately in a secret manager. List and retrieve responses use a schema without the secret, so your integration must not depend on reading it later.

user

Delivers matching events for the associated user context. Use it for a direct account or one intentionally scoped consumer.

reseller

Accepts events for the reseller account and child users whose user.reseller_id matches that account.

system

Reserved for platform-wide context when that scope is provisioned and authorized by the deployment.

The accountant sends:

POST /webhooks/proxy-usage HTTP/1.1
Content-Type: application/json
X-Signature: BASE64_HMAC_SHA256
DELIVERY CONTRACT

Follow the nested event contract

{
  "events": [
    {
      "event": "user.data.change",
      "date": "2026-08-01T10:15:00Z",
      "user": {
        "id": "user_42",
        "username": "acme-customer",
        "reseller_id": "reseller_01"
      },
      "package": {
        "id": "package_9",
        "alias": "residential"
      },
      "order": {
        "id": "order_71",
        "data": 5368709120,
        "data_remaining": 2147483648,
        "ledger_id": "ledger_12"
      }
    }
  ],
  "date": "2026-08-01T10:15:01Z"
}
events
One delivery can contain multiple balance events.
user
Affected user identity and its reseller ownership context.
package
Product identifier and accounting-time alias.
order.data
Remaining shared-ledger balance in bytes; overdraft can be negative.
order.data_remaining
Finite child-order balance, or null for root and unlimited orders.
date
Appears on both the event and the delivery batch.
01 Read raw body02 HMAC-SHA25603 Base6404 Compare X-Signature

The delivery body is a batch envelope: events contains one or more event objects and the outer date records when the batch was created. Inside each event, fields are grouped by responsibility:

  • user contains id, username, and reseller_id;
  • package contains id and alias;
  • order contains id, the shared-ledger data, the child-order data_remaining, and ledger_id.

order.data_remaining is always serialized. It is an integer for a finite child order and JSON null for a root or unlimited order.

The signature formula is:

Base64(HMAC-SHA256(webhook_secret, raw_request_body))

Verify the exact bytes received from the network. Parsing JSON and serializing it again can change whitespace, key order, or escaping and will invalidate the signature.

import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebhook(rawBody, signature, secret) {
if (!signature) return false;
const expected = createHmac('sha256', secret).update(rawBody).digest();
const received = Buffer.from(signature, 'base64');
return (
received.length === expected.length &&
timingSafeEqual(received, expected)
);
}
  1. Read the request body as bytes with an explicit size limit.
  2. Read X-Signature and reject a missing or invalid value.
  3. Parse the verified JSON and validate events[], user, package, and order before reading their fields.
  4. Store a delivery record or enqueue each event for asynchronous processing.
  5. Return a small 2xx response quickly.

The current sender treats any response below 400 as delivered, but your endpoint should return a direct 2xx and avoid redirects. Network errors, timeouts, and 4xx/5xx responses consume the configured attempt budget.

  • A request contains an array, not one event.
  • Balance changes with the same event/user/order/package key can be coalesced before a flush.
  • Run-out events are suppressed for a cooldown period after delivery is queued.
  • Retries can deliver the same serialized batch again.
  • The payload has no event ID and does not promise array ordering.

Build a bounded deduplication fingerprint from stable fields such as event, user.id, order.id, package.id, the event date, order.data, and order.data_remaining. Keep the raw verified delivery for investigation, and design updates so reprocessing the same balance does not double-charge a customer.

Webhooks are the reaction channel, not a complete invoice ledger. Query analytics for closed reporting windows and compare totals with your local projection. See analytics and reconciliation and the complete webhook event reference.