Event reference
All event types with their SDK constant on Anikeen\Id\Webhooks\EventType. Opt-in events are only sent to endpoints that select them explicitly (* doesn't include them).
| Type | Constant | Opt-in |
|---|---|---|
order.created | ORDER_CREATED | |
order.completed | ORDER_COMPLETED | |
order.payment_failed | ORDER_PAYMENT_FAILED | |
order.cancelled | ORDER_CANCELLED | |
order.shipped | ORDER_SHIPPED | |
order.refunded | ORDER_REFUNDED | |
subscription.created | SUBSCRIPTION_CREATED | |
subscription.activated | SUBSCRIPTION_ACTIVATED | |
subscription.paused | SUBSCRIPTION_PAUSED | |
subscription.resumed | SUBSCRIPTION_RESUMED | |
subscription.cancelled | SUBSCRIPTION_CANCELLED | |
subscription.expired | SUBSCRIPTION_EXPIRED | |
subscription.trial_will_end | SUBSCRIPTION_TRIAL_WILL_END | |
subscription.discount_ended | SUBSCRIPTION_DISCOUNT_ENDED | |
subscription.updated | SUBSCRIPTION_UPDATED | |
subscription.renewed | SUBSCRIPTION_RENEWED | yes |
checkout.completed | CHECKOUT_COMPLETED | |
checkout.cancelled | CHECKOUT_CANCELLED | |
checkout.expired | CHECKOUT_EXPIRED | |
billing_limit.reached | BILLING_LIMIT_REACHED | yes |
billing_limit.available | BILLING_LIMIT_AVAILABLE | yes |
user.updated | USER_UPDATED | yes |
user.deleted | USER_DELETED | yes |
preference.updated | PREFERENCE_UPDATED | yes |
app_access.revoked beta | APP_ACCESS_REVOKED | yes |
webhook.test | TEST |
Order events
data.object is the order. It carries the group and metadata your app set, both addresses, oauth_client_id (the app that bills the order), the order's invoice (null before payment; on order.completed usually still status: pending while the PDF is rendered – pdf_url stays valid) and items with type, image_url, subscription_id, period_start, period_end, net_amount, vat_amount, gross_amount, description, meter, charge_id, monthly_cap and discount.
| Type | When |
|---|---|
order.created | an order of your app was created (not for monthly bills) |
order.completed | the order was paid: fulfil it now |
order.payment_failed | a charge was declined; it is retried automatically |
order.cancelled | the user cancelled, a draft expired, or the order was revoked |
order.shipped | a shipment was recorded; the order carries its shipments |
order.refunded | money was given back (a refund or a partial cancellation, with a credit note) |
order.refunded: the order has refunded_total and refunds: [{id, kind, status, method, reason, net_total, vat_total, gross_total, credit_note_number, created_at, items: [{order_item_id, name, units, net_amount, gross_amount, vat_rate}]}] (kindgoodwill or cancellation). The order's totals stay what was invoiced. A full refund also cancels the order, so order.cancelled is sent too; don't rely on the order of the two events.
Subscription events
data.object is the subscription.
| Type | When | Extra data |
|---|---|---|
subscription.created | a subscription was created | |
subscription.activated | it started (checkout, confirmation or a trial start) | |
subscription.paused | it was paused | |
subscription.resumed | it was resumed | |
subscription.cancelled | it was cancelled | reason: trial_ended_without_payment_method when the billing ended it, else null |
subscription.expired | a paused subscription ran out | |
subscription.trial_will_end | a free trial ends in a few days | payment_method_missing |
subscription.discount_ended | the last discounted period was billed | |
subscription.updated | a change of a running subscription took effect | previous_attributes: the old values of the changed fields |
subscription.renewed | a period was booked (opt-in, see below) | period |
subscription.renewed
Sent once for every period booked on the user's monthly bill, the first one included, so you can follow every charge of a subscription from this event alone. Hourly subscriptions renew every hour, so this event is opt-in.
{
"type": "subscription.renewed",
"data": {
"object": {"id": "01K…", "status": "active", "…": "…"},
"period": {
"object": "subscription_period",
"subscription_id": "01K…",
"period_start": "2026-11-01T00:00:00Z",
"period_end": "2026-12-01T00:00:00Z",
"periods": 1,
"first_period": false,
"unit": "month",
"order_id": "01K…",
"order_item_id": "01K…",
"items": [{"order_item_id": "01K…", "units": 1, "unit_price": "6.714286", "net_amount": "6.71", "monthly_cap": null, "discount": null}],
"currency_iso": "EUR",
"net_amount": "6.71"
}
}
}period_endis exclusive.periods> 1 when several periods were booked at once (e.g. elapsed periods at checkout).net_amountis what this booking adds (net, after a discount).itemsare the lines it was booked on (with a monthly cap also the rest of the cap and the free units at price 0; with a discount also the negative discount line).first_periodistruefor the subscription's first paid period – the checkout (sent aftersubscription.activated), also with elapsed periods, or the first period after a free trial – andfalsefor every later one (renewals, resuming an expired subscription, a unit change applied right away).
if ($event['type'] === EventType::SUBSCRIPTION_RENEWED) {
$period = $event['data']['period'];
Commission::record($period['subscription_id'], $period['net_amount'], first: $period['first_period']);
}Checkout events
data.object is the checkout session. Sent after the events of its subscriptions.
| Type | When |
|---|---|
checkout.completed | the user confirmed: all subscriptions are running |
checkout.cancelled | the user or your app cancelled the session; its drafts were cancelled |
checkout.expired | it wasn't confirmed within 24 hours; its drafts were cancelled |
Billing limit events
billing_limit.reached and billing_limit.available (opt-in): data.object is the billing limit as your app reads it, data.reason says why. Sent only to apps that may read the limit of a user connected to them. Ignore events whose version is lower than or equal to the one you hold.
User events
Opt-in, sent only to apps the user is connected to.
user.updated
Sent for changes to name, username, the verified email address, email_verified and the profile picture, from any source (the user's settings, the profile API, support). Untrusted apps get it only while the user has given them a token with user, user:read or *; the app that made the change gets it too.
{
"id": "01JEVT…",
"type": "user.updated",
"created": 1762500000,
"data": {
"object": {
"object": "user",
"id": "01JUSER…",
"name": "Erika Mustermann",
"username": "erika",
"email": "erika@example.com",
"email_verified": true,
"avatar": "https://id.anikeen.com/avatars/01JUSER…",
"version": 8
},
"changed": ["name", "avatar"]
}
}data.objectis the full profile;changednames the changed fields (never old values).- A new email address arrives only once it is verified:
emailisnullwhile the address isn't, and a requested change sends nothing until the user confirms it (thenchangedis["email", "email_verified"]or["email"]). avataris a stable URL;avatarinchangedmeans the picture behind it changed: drop a cached copy.- Versions:
versiongrows with every such change (profile_versionin the REST shapes). Keep the highest version you have seen and ignore older ones. Catch up on a missed event withGET /api/v1/client/users/{user}. - Trusted apps also get
role(androleinchanged); a change of the role alone is sent to them only.
user.deleted
{"data": {"object": {"object": "user", "id": "01JUSER…", "deleted": true, "deleted_at": "2026-11-07T10:00:00+00:00"}}}Final and sent once: delete or anonymise your copy of the account (and what you keep only for it), and stop acting for the user. No other event about the user follows it. If a user.updated arrives later for the same ID, support restored the account: treat it as a returning user.
preference.updated
Opt-in, sent to trusted apps that may read the namespace – never to the app that made the change.
{
"data": {
"object": {
"object": "preference",
"user_id": "01JUSER…",
"namespace": "acme",
"key": "onboarding",
"version": 1043,
"deleted": false,
"updated_at": "2026-11-06T10:00:00Z",
"updated_by": {"type": "user", "client_id": "9f0c…"}
}
}
}Thin, never the value: fetch the preference again, or drop your copy when deleted is true. Ignore events with a lower version than the one you hold (versions are per user). Nothing is sent for a write that changes nothing, nor when the account is deleted (user.deleted covers that).
app_access.revoked beta
A user removed an app's access in Anikeen ID (Settings → Connected apps), or Anikeen revoked the app or no longer trusts it: the app's tokens for that user stop working.
{
"data": {
"object": {
"object": "app_access",
"user_id": "01JUSER…",
"client": {"id": "9f0c…", "name": "Example App"},
"revoked_at": "2026-11-09T10:00:00+00:00"
}
}
}- Opt-in, sent only to trusted apps the user is connected to, one event per app – never to the app that lost access (it learns it from
401answers and a failing refresh). - Meant for apps that hold data another app managed for the user, e.g. the Anikeen Console releases what the revoked app managed for the user.
- Not sent when the user had no working token of the app any more.
webhook.test
A test event to check your endpoint. Answer it with a 2xx like any other event.