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.

They do not announce payment completion: there is no invoice.paid event in this sender. Confirm purchases by reading the invoice and resulting order as shown in billing. A delivery is a balance snapshot, not a command to grant those bytes again.

RequestPOST /webhooks
curl --request POST 'https://{api_host}/api/v1/webhooks' \
--header 'Authorization: Static {api_key}' \
--header 'Content-Type: application/json' \
--data '{
"type": "reseller",
"endpoint": "{webhook_url}",
"read_timeout": 5,
"write_timeout": 5,
"retries": 3,
"retry_timeout": 10
}'
Response201 Created

Selected fields

{
"id": "550e8400-e29b-41d4-a716-446655440006",
"type": "reseller",
"endpoint": "https://integrator.example/webhooks/proxy-usage",
"secret": "example-signing-secret-not-for-production"
}
Show full responseShow selected fields
{
"id": "550e8400-e29b-41d4-a716-446655440006",
"type": "reseller",
"endpoint": "https://integrator.example/webhooks/proxy-usage",
"secret": "example-signing-secret-not-for-production",
"read_timeout": 5,
"write_timeout": 5,
"retries": 3,
"retry_timeout": 10,
"created": "2026-09-16T12:00:00Z"
}

Replace placeholders in braces with your values. Responses use synthetic example data.Install SDKsAPI reference →

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.

Persist the webhook ID with that secret. List/retrieve let you check configuration; delete the obsolete destination only after a replacement receiver is verified. See retries and conditional writes before automating replacement.

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",
        "is_unlimited_data": false,
        "data_allocated": 10737418240,
        "data_available": 2147483648,
        "data_available_percentage": 20,
        "latest_data_top_up": 5368709120,
        "latest_data_top_up_date": "2026-07-28T09:00:00Z"
      }
    }
  ],
  "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.
order.data_available_percentage
Normalized available traffic from 0 to 100, rounded to two decimal places.
order.latest_data_top_up
Most recent allocation in bytes, with its UTC timestamp inlatest_data_top_up_date.
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 the ledger context, current traffic allowance, normalized available percentage, unlimited status, and latest top-up metadata.

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

Balance fields are produced from Accounting records after the usage batch has been saved durably. For a root order, data_allocated and data_available aggregate the currently usable ledgers. For a virtual child order, they describe that child’s personal allocation and remaining allowance. A sub-user with its own purchased order receives the balance of that order and keeps the owning reseller in user.reseller_id.

Use order.data_available_percentage for low-balance automation. Finite balances are clamped to 0100 and rounded to two decimal places. An unlimited package reports is_unlimited_data: true, a percentage of 100, and JSON null for data_allocated and data_available.

latest_data_top_up is the most recent allocation in bytes and latest_data_top_up_date is its UTC timestamp. Both values are JSON null for legacy orders when an exact historical top-up cannot be established.

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.

Install a compatible SDK first. Read the body as raw bytes before your framework’s JSON parser runs, and pass the X-Signature header unchanged. The SDK helpers below verify before decoding and throw on an invalid signature.

Deliveries have no signed timestamp. Signature verification proves authenticity, not freshness: keep the duplicate and ordering strategy even after verification succeeds. The SDK accepts only the current Base64 signature format.

import { WebhookVerifier } from '@proxyrequest/sdk';
// rawBody: unmodified request bytes; signature: X-Signature header.
export async function readWebhook(rawBody, signature) {
return WebhookVerifier.decodeVerifiedJson(
rawBody,
signature,
'{webhook_secret}',
);
}
  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_available, and order.data_available_percentage. Keep the raw verified delivery for investigation, and design updates so reprocessing the same balance does not send the same warning or 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.