Skip to content

Webhooks

Webhooks tell your server about events as they happen. Add endpoints per application under Applications → Webhooks, or with the API.

Events

game.startedA player started a paid (or test) session
game.completedA round finished and a score was recorded
score.submittedA score was submitted
credits.lowWallet balance dropped below the low-balance threshold
credits.depletedWallet balance reached zero
application.createdA new application was created on your account
api_key.revokedAn API key was revoked
api_key.rotatedAn API key was rotated
pingSent 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 2xx within 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 id to de-duplicate.