Skip to main content
Commerce turns products into access. The common flow is:
  1. Create a product.
  2. Add one or more prices.
  3. Attach the product to a hub checkout display.
  4. Start checkout as a member.
  5. Webhooks update orders, subscriptions, payments, and entitlements.

Admin setup

Products are team-scoped:
Prices live under a product:
Payment accounts connect the team to Stripe Connect:

Member checkout

The member-facing checkout route starts a hosted checkout session:
When Stripe sends the buyer back, read the session status. The route needs no auth, because the buyer usually has no session yet:
The checkout_session_statuses resource carries payment_status and status. Once the session is complete and this hub holds an order for it, it also carries email: the buyer’s address with the local part cut to its first character (jane.doe@example.com comes back as j***@example.com), enough to say “we sent a link to j***@example.com”. email is null when the address can’t be resolved, and it never contains the full address. It comes only from the order this hub holds for that session, never from Stripe. For a session that hasn’t completed, or one this hub has no order for, the key is left out, so treat a missing email the same as null. After payment, use the member billing routes:

When a renewal payment fails

When a renewal payment fails, Stripe keeps retrying it for a while and the subscription’s status is past_due. The member keeps access during that time. Each subscriptions resource tells you when to ask the member to update their payment method:
  • payment_update_needed is true while the status is past_due and payment collection is not paused. A paused collection is the hub owner’s choice and leaves the member nothing to fix, so it reads false. Once the payment goes through, the status returns to active and the flag to false. When the retries run out and Stripe moves the subscription on (to unpaid or canceled, depending on the account’s retry settings), the flag is false too, by design: the prompt covers the retry window only, and by then the member’s access has ended, which status shows.
  • last_payment_failed_at, last_payment_failed_amount (in the currency’s minor units) and last_payment_failed_currency describe the latest failed attempt. They are null unless payment_update_needed is true and the attempt is not older than current_period_start: an older attempt belongs to an earlier renewal that was settled. There is no upper bound. An attempt after current_period_end is a later renewal that failed before the subscription’s new period was recorded, and the member still owes it. The details can also be null while payment_update_needed is true, when no failed attempt is on record yet. Amount and currency are null together.
All four attributes are optional: a client should read a missing payment_update_needed as false and missing details as null. Send the member to the billing portal to update their card:
return_url must be on an origin the hub allows, and must not contain a # fragment. While the hub’s billing is being migrated, this route returns 409 cutover_not_open (see below).

Errors to handle

409 cutover_not_open

A team’s billing can be in the middle of a migration. While it is, these actions are unavailable and return 409 with the error code cutover_not_open:
  • Starting or resuming checkout (POST /api/v1/hub/{hub_id}/checkout), including replaying a request that already completed under the same Idempotency-Key.
  • Opening the billing portal (GET /api/v1/hub/{hub_id}/me/billing-portal-link).
  • Cancelling a subscription, as an admin or as the member.
  • Refunding a payment.
  • A member deleting their account (POST /api/contact-auth/me/hubs/{hub_id}/delete-account) while they have a live subscription.
  • Deleting a product or a price, detaching a product from a hub, and adopting products from Stripe (POST /api/v1/teams/{team_id}/products/adopt-from-stripe).
Nothing was changed when you get this error, so there is nothing to undo. The action stays unavailable until the migration completes: show the member a “try again later” message and retry later. GET /api/v1/me/billing-portal-links leaves out the entry for a team in this state rather than failing. Creating and editing products, prices and coupons keeps working. The same applies to the MCP tools. start_test_checkout fails with a cutover_not_open tool error, and get_checkout_link reports a cutover_not_open blocker instead of a link.

503 checkout_deadline_exceeded

Starting or resuming checkout has a 120-second limit. If the limit passes before the payment provider is called, the request returns 503 with the error code checkout_deadline_exceeded and no checkout session is created. It is safe to retry. If you send the same Idempotency-Key again, checkout resumes the earlier attempt instead of starting a new one (see Idempotency). If the payment provider does not answer within 60 seconds, the request returns the usual 503 payment_provider_error, and you can retry the same way. The MCP tool start_test_checkout has the same limit: past it, the tool fails with a checkout_deadline_exceeded tool error (status 503), and you can retry it the same way.

Operational routes

Admins can inspect payments, subscriptions, orders, and webhook events. Use these for support tooling and reconciliation.