Checkout sessions
A checkout session lets the user confirm several subscriptions at once on Anikeen ID: it bundles new subscriptions (or existing drafts of your app) of one user. The user sees every item with its price and the total of the first period, confirms – all subscriptions start, or none – or cancels, and returns to your return_url.
Open sessions expire after 24 hours with their drafts. Trusted apps can still check subscriptions out directly; sessions are opt-in for them.
Endpoints
| Token | Method | Path | |
|---|---|---|---|
the user's token, any app, subscriptions | POST | /api/v1/checkout-sessions | create for the token's user |
subscriptions:read is enough to show | GET | /api/v1/checkout-sessions/{id} | show (same user and app only) |
subscriptions | POST | /api/v1/checkout-sessions/{id}/cancel | withdraw |
app token, trusted, subscriptions:client | POST | /api/v1/client/checkout-sessions | create for user_id |
GET | /api/v1/client/checkout-sessions/{id} | show | |
POST | /api/v1/client/checkout-sessions/{id}/cancel | withdraw an open session |
Creating a session
| Field | |
|---|---|
subscriptions | required, 1–50 items: each either {"id": "<a draft of your app and this user>"} or the fields of a new subscription (name, unit, price, description, group, payload, ends_at, monthly_cap, trial and discount fields) |
return_url | where the user returns; the origin of one of your redirect URIs (422 otherwise) |
metadata | your own data (an object), returned as is |
idempotency_key | a retry with the same key returns the first session (200) |
withdrawal_waiver_required | ask for the early start |
user_id | app-token endpoint only: the user |
$session = $request->user()->appCheckoutSessions()->create([ // app token; or ->checkoutSessions() with the user's token
'return_url' => route('builder.done'),
'metadata' => ['cart' => $cart->id],
'idempotency_key' => "cart:{$cart->id}",
'subscriptions' => $cart->items->map(fn ($item) => [
'group' => $team->id,
'name' => $item->product->name,
'description' => $item->label,
'unit' => $item->unit, // hour, day, week, month, year
'price' => $item->priceAfterCoupon(), // net, per unit
'payload' => ['cart_item' => $item->id],
])->all(),
]);
$cart->update(['checkout_session_id' => $session->id, 'subscription_ids' => $session->subscriptionIds()]);
return redirect($session->confirmationUrl());
// later: https://app.example.com/builder/done?status=completed&checkout_session=01K…Response (data):
{
"object": "checkout_session",
"id": "01K…",
"user_id": "01j…",
"oauth_client_id": "9f0c…",
"status": "open",
"return_url": "https://app.example.com/builder/done",
"metadata": {"cart": "42"},
"confirmation_url": "https://id.anikeen.com/checkout/sessions/01K…",
"subscription_ids": ["01K…", "01K…"],
"subscriptions": [{"id": "01K…", "status": "draft"}],
"expires_at": "…",
"completed_at": null,
"cancelled_at": null,
"created_at": "…"
}Confirmation
On the confirmation page the user sees every item with its price per unit (VAT included), the total of the first period, the billing address and payment method (both can be added in place), and one payment agreement naming all subscriptions. Confirming starts all subscriptions in one step; if one can't start (e.g. no billing address), none does and the session stays open. Subscriptions are billed in arrears, so nothing is charged at that moment.
The user returns to return_url?status=completed|cancelled&checkout_session=<id>. Check the status through the API or the webhooks, never the query string.
Status
status is open, completed, cancelled or expired (SDK: isOpen(), isCompleted(), isCancelled() for cancelled or expired).
While a session is open, its subscriptions can't be checked out or repriced by the app (409 in_checkout_session / not_changeable; third-party apps get 403 confirmation_required with the session's URL). Cancelling – by the user or the app – cancels all drafts.
Webhooks
subscription.activated / subscription.cancelled are sent per subscription, then checkout.completed, checkout.cancelled or checkout.expired with the session object above.
use Anikeen\Id\Webhooks\EventType;
match ($event['type']) {
EventType::CHECKOUT_COMPLETED => Provisioner::provision($event['data']['object']['subscriptions']), // all running
EventType::CHECKOUT_CANCELLED, EventType::CHECKOUT_EXPIRED => Cart::release($event['data']['object']['metadata']['cart'] ?? null),
default => null,
};
app(\Anikeen\Id\AnikeenId::class)->appCheckoutSessions()->find($id)?->isCompleted(); // any user's session
$session->refresh()->subscriptions(); // AppSubscription objects
$session->cancel(); // withdraw an open session and its drafts (e.g. the cart changed)Errors
| Status | error |
|---|---|
| 403 | user_token_required, confirmation_required |
| 404 | checkout_session_not_found |
| 409 | checkout_session_completed, checkout_session_cancelled, checkout_session_expired, idempotency_key_conflict |
| 422 | validation |