user
Delivers matching events for the associated user context. Use it for a direct account or one intentionally scoped consumer.
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.
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}'import { ProxyRequestClient } from "@proxyrequest/sdk";
const client = ProxyRequestClient.withApiKey( "{api_key}", { baseUrl: "https://{api_host}/api/v1" });
const result = await client.webhooks.create({ body: { "type": "reseller", "endpoint": "{webhook_url}", "read_timeout": 5, "write_timeout": 5, "retries": 3, "retry_timeout": 10 },});from proxyrequest_sdk.models import WebhookCreateRequestfrom proxyrequest_sdk import Client
with Client.with_api_key( "{api_key}", base_url="https://{api_host}/api/v1") as client: result = client.webhooks.create( body=WebhookCreateRequest.from_dict({ "type": "reseller", "endpoint": "{webhook_url}", "read_timeout": 5, "write_timeout": 5, "retries": 3, "retry_timeout": 10 }), )<?phprequire __DIR__ . '/vendor/autoload.php';
use ProxyRequest\Client;use ProxyRequest\Dto\WebhookCreateRequest;
$client = Client::withApiKey( '{api_key}', 'https://{api_host}/api/v1');
$result = $client->webhooks()->create( webhookCreateRequest: new WebhookCreateRequest([ 'type' => 'reseller', 'endpoint' => '{webhook_url}', 'readTimeout' => 5, 'writeTimeout' => 5, 'retries' => 3, 'retryTimeout' => 10 ]),);Selected fields
{ "id": "550e8400-e29b-41d4-a716-446655440006", "type": "reseller", "endpoint": "https://integrator.example/webhooks/proxy-usage", "secret": "example-signing-secret-not-for-production"}{ "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 →
| Field | Allowed by the public schema | Default |
|---|---|---|
type | user, reseller, or system | Required |
endpoint | Public URI, maximum 500 characters | Required |
read_timeout | 1–60 seconds | 5 |
write_timeout | 1–60 seconds | 5 |
retries | 0–10 | 3 |
retry_timeout | 1–300 seconds | 10 |
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.1Content-Type: application/jsonX-Signature: BASE64_HMAC_SHA256{
"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"
}null for root and unlimited orders.0 to 100, rounded to two decimal places.latest_data_top_up_date.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 0–100 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}', );}from proxyrequest_sdk import WebhookVerifier
# raw_body: unmodified request bytes; signature: X-Signature header.def read_webhook(raw_body: bytes, signature: str) -> dict: return WebhookVerifier.decode_verified_json( raw_body, signature, "{webhook_secret}", )<?phprequire __DIR__ . '/vendor/autoload.php';
use ProxyRequest\Webhook\WebhookVerifier;
$rawBody = file_get_contents('php://input');if ($rawBody === false) { throw new RuntimeException('Cannot read webhook body');}$payload = WebhookVerifier::decodeVerifiedJson( $rawBody, $_SERVER['HTTP_X_SIGNATURE'] ?? '', '{webhook_secret}',);// Standalone Go example; the SDK helpers above cover JS/TS, Python and PHP.import ( "crypto/hmac" "crypto/sha256" "encoding/base64")
func verifyWebhook(body []byte, signature string, secret []byte) bool { received, err := base64.StdEncoding.Strict().DecodeString(signature) if err != nil || len(secret) == 0 || len(received) != sha256.Size || base64.StdEncoding.EncodeToString(received) != signature { return false }
mac := hmac.New(sha256.New, secret) _, _ = mac.Write(body) return hmac.Equal(received, mac.Sum(nil))}X-Signature and reject a missing or invalid value.events[], user, package, and order before reading their fields.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.
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.