RevKeenDocs

Cart Sessions

Stage a purchase with line items, add-ons, and a discount code before converting to a Checkout Session

Cart Sessions are the staging primitive that sits in front of a Checkout Session. A cart holds the line items, selected add-ons, and an optional discount code while the customer is still composing their purchase. When the customer is ready to pay, you convert the cart and RevKeen returns a hosted Checkout Session URL to redirect to.

A Cart Session is the only path that produces an Invoice, Subscription, or Order from cart-like state — no other API path takes a partial purchase and turns it into a billable artifact.

Best For

  • Multi-step purchase composition where line items, add-ons, and a discount code are decided across multiple requests.
  • Drawer-cart UIs on a merchant storefront that need a persistent server-side cart before checkout.
  • Flows that need server-side mutation events (add-on toggled, discount applied) for analytics or webhook consumers.

If you have everything needed for a purchase in one request, use a Checkout Session directly and skip the cart.

Lifecycle

        create / mutate
              v
            open  ── convert ──>  converted  ── (checkout session takes over)

              ├── inactivity > 60 min + has line items ──>  abandoned

              └── now > expiresAt ──>  expired
StatusMeaning
openMutable. Line items, add-ons, and discount code can change.
convertedA Checkout Session has been materialized from this cart. Cart is no longer mutable.
abandonedSweep transitioned the cart after > 60 min of inactivity with line items. Triggers commerce.cart.abandoned for downstream recovery.
expiredSweep transitioned the cart after it crossed expiresAt (default 24 hours). No event emitted.

Cart expiry, abandonment cutoff, and sweep cadence are platform defaults today. Per-merchant configurability ships in a follow-up.

Endpoints

MethodPathPurpose
POST/v2/cart-sessionsCreate an empty cart for a currency.
GET/v2/cart-sessions/:idFetch the current cart state.
PATCH/v2/cart-sessions/:idAdd, update, or remove line items.
POST/v2/cart-sessions/:id/add-onsToggle an add-on product on or off.
POST/v2/cart-sessions/:id/discount-codeSet or clear the discount code.
POST/v2/cart-sessions/:id/convertConvert the cart into a Checkout Session.

All write endpoints require the cart:write scope; reads require cart:read.

Add-Ons

Add-ons are pre-defined optional products attached to a cart's addOnsOffered. The customer toggles them on or off; toggling is idempotent (sending selected: true for an already-selected product is a no-op and emits no event).

await client.cartSessions.toggleAddOn(cart.id, {
  product_id: "prod_priority_support",
  selected: true,
});

Discount Codes

A cart can carry one discount code at a time. Today the code is stored only — pricing math is not applied yet (totals are unchanged). Convert blocks with CART_SESSION_DISCOUNT_PENDING if the cart has a discount code, so a cart with a discount can't accidentally convert before pricing lands.

await client.cartSessions.applyDiscountCode(cart.id, { code: "SAVE10" });

// Clear with code: null
await client.cartSessions.applyDiscountCode(cart.id, { code: null });

Until the discount-pricing slice ships, you must clear the discount code (code: null) before calling convert, or convert returns CART_SESSION_DISCOUNT_PENDING.

Convert

Convert is the atomic boundary from cart to checkout. It runs inside one transaction that:

  1. Locks the cart with a compare-and-swap (open -> converted) — concurrent callers serialize, the loser falls through to the idempotent re-read branch.
  2. Creates the checkout_session row carrying the cart snapshot (line items, add-ons, currency, totals).
  3. Sets cart.converted_to_checkout_session_id so the cart points at its successor.
  4. Emits commerce.cart.converted and commerce.checkout.started through the outbox.

Convert is idempotent. A second POST /:id/convert after success returns the existing Checkout Session and emits no further events.

Validation that runs inside the lock and rolls back the entire transaction on failure:

Error codeCause
CART_SESSION_EMPTYThe cart has no line items.
CART_SESSION_DISCOUNT_PENDINGThe cart has a discount_code set. Clear it first.
CART_SESSION_NOT_FOUNDNo cart for this id under the calling merchant.
CART_SESSION_CLOSEDThe cart is already abandoned or expired.

On rollback the cart status is restored to open — the customer can retry after fixing the cause.

TypeScript Example

import { Revkeen } from "@revkeen/sdk";

const client = new Revkeen({ apiKey: process.env.REVKEEN_API_KEY });

// 1. Create an empty cart
const cart = await client.cartSessions.create({
  currency: "GBP",
  mode: "payment",
});

// 2. Add a line item
await client.cartSessions.addLineItem(cart.data.id, {
  product_id: "prod_main",
  name: "Annual plan",
  quantity: 1,
  unit_price_minor: 9900,
  currency: "GBP",
});

// 3. Toggle an add-on
await client.cartSessions.toggleAddOn(cart.data.id, {
  product_id: "prod_priority_support",
  selected: true,
});

// 4. Apply a discount code (stored only today — pricing math follows)
//    Skip or clear before convert until the pricing slice ships.
await client.cartSessions.applyDiscountCode(cart.data.id, { code: null });

// 5. Convert into a hosted Checkout Session
const result = await client.cartSessions.convert(cart.data.id);

return redirect(result.data.checkout_session.url);

cURL Example

# Convert an open cart into a Checkout Session
curl -X POST "https://api.revkeen.com/v2/cart-sessions/cart_123/convert" \
  -H "x-api-key: $REVKEEN_API_KEY"
{
  "data": {
    "cart_session": {
      "id": "cart_123",
      "status": "converted",
      "converted_to_checkout_session_id": "cs_456"
    },
    "checkout_session": {
      "id": "cs_456",
      "url": "https://pay.revkeen.com/p/rvk_cs_456",
      "status": "pending",
      "currency": "GBP",
      "amount_cents": 9900
    }
  }
}

Idempotency and Retries

OperationIdempotent?Behaviour on retry
Toggle add-onYesSame desired state -> no-op, no event.
Apply discount codeYesSame code (including null) -> no-op, no event.
ConvertYesReturns the existing Checkout Session, emits no further events.
Add / update / remove line itemYes per cart versionA duplicate write to the same product on the same line is treated as an update.

All mutations also write to the domain-event outbox inside the same transaction as the state change, so emitted events always match committed cart state.

Abandonment and Recovery

A background sweep runs every 15 minutes (cart-abandonment-sweep on cron-worker). It scans for open carts that have been inactive longer than the cutoff (60 minutes by default) and transitions them:

  • Has line items -> abandoned, emits commerce.cart.abandoned v1.0.
  • No line items -> left as-is; the next sweep window catches it once it crosses expiresAt.
  • Past expiresAt (regardless of contents) -> expired, no event.

Subscribe to commerce.cart.abandoned via webhooks to trigger your own recovery flow (email, SMS, retargeting). commerce.cart.recovered for re-engaged carts ships in a follow-up.

Events

EventWhen it fires
commerce.cart.createdCart was created.
commerce.cart.updatedLine items added, updated, or removed.
commerce.cart.addon_toggledAdd-on selection changed.
commerce.cart.discount_appliedDiscount code was set or cleared.
commerce.cart.convertedCart converted into a pending Checkout Session.
commerce.cart.abandonedSweep transitioned a stuck open cart with line items past the inactivity cutoff.

See the event catalogue for the canonical list and envelope format.

  • Checkout Sessions -- The hosted payment surface that a converted cart redirects to.
  • Checkout Links -- Reusable URLs for static product sales without server-side cart composition.
  • Webhook events -- Subscribe to commerce.cart.* for analytics, recovery, and integration hand-offs.