Billing limit
A user can set one monthly limit (gross, in EUR) for their whole open monthly bill across all Anikeen services. A charge that would take the bill above it is refused with 409 billing_limit_reached; exactly reaching the limit is allowed, and 0.00 refuses every charge.
Subscription renewals and usage are still billed, so the bill can end above the limit. Apps should start no new paid service while the limit is reached.
Reading a user's limit (trusted apps)
GET /api/v1/client/users/{user}/billing-limit
Authorization: Bearer APP_TOKEN{
"data": {
"object": "billing_limit",
"user_id": "01j…",
"currency": "EUR",
"basis": "gross",
"limit": "100.00",
"used": "82.40",
"used_by_app": "60.00",
"remaining": "17.60",
"remaining_net": "14.78",
"vat_rate": 19,
"reached": false,
"resets_at": "2026-12-01T02:00:00Z",
"version": 7
}
}| Field | |
|---|---|
limit | the user's limit, null without one |
used | the user's open bills across all Anikeen services; null without a limit |
used_by_app | your app's share; always there. used − used_by_app is "your other Anikeen services" |
remaining | gross room left (max(0, limit − used)), null without a limit |
remaining_net | remaining at the user's VAT rate, rounded down to the cent: a hint only; null without a billing address |
reached | used >= limit; false without a limit |
resets_at | when this month's bills are closed |
version | only goes up; compare with the webhooks |
Amounts are strings with two decimals. The read is advisory – it reserves nothing; the binding check happens when a charge is booked.
Privacy
used (the user's whole open bill) is visible to apps only while the user has set a limit. Without one, limit, used, remaining and remaining_net are null. Apps learn nothing else – no balance, payment method or other apps' figures.
Who may read it: trusted apps that may charge, or that Anikeen allowed to read billing limits, for users connected to them. Others get 403 billing_limit_not_enabled. The other errors are the same as for charges.
$limit = app(\Anikeen\Id\AnikeenId::class)->billingLimitForUser($user->anikeen_id);
$limit->hasLimit(); // false: no limit, every charge fits
$limit->isReached(); // start no new paid service
$limit->remaining(); // "17.60", null without a limit
$limit->remainingNet(); // "14.78", a hint
$limit->allows('9.52'); // would this charge fit right now (advisory)
$limit->resetsAt();
$limit->version();
$limit->used();
$limit->usedByApp();The user's own limit
GET /api/v1/billing (user token) carries the same object as billing_limit – without used_by_app, with used always present – or null while the user can't set one. With the SDK: $user->billingOverview()->billing_limit.
Webhooks
billing_limit.reached and billing_limit.available are sent when the limit is reached or has room again (a raised or removed limit, a void, the monthly reset). They are opt-in (select them for your endpoint; * doesn't include them) and go only to apps that may read the limit of a user connected to them.
data.objectis the billing limit as your app reads it, frozen when the event happened.data.reasonsays why:bookingorlimit_changedforreached;limit_changed,limit_removed,voided,period_resetororder_changedforavailable. The reason is informational; the state is what counts.- Ordering: keep the highest
versionper user and ignore events with a lower or equal one, so a late delivery can't undo a newer state.
use Anikeen\Id\Webhooks\EventType;
match ($event['type']) {
EventType::BILLING_LIMIT_REACHED, EventType::BILLING_LIMIT_AVAILABLE => Customer::syncBillingLimit(
$object['user_id'],
reached: $event['type'] === EventType::BILLING_LIMIT_REACHED,
version: $object['version'],
),
default => null,
};The limit is checked live when a charge is booked, so a late or missed webhook never lets a charge through; poll the limit if you need the current state.