Skip to content

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 ​

TokenMethodPath
the user's token, any app, subscriptionsPOST/api/v1/checkout-sessionscreate for the token's user
subscriptions:read is enough to showGET/api/v1/checkout-sessions/{id}show (same user and app only)
subscriptionsPOST/api/v1/checkout-sessions/{id}/cancelwithdraw
app token, trusted, subscriptions:clientPOST/api/v1/client/checkout-sessionscreate for user_id
GET/api/v1/client/checkout-sessions/{id}show
POST/api/v1/client/checkout-sessions/{id}/cancelwithdraw an open session

Creating a session ​

Field
subscriptionsrequired, 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_urlwhere the user returns; the origin of one of your redirect URIs (422 otherwise)
metadatayour own data (an object), returned as is
idempotency_keya retry with the same key returns the first session (200)
withdrawal_waiver_requiredask for the early start
user_idapp-token endpoint only: the user
php
$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):

json
{
  "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.

php
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 ​

Statuserror
403user_token_required, confirmation_required
404checkout_session_not_found
409checkout_session_completed, checkout_session_cancelled, checkout_session_expired, idempotency_key_conflict
422validation

Anikeen ID developer documentation