Basics
Webhooks
Instead of polling, add an HTTPS URL in the dashboard and we POST an event to it when something happens on your account. Every request is signed, so you can check it came from us.
Events
| Event | When | Needs |
|---|---|---|
order.paid | An order on your account was paid and approved. | orders:read |
balance.changed | Your balance went up or down (top-up, purchase, refund, bonus). | balance:read |
boost.status | An Hour Boost plan or its Steam account changed status (started, paused, stopped, expired). | boost:read |
ping | You press Test in the dashboard. | any |
Your app must have the permission next to an event to subscribe to it. Real events are sent only for approved apps; test events work right away.
Request
A POST with a JSON body and these headers:
| Header | Value |
|---|---|
Adolfjon-Event | The event type, e.g. order.paid. |
Adolfjon-Delivery | The event id (evt_…). The same on retries: use it to skip duplicates. |
Adolfjon-Signature | t=<unix time>,v1=<hex HMAC-SHA256> |
User-Agent | Adolfjon-Webhooks/1 |
order.paid
{
"id": "evt_8Jd2kQ0xLm4Pz7Rt1VbN",
"type": "order.paid",
"created_at": "2026-10-26T14:05:03+05:00",
"app_id": 4,
"data": {
"id": "A7K2M9",
"status": "approved",
"refunded": false,
"cancelled": false,
"payment_method": "balance",
"total_uzs": 59000,
"discount_uzs": 0,
"coupon": null,
"items": [
{
"product_id": 341,
"title": "VIP 30 days",
"type": "token",
"quantity": 1,
"price_uzs": 59000,
"delivery": "token"
}
],
"url": "https://adolfjon.uz/store/orders/A7K2M9",
"created_at": "2026-10-26T14:05:00+05:00",
"updated_at": "2026-10-26T14:05:00+05:00"
}
}balance.changed
{
"id": "evt_8Jd2kQ0xLm4Pz7Rt1VbN",
"type": "balance.changed",
"created_at": "2026-10-26T14:05:03+05:00",
"app_id": 4,
"data": {
"id": 90412,
"type": "topup",
"amount": 150000,
"balance_after": 245000,
"reference": {
"type": "humo",
"id": 7712
},
"created_at": "2026-10-26T14:05:00+05:00"
}
}boost.status
{
"id": "evt_8Jd2kQ0xLm4Pz7Rt1VbN",
"type": "boost.status",
"created_at": "2026-10-26T14:05:03+05:00",
"app_id": 4,
"data": {
"id": 4401,
"tier": "custom",
"status": "active",
"hours": {
"total": 1000,
"used": 312.75,
"left": 687.25
},
"max_games": 32,
"games": [
730,
570,
440
],
"card_farming": true,
"account": {
"status": "active",
"steam_id": "76561198000000000",
"boosting": true,
"started_at": "2026-10-26T14:05:00+05:00",
"last_tick_at": "2026-10-26T14:05:00+05:00"
},
"created_at": "2026-10-26T14:05:00+05:00",
"previous": {
"status": "active",
"account_status": "paused"
}
}
}The data object has the same shape as the matching API endpoint (order, balance history, Hour Boost). boost.status adds previous.
Check the signature
Compute HMAC-SHA256 of <t>.<raw body> with your whsec_… secret and compare it with v1. Reject requests older than 5 minutes. Use the raw body, before any JSON parsing.
<?php
$secret = 'whsec_YOUR_SECRET';
$body = file_get_contents('php://input');
$hdr = $_SERVER['HTTP_ADOLFJON_SIGNATURE'] ?? '';
preg_match('/t=(\d+),v1=([a-f0-9]{64})/', $hdr, $m);
$ok = $m && abs(time() - (int)$m[1]) < 300
&& hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, $secret), $m[2]);
if (!$ok) { http_response_code(400); exit; }
$event = json_decode($body, true);
http_response_code(200);
if ($event['type'] === 'order.paid') {
error_log('paid: ' . $event['data']['id']);
}import crypto from 'node:crypto';
import http from 'node:http';
const SECRET = 'whsec_YOUR_SECRET';
http.createServer((req, res) => {
let body = '';
req.on('data', (c) => (body += c));
req.on('end', () => {
const m = /t=(\d+),v1=([a-f0-9]{64})/.exec(req.headers['adolfjon-signature'] || '');
const want = m && crypto.createHmac('sha256', SECRET).update(`${m[1]}.${body}`).digest('hex');
const fresh = m && Math.abs(Date.now() / 1000 - Number(m[1])) < 300;
if (!m || !fresh || !crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]))) {
res.writeHead(400).end();
return;
}
const event = JSON.parse(body);
res.writeHead(200).end();
console.log(event.type, event.data);
});
}).listen(8080);import hashlib, hmac, json, re, time
from flask import Flask, request, abort
SECRET = b"whsec_YOUR_SECRET"
app = Flask(__name__)
@app.post("/adolfjon/webhook")
def webhook():
body = request.get_data()
m = re.search(r"t=(\d+),v1=([a-f0-9]{64})", request.headers.get("Adolfjon-Signature", ""))
if not m or abs(time.time() - int(m[1])) > 300:
abort(400)
want = hmac.new(SECRET, m[1].encode() + b"." + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, m[2]):
abort(400)
event = json.loads(body)
print(event["type"], event["data"])
return "", 200Answer fast, retry safe
- Return any
2xxwithin 6 seconds. Do slow work after you answer. - Anything else (or no answer) is retried after 1, 5, 30, 120 and 360 minutes. After that the delivery is marked failed.
- After 30 failed deliveries in a row we turn the webhook off and message you. Fix it, then turn it on in the dashboard.
- Events can arrive more than once or out of order. Store the
Adolfjon-Deliveryid and ignore repeats. - Events are sent within about a minute.
Limits and safety
- Up to 3 webhooks per app. HTTPS only, port 443 or 8443, no redirects followed.
- Private, local and reserved addresses are refused.
- Order events never include delivered items or keys, the same as the API.
- Lost the secret? Show it again (needs a quick re-confirm) or rotate it in the dashboard.