Skip to content

Receiving webhooks ​

Anikeen ID sends events about your app's orders, subscriptions and checkout sessions – and, if you opt in, about users, preferences and billing limits – to your webhook endpoints, like Stripe webhooks.

Setting up an endpoint ​

Webhook endpoints are registered for your app by Anikeen: the URL (e.g. https://shop.example.com/webhooks/anikeen) and the events it should receive. Each endpoint has a signing secret whsec_…. A webhook.test event can be sent to check your endpoint.

Use * to receive every event – except the opt-in events, which an endpoint only gets when it selects them explicitly:

  • subscription.renewed
  • billing_limit.reached, billing_limit.available
  • user.updated, user.deleted
  • preference.updated
  • app_access.revoked beta

The request ​

http
POST /webhooks/anikeen HTTP/1.1
Content-Type: application/json
Anikeen-Signature: t=1762500000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Anikeen-Event-Id: 01JEVT…
Anikeen-Event-Type: order.completed

{
  "id": "01JEVT…",
  "type": "order.completed",
  "created": 1762500000,
  "data": {
    "object": {"id": "01JORDER…", "status": "completed", "…": "…"}
  }
}
  • id identifies the event; use it to skip duplicates.
  • type is the event type, see the event reference.
  • created is a Unix timestamp.
  • data.object is the order, subscription, checkout session, … as the API returns it, frozen when the event happened. Some events add fields next to it, e.g. data.previous_attributes, data.period, data.reason or data.changed.

Verifying the signature ​

Anikeen-Signature has the form t=<unix timestamp>,v1=<signature>. The signature is the hex HMAC-SHA256 of "<timestamp>.<raw body>" with your endpoint's signing secret.

  1. Split the header at , and read t and every v1.
  2. Compute HMAC-SHA256(secret, t + "." + raw_body) as lowercase hex.
  3. Compare it with each v1 in constant time; one must match.
  4. Reject the request if t is more than 5 minutes away from the current time (replays).

Always use the raw request body – re-encoded JSON won't match.

php
use Anikeen\Id\Exceptions\SignatureVerificationException;
use Anikeen\Id\Webhooks\Webhook;

try {
    $event = Webhook::constructEvent(
        $request->getContent(),
        $request->header('Anikeen-Signature'),
        config('services.anikeen.webhook_secret'),
    );
} catch (SignatureVerificationException) {
    abort(400);
}
js
import crypto from 'node:crypto'

function verify(rawBody, header, secret, tolerance = 300) {
  let t = null
  const signatures = []

  for (const part of (header ?? '').split(',')) {
    const [key, value] = part.trim().split('=', 2)
    if (key === 't') t = Number(value)
    if (key === 'v1' && value) signatures.push(value)
  }

  if (!t || signatures.length === 0) throw new Error('Missing or malformed signature header')

  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')

  const ok = signatures.some((s) =>
    s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))

  if (!ok || Math.abs(Date.now() / 1000 - t) > tolerance) throw new Error('Invalid signature')
  return JSON.parse(rawBody)
}

Answering ​

  • Answer with a 2xx status within 10 seconds. Redirects and other answers count as failures.
  • Do the work in a queue if it takes longer.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and then every 12 hours: 10 attempts over about three days.

Handling events safely ​

  • Duplicates: an event can arrive more than once. Store id and skip events you handled.
  • Order: events can arrive out of order. Reload the object through the API when the current state matters, or compare versions where the event carries one (user.updated, preference.updated, billing_limit.*).
  • Events are sent after the change is committed: a change that failed sends nothing.
php
if (! ProcessedEvent::firstOrCreate(['id' => $event['id']])->wasRecentlyCreated) {
    return response()->noContent(); // already handled
}

Who receives an event ​

  • Order, subscription and checkout events go to the app that created the order, subscription or session – for a monthly bill, the app that bills it.
  • User, preference, billing limit and app access events go only to apps the user is connected to (or that may read the namespace or limit); see each event.

With the SDK, see Webhooks in Laravel.

Anikeen ID developer documentation