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.renewedbilling_limit.reached,billing_limit.availableuser.updated,user.deletedpreference.updatedapp_access.revokedbeta
The request
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", "…": "…"}
}
}ididentifies the event; use it to skip duplicates.typeis the event type, see the event reference.createdis a Unix timestamp.data.objectis 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.reasonordata.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.
- Split the header at
,and readtand everyv1. - Compute
HMAC-SHA256(secret, t + "." + raw_body)as lowercase hex. - Compare it with each
v1in constant time; one must match. - Reject the request if
tis more than 5 minutes away from the current time (replays).
Always use the raw request body – re-encoded JSON won't match.
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);
}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
idand 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.
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.