Webhooks
Webhooks tell your server about events as they happen. Add endpoints per application under Applications → Webhooks, or with the API.
Events
game.started | A player started a paid (or test) session |
game.completed | A round finished and a score was recorded |
score.submitted | A score was submitted |
credits.low | Wallet balance dropped below the low-balance threshold |
credits.depleted | Wallet balance reached zero |
application.created | A new application was created on your account |
api_key.revoked | An API key was revoked |
api_key.rotated | An API key was rotated |
ping | Sent by the "Send test" button |
Payload
POST /your-endpoint
Content-Type: application/json
ACBIZ-Event-Id: evt_9b1c…
ACBIZ-Event-Type: game.completed
ACBIZ-Signature: t=1790150000,v1=5f2a…
{
"id": "evt_9b1c…",
"type": "game.completed",
"created": 1790150000,
"livemode": true,
"application": 12,
"data": {
"session_id": "a41f…",
"game": { "slug": "mental-math-sprint", "title": "Mental Math Sprint" },
"player_id": "student-1042",
"player_verified": true,
"score": 1280,
"level": 4,
"is_personal_best": true
}
}Verifying the signature
Compute HMAC-SHA256(secret, t + "." + raw_body) and compare it with v1 in constant time. Reject timestamps older than 5 minutes to stop replays. Always use the raw request body.
PHP
$secret = getenv("ACBIZ_WEBHOOK_SECRET"); // whsec_…
$body = file_get_contents("php://input");
$header = $_SERVER["HTTP_ACBIZ_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $header), $sig); // t=…&v1=…
$valid = isset($sig["t"], $sig["v1"])
&& abs(time() - (int) $sig["t"]) <= 300
&& hash_equals(hash_hmac("sha256", $sig["t"] . "." . $body, $secret), $sig["v1"]);
if (!$valid) { http_response_code(400); exit; }
$event = json_decode($body, true);
// Process idempotently: you may receive the same event id more than once.
http_response_code(200);Node.js
const crypto = require("crypto");
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - parts.t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Delivery & retries
- Respond with any
2xxwithin 5 seconds. Do slow work after responding. - Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 6 h (6 attempts total). You can also retry from the dashboard.
- An endpoint that fails 20 times in a row is disabled automatically and you're notified.
- Endpoints must be public
https://URLs. Redirects are not followed. - Events can arrive more than once or out of order — use
idto de-duplicate.