Cart Checkout
Build a backend-owned dynamic cart with Checkout Sessions and hosted checkout.
Use Checkout Sessions when your backend owns a concrete cart or purchase attempt. The session may reference catalog Prices with priceId, or snapshot inline priceData without creating Product, Plan, or Price records.
Keep
x-api-keyon your backend. Never call/api/v1directly from browser code and never accept authoritative prices, recurrence, or account scope from the browser.
API conventions
Create separate API keys with the minimum required permissions. Read operations require READ; create, update, delete, expire, cancel, and refund operations require WRITE. A valid key without the operation permission returns 403; a missing, invalid, or expired key returns 401.
List Products, Payment Links, and Checkout Sessions with limit (20 by default, 100 maximum), startingAfter, or endingBefore. Use only one cursor and read the next cursor from meta.lastId or the previous cursor from meta.firstId. Every list returns { data, meta }.
Every error returns { error: { code, message, details? }, correlationId }. Log the correlation ID with your own request identifier. Retry only failures that are safe for the operation and reuse the same Idempotency-Key; retrieve the latest resource before retrying a version conflict. See the API Reference for each operation's statuses and schemas.
Resource IDs
Pharos IDs are opaque strings with a resource prefix, such as cs_…, price_…, cus_…, or pi_…. Store and send them unchanged; do not parse their suffix. During the compatibility window, API routes also accept a historical CUID returned before this format.
Runtime architecture
Buyer browser
→ your backend
→ Pharos /api/v1/checkout-sessions
→ Pharos-hosted /checkout/session/:id
→ shared quote, invoice, subscription, and payment pipeline
→ signed webhook to your backendYour backend owns cart construction and session mutations. Pharos owns the hosted buyer experience and the downstream commercial records. The browser only needs the returned checkout url.
Runtime flow
- Browser → your backend: The buyer chooses Checkout. Send only your own cart identity or authenticated cart context—not prices supplied by the browser.
- Your backend → Pharos API: Create a Checkout Session with
POST /api/v1/checkout-sessionsand a stableIdempotency-Keyfor that create operation. - Your backend stores the response: Associate the returned
id,version,ETag, andurlwith your cart. - Your backend optionally mutates the cart: Add, update, or remove items while the session is
openand unprepared. Every mutation uses a uniqueIdempotency-Keyand the latestETaginIf-Match. - Backend → browser: Return only the hosted
url; the browser redirects to/checkout/session/:id. - Buyer → Pharos Checkout: The buyer completes the same hosted checkout used by Payment Links. Browser-side quantity changes are constrained by the server-owned item snapshot.
- Checkout → shared commerce pipeline: Pharos prepares the final quote and creates the applicable customer, invoice, subscription, payment, and payment-attempt records.
- Pharos ↔ payment provider: The result may be immediate, redirect-based, actionable, or asynchronous.
- Pharos → buyer:
successUrlorcancelUrlcontrols navigation when applicable; neither URL proves payment success or expires the session. - Pharos → your webhook endpoint: Reconcile by
checkoutSession.idandclientReferenceId. Fulfill only after verifying and deduplicatinginvoice.paid.
Create a session on your backend
const pharosApi = 'https://api.example.com/api/v1'
type Cart = {
id: string
items: Array<{ priceId: string; quantity: number }>
}
export async function createCheckout(cart: Cart) {
const response = await fetch(`${pharosApi}/checkout-sessions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': process.env.PHAROS_API_KEY!,
'idempotency-key': `cart:${cart.id}:checkout-session`,
},
body: JSON.stringify({
mode: 'PAYMENT',
theme: 'light',
clientReferenceId: cart.id,
lineItems: cart.items.map((item) => ({
...item,
adjustableQuantity: {
enabled: true,
minimum: 1,
maximum: 10,
},
})),
successUrl: 'https://shop.example/success?session={CHECKOUT_SESSION_ID}',
cancelUrl: 'https://shop.example/cart',
}),
})
if (!response.ok) {
throw new Error(await response.text())
}
return {
session: await response.json(),
etag: response.headers.get('etag'),
}
}Use a stable create key derived from your cart or checkout attempt. Retrying the same request with the same key returns the same logical session; reusing that key with a different body causes an idempotency conflict.
Attach a known customer
Send customer with an existing Customer ID when your application already knows the buyer. Pharos validates that the customer belongs to the same account and environment, then prefills the hosted checkout with the saved profile. When customer is present, omit customerEmail; use customerEmail only for a guest whose profile should be resolved or created during checkout.
customerUpdate controls which saved fields the buyer may replace during checkout. Fields set to auto are written to the Customer automatically after the checkout completes successfully. Eligible cards are saved automatically after the buyer accepts the required checkout terms. savedPaymentMethodOptions.redisplay: "enabled" controls whether saved methods appear in later sessions; the legacy save option is accepted for compatibility but no longer changes checkout behavior.
Choose catalog or inline pricing
Use priceId when the offer already exists in the Pharos catalog. Use inline priceData when your backend needs to snapshot a contextual price without creating catalog records.
const customLineItem = {
priceData: {
currency: 'USD',
unitAmount: '25.00',
productData: {
name: 'Custom support package',
description: 'Configured for this order',
},
recurring: {
interval: 'MONTH',
intervalCount: 1,
trialDays: 7,
},
},
quantity: 1,
}Inline data becomes a durable session snapshot. The hosted browser must never submit or override unit amounts or recurrence.
Mutate an open cart safely
export async function addItem(sessionId: string, etag: string, priceId: string) {
const response = await fetch(`${pharosApi}/checkout-sessions/${sessionId}/line-items`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': process.env.PHAROS_API_KEY!,
'idempotency-key': crypto.randomUUID(),
'if-match': etag,
},
body: JSON.stringify({
lineItems: [{ priceId, quantity: 1 }],
}),
})
if (response.status === 409) {
throw new Error('Reload and reconcile the session before retrying')
}
if (!response.ok) {
throw new Error(await response.text())
}
return {
session: await response.json(),
etag: response.headers.get('etag'),
}
}After every successful mutation, replace your stored session, version, and ETag with the response. A 409 SESSION_VERSION_CONFLICT means another writer changed the session. Retrieve it, reconcile it with your cart, and retry as a new mutation. Do not blindly replay a stale body.
You may also:
PATCH /checkout-sessions/{id}to update non-monetary settings such as theme, redirects, email, reference, or metadata.PATCH /checkout-sessions/{id}/line-items/{itemId}to change quantity or adjustable bounds.DELETE /checkout-sessions/{id}/line-items/{itemId}to remove an allowed item.POST /checkout-sessions/{id}/expirewhen the cart should no longer be usable.
API-owned sessions can be mutated only while they are open and payment preparation has not locked the snapshot.
Redirect from the browser
const response = await fetch('/api/cart/checkout', { method: 'POST' })
if (!response.ok) throw new Error('Checkout could not be created')
const { url } = await response.json()
window.location.assign(url)Your /api/cart/checkout handler performs the authenticated Pharos request and returns the hosted URL. Do not expose the Pharos API key in this response or in client-side code.
Lifecycle and fulfillment
For direct server-side collection, create a PaymentIntent with POST /api/v1/payment_intents. It does not charge by default; set confirm: true or call /api/v1/payment_intents/{id}/confirm to create a Charge. Retrieve a Charge by ID and create a separate Refund with POST /api/v1/refunds.
Session status describes the buyer journey:
open: the session may be used; API mutations also require it to be unprepared.complete: the buyer flow reached a completed state.expired: the session is no longer usable.
paymentStatus independently describes money movement:
unpaid: no successful payment result exists yet.processing: an asynchronous or provider-side result is pending.paid: payment succeeded.failed: the current attempt failed and may allow a retry.no_payment_required: a free or trial flow completed without a charge.
The success redirect and checkout_session.completed are not fulfillment signals. Retrieve the session to reconcile clientReferenceId, customer, invoice, payment, subscription, and original cart, but fulfill only on the signed invoice.paid webhook.
Failure handling
- Retry network failures with the same idempotency key only when retrying the exact same operation.
- On
400, fix the request or cart composition instead of retrying unchanged. - On
401or403, check the API key, environment, and required permission. - On
404, confirm that the API key and session belong to the same account. - On
409, distinguish an idempotency conflict from a version conflict and reconcile before retrying. - If the session is locked, create a new cart attempt rather than mutating a prepared or completed snapshot.
- If payment is processing, wait for the signed webhook instead of polling the success page for fulfillment.
Commercial configuration
Use Products and prices for catalog carts. Review Coupons, Promotions and Combine discounts before enabling benefits through the settings supported by the API reference. Product-based conditions need matching catalog identities; do not assume inline items qualify simply because their names match.
Use Subscriptions and Invoices to interpret the records created by checkout. Do not submit unsupported commercial settings or assume every dashboard setting has an external API equivalent.