Skip to content

App subscriptions (trusted apps) ​

subscriptions() acts with the user's own access token and stops working when the user's login expired. Trusted apps can manage a user's subscriptions with the app's own token (client credentials, scope subscriptions:client) instead – e.g. to bill servers by the hour from a backend job.

Your app sees the subscriptions it created; others answer 404.

Endpoints ​

All under /api/v1/client/subscriptions:

MethodPath
GET/list; filters user_id, group (exact), active, cancelled, per_page
POST/create, see below
GET/{id}show
PUT/{id}change a subscription that wasn't checked out yet (afterwards only the description)
PUT/{id}/checkoutstart billing: starts_at?, bill_elapsed_periods?, max_net_amount?, withdrawal_waiver?
PUT/{id}/changechange a running subscription, see below
DELETE/{id}/changedrop the change scheduled for the period end
PUT/{id}/pausepause
PUT/{id}/resumeresume
PUT/{id}/revokecancel; refund credits usage not yet charged
POST/monthly-capsset or lower the monthly cap of running subscriptions
GET/{id}/usagemetered usage records
POST/{id}/usagereport metered usage
DELETE/{id}/usage/{record}void a usage record

Create fields: user_id, group, name, description, unit, price, monthly_cap?, predecessor_id?, takeover?, vat_rate?, payload?, ends_at?, trial fields, discount? (see Trials & discounts) and idempotency_key? – a repeated key returns the first subscription (200).

With the SDK ​

php
$subscriptions = $team->owner->appSubscriptions(); // nothing is requested yet

$subscription = $subscriptions->create([
    'group' => $team->id,
    'name' => 'Cloud Server S',
    'unit' => 'hour',
    'price' => 0.0137,
    'idempotency_key' => "server:{$pendingServer->id}", // retries return the same subscription
]);

$subscription->checkout();             // bill the first period
$subscription->pause();                // expires when the paid period ends
$subscription->resume();               // expired subscriptions start a new period
$subscription->revoke(refund: false);  // cancel

$subscriptions->find($id);             // null if unknown or not this user's
$subscriptions->all(['group' => $team->id, 'active' => true]);

The user's Anikeen ID is read from the billable's anikeen_id attribute (AnikeenId::useUserIdField('…') to change it).

Starting from an earlier date ​

checkout with starts_at books a service that is already running, e.g. a domain registered before the customer came to your app. If the first period from that date is still running, it is billed now and renews when it ends.

An older start is refused with 409 period_already_ended, unless you pass bill_elapsed_periods: true with max_net_amount (net, up to two decimals; both together):

  • every period that has ended plus the running one is billed now, as one line with the full service period. It lands on the user's monthly bill of the current month and is invoiced and charged with the next monthly bill; nothing is back-dated.
  • the start may be at most about three years ago (422 otherwise).
  • if the periods come to more than max_net_amount, nothing is booked: 409 elapsed_amount_exceeded with the real amount in net_amount (e.g. "20.76"), so you can show it and retry.
  • 409 elapsed_periods_not_allowed when the subscription can't be billed this way (e.g. it ran before or replaces another one).
  • Hourly and daily subscriptions with a monthly cap are capped per calendar month for the elapsed months; max_net_amount limits the total.
php
use Anikeen\Id\Exceptions\ApiException;

try {
    $subscription->checkout(startsAt: $startsAt, billElapsedPeriods: true, maxNetAmount: $confirmed);
} catch (ApiException $e) {
    if ($e->error === 'elapsed_amount_exceeded') {
        $actual = $e->body()->net_amount; // ask again with the real amount
    } else {
        throw $e;
    }
}

Changing running subscriptions ​

PUT …/{id} changes a subscription before its checkout. PUT …/{id}/change changes price, unit, name, description or discount of a running subscription, like a plan change (send only what changes), with apply:

php
use Anikeen\Id\Resources\AppSubscription;

// upgrade now, prorated
$subscription->change(['apply' => AppSubscription::APPLY_NOW, 'price' => '0.0274', 'name' => 'Cloud Server M']);

// downgrade from the next period on
$subscription->change(['apply' => AppSubscription::APPLY_PERIOD_END, 'price' => '4.99']);
$subscription->scheduled_change;       // {applies_at, name, description, unit, price, discount, created_at}
$subscription->cancelScheduledChange(); // keep the current values after all
  • now credits the unused part of the current period at the rate it was billed at and charges the rest at the new price, both on the user's open bill. A new unit starts a new, fully billed period now. A downgrade whose credit would exceed the open bill is refused with 409 credit_exceeds_bill: schedule it for the period end instead. 409 proration_not_possible when the current period wasn't billed to the subscription; period_end works.
  • period_end changes nothing now; the subscription has scheduled_change and the next period is billed at the new values. A new period_end change replaces the scheduled one, now drops it.
  • During a free trial nothing was billed: both just change what the trial ends in.
  • discount replaces the discount from the next billed period on, null ends it. When the unit changes, a discount that counts in periods or an amount_off has to be sent again (422).
  • Subscriptions with a monthly cap change only with period_end.
  • Only running subscriptions can change: 409 not_changeable for drafts (use PUT …/{id}), paused, cancelling, expired and cancelled ones.

When a change takes effect, Anikeen ID sends subscription.updated with the old values in data.previous_attributes. Telling the user about a price increase (and getting consent where needed) is up to your app.

Monthly caps ​

Hourly and daily subscriptions can have a monthly_cap (net): billed per unit, but per calendar month at most the cap. A resize – a new subscription with predecessor_id naming the one it replaces – shares the cap with its predecessor within the month.

POST …/monthly-caps gives running subscriptions a cap or a lower one – only in the customer's favour (never a higher cap, never none). It applies to usage booked from then on.

json
{"subscriptions": [{"id": "01K…", "monthly_cap": "5.00"}], "notify": true}
Field
subscriptions1–100 items of {id, monthly_cap}
notifytrue: each owner gets one mail listing their changed subscriptions
reasonoptional

Each subscription answers with status updated, unchanged or refused (with error: subscription_not_found, not_running, invalid_monthly_cap, monthly_cap_not_lowered), previous_monthly_cap, monthly_cap and notified.

Transfers ​

When a product changes hands in your app (e.g. between teams), the recipient can take over billing with a subscription of their own: create it with takeover: true and predecessor_id = the previous holder's running subscription (same app, another user, same unit). The recipient confirms it like a purchase (e.g. in a checkout session). On checkout the predecessor is revoked without refund and the new subscription continues from the end of the predecessor's paid period: no period is billed twice, none is left out.

Metered usage ​

Usage your app measures itself (traffic above what is included, storage) is reported per subscription, meter and period once the period has ended. It becomes a line of its own on the user's open monthly bill (usage of October reported on November 1st is on November's bill; the line states October as its period). It is not subject to the monthly cap.

http
POST /api/v1/client/subscriptions/{id}/usage

{
  "meter": "server_traffic",
  "period_start": "2026-10-01T00:00:00Z",
  "period_end": "2026-11-01T00:00:00Z",
  "quantity": "12.5",
  "unit": "gigabyte",
  "unit_price": "0.01",
  "idempotency_key": "traffic:server-42:2026-10"
}
Field
meter[a-z0-9_.-], e.g. server_traffic
period_start, period_endthe period; it must have ended
quantity> 0, up to 4 decimals
unitgigabyte (10⁹ bytes) or gibibyte (2³⁰ bytes)
unit_pricenet, up to 6 decimals
descriptionoptional, default: the subscription's
idempotency_keyrequired; a retry returns the first record (200)

The response is a usage_record: {object, id, subscription_id, meter, period_start, period_end, quantity, unit, unit_price, amount, description, idempotency_key, status, order_id, order_item_id, voided_at, created_at} with status open, charged or voided.

Another record for the same or an overlapping period of the meter answers usage_already_recorded (with its usage_record_id). DELETE …/usage/{record} voids a record while its bill is open; afterwards 409 usage_already_charged. Revoking the subscription leaves reported usage charged.

Error codes ​

Failures are {message, error} with a stable error:

subscription_not_found (404), already_active, already_cancelled, not_active, already_paused, already_resumed, billing_address_missing, not_changeable, idempotency_key_conflict, in_checkout_session, trial_already_used, proration_not_possible, credit_exceeds_bill, period_already_ended, elapsed_periods_not_allowed, elapsed_amount_exceeded (with net_amount), subscription_conflict (409).

php
try {
    $subscription->revoke();
} catch (ApiException $e) {
    if ($e->isNotFound() || $e->error === 'already_cancelled') {
        // already gone
    } elseif ($e->isTemporary()) {
        // retry later
    } else {
        throw $e;
    }
}

SDK coverage

Monthly caps, transfers and metered usage have no dedicated SDK methods yet. Call them with the app token through asApp(), which returns a Result:

php
$result = app(\Anikeen\Id\AnikeenId::class)->asApp()
    ->post("v1/client/subscriptions/{$subscriptionId}/usage", $usage);

Anikeen ID developer documentation