Release notes

Developer Changelog

Public, developer-facing contract changes for the Throttle API, embedded checkout, webhooks, and SDKs. Internal refactors and UI-only changes are excluded.

2026-09-14 — PayPal connector form explains its fields

  • Dashboard. Add connector → PayPal now says where each credential lives in the PayPal Developer Dashboard, which of the five fields are optional, and that BN Code and Merchant ID should be left blank for a merchant's own account. The form links to a new product doc, Connecting PayPal, with the full walkthrough. No API change.

2026-09-12 — Paid orders can no longer be set back to Draft or Pending

  • Dashboard. The Goods status picker on an order withholds Draft and Pending once a payment has been authorized or captured. Both statuses mean the order has not been paid, so on a paid order they recorded a false state while the capture stayed in place underneath. The order's current status is always still shown. No API change: POST /api/v1/orders/{id}/status still accepts any of the seven statuses.

2026-09-11 — Mint an update-card link from the API

  • API. POST /api/v1/payment-methods/client-token accepts an optional subscriptionId. When given, the token names that subscription, lives 14 days, and the response adds updateCardUrl, the same hosted page the renewal-failed email links to, so a merchant can send a buyer the link by any channel. A subscription that is not the customer's reads as 404. Without the field the route is unchanged.

2026-09-11 — Renewal-failed emails link to the hosted update-card page

  • Emails. customer.subscription_payment_failed (v3) now carries an Update your card button. The link is minted per send for that customer and subscription and is valid 14 days, long enough to outlast the retry ladder. The template gains links.updateCard and subscription.id; a custom override can reference both. On a deployment that cannot sign the link the button is hidden and the copy falls back to the previous wording.

2026-09-11 — Hosted "update your card" page

  • Hosted pages. https://checkout.usethrottle.dev/billing/{token} is a branded page where a buyer saves a new card for a subscription. The token is a pm_client_token minted for one customer and one subscription. The page shows the plan, the amount due, and the card on file; the buyer enters a card through the add-only embed, it becomes the default, and a past-due subscription is charged on the spot with the result shown in place ("Payment received", "Card saved, but the charge was declined", or "Card saved" when nothing is owed). Expired, forged, and someone-else's-subscription tokens all render the same expired state. The renewal-failed email links here next.

2026-09-11 — Buyer-scoped subscription routes for the update-card flow

  • API. GET /api/v1/me/subscriptions/{id} returns one of the authenticated customer's subscriptions with the default card that renews it, and POST /api/v1/me/subscriptions/{id}/retry-charge charges a past-due subscription immediately, answering charged. Both authenticate with X-Throttle-PM-Token, the same buyer credential as the /me/payment-methods wallet, which may now carry a subscriptionId claim. A subscription that is not the token's customer's reads as 404. First step of the hosted "update your card" link for past-due renewals; the hosted page and the email link follow.

2026-09-11 — Deposit terms on any quote revision

  • Dashboard. The working-revision editor on a quote now offers "Deposit + balance" beside Pay in full and Net-N, with the deposit type, amount, and balance Net days editable. Before, the split could only be chosen on the new-quote form, so a buyer-submitted request could never be quoted with a deposit, and an existing deposit split could not be adjusted or removed. Percent, fixed-amount, and Net-day inputs are validated in the form with the same rules the API enforces. No API change.

2026-09-11 — Overdue renewals stand out in the subscriptions list

  • Dashboard. An active or trialing subscription whose period ended on an earlier day and never renewed now reads "renewal overdue · N days ago" in red and its row is highlighted, the same treatment as a past-due row with failed attempts. Before, it read like any other row. The status and interval filters show "Past due" and "Monthly" rather than the raw values. No API change.

    2026-09-11 — Paying at the end of a trial unlocks Production

    • Fixed. POST /api/v1/billing/subscribe now advances the workspace's lifecycleStage to production along with marking the subscription active. It set the stage only on the application-billing mirror, while the dashboard's environment selector gates the Production environment on the workspace stage — so a workspace that subscribed after its trial ended stayed sandbox and Production remained disabled after a successful charge. Resume after an unpaid period already wrote the stage; the two paths now match.

    2026-09-11 — Ship part of an order from the dashboard

    • Dashboard. Create fulfillment now lists the order's lines with what is still outstanding on each and a quantity per line, defaulting to everything left. Picking less sends lineItemIds with quantities to POST /api/v1/orders/{id}/fulfillments, which the API has always accepted (and capped with 409 over_fulfillment); the dashboard used to send an empty list, so partially_fulfilled was unreachable from the UI. Each fulfillment card now says which lines it covers. No API change.

    2026-09-11 — A "not shipped yet" orders filter

    • New. GET /api/v1/orders?delivery=not_shipped (and the CSV export) returns orders in processing or partially_fulfilled with no shipment handed to a carrier and no return. The status scope is part of the predicate, so a fulfiller's queue never accumulates cancelled or closed orders. The dashboard's delivery filter gains "Not shipped yet"; previously the only way to build the morning pick list was to combine Processing and Paid by hand.

    2026-09-11 — Undo a scheduled cancel while past due

    • Fixed. POST /api/v1/subscriptions/{id}/resume now clears cancelAtPeriodEnd on a subscription in any non-cancelled status. It used to answer 400 invalid_subscription_state for a past_due or trialing subscription, even though /cancel with atPeriodEnd: true accepts those, so a scheduled cancellation there could not be undone. Status is unchanged by the undo; a past-due subscription stays past due. The dashboard's Cancel at period end now asks for confirmation.

      2026-09-11 — Recording delivery completes the fulfillment

      • Fixed. POST /api/v1/fulfillments/{id}/delivered now moves a pending or processing shipment fulfillment to completed before recording the arrival, emitting fulfillment.completed and then fulfillment.shipment.delivered, and rolling the order up to fulfilled. A fulfillment created with a tracking number used to sit at pending with a delivery date on it, and its order stayed processing. Already-completed fulfillments are unchanged.

        2026-09-11 — Signup charge counts as the first payment

        • Fixed. A subscription created active with a signup charge now carries lastPaymentAt from the moment it is created, alongside the paid period-1 invoice it has always recorded. It used to stay null until the first renewal, so a subscription whose first renewal failed reported no successful payment ever while showing a paid invoice. Trials and hybrid checkouts (signupCharged: false) are unchanged: no charge, no stamp. The renewal guard compares against currentPeriodEnd, so renewals are unaffected.

          2026-09-11 — Merchants can decline a quote request

          • New. PATCH /api/v1/quotes/{id} accepts status: 'declined' with an optional declinedReason. Legal from requested, under_review, revision_requested, and proposed; terminal apart from archive. The buyer receives customer.quote_declined with the reason, the timeline records a merchant decline, and quote.declined fires with declinedBy: 'merchant'. Before this the only exit for a request a merchant would not price was archiving it, which told the buyer nothing.
          • Dashboard. A Decline action with a reason field sits beside Archive on the quote page, and Issue to buyer is disabled until the working revision has a line (the API already refused with 409 empty_revision).

          2026-09-11 — Partner referrals report handoff progress

          • Pay for client is pre-handoff only. POST /api/v1/partner/trials/{referralId}/claim-billing answers 409 client_owns_workspace once the workspace has been handed off (invite outstanding or accepted). Handoff also restores a referral's commission eligibility if an earlier partner-pays period had marked it ineligible.
          • Undo. POST /api/v1/partner/trials/{referralId}/revoke-handoff cancels an outstanding client ownership invite and makes the acting partner member the owner again. Answers 409 handoff_not_revocable once the client has accepted, or when the workspace was never handed off. Requires a workspace member session (partner:write).
          • New field. Each item of GET /api/v1/partner/me/referrals carries handoffState: not_started (the partner still owns the client workspace), invited (the client owner has been invited and has not accepted), or claimed. The existing status field is conversion state and does not change at handoff. Items are now ordered newest first.

2026-09-11 — Billing writes are owner-only for dashboard sessions

  • Enforced. The permission catalog has always declared workspace:billing as owner-only, but the billing routes never checked it, so any workspace admin could add a card, subscribe, cancel, or resume. Dashboard (Clerk session) callers now get 403 permission_denied on POST /api/v1/billing/subscribe, /cancel, /resume, and on writes to /api/v1/billing/payment-methods unless they are the workspace owner. Reads are unchanged. API keys are gated by scope alone and are not affected.
  • New field. billedByWorkspaceId (nullable UUID) on each item of GET /api/v1/workspaces, on GET /api/v1/billing/state, and on workspace in GET /api/v1/billing/overview. Set when a partner has claimed billing for the workspace. The dashboard uses it to stop asking a partner-billed workspace's members for a payment method, and no longer shows a payment call to action to members who cannot act on it.

    2026-09-10 — Endpoints that stop verifying are reported in minutes, not days

    • Why. The 2026-09-08 outage below ran for hours with nobody told. The only automatic signal was auto-suspend after five dead-lettered deliveries, each the end of a 31-hour retry ladder, and the hourly failing-webhooks digest reached the merchant only, never the extension publisher who could fix it.
    • Unauthorized streak alert. Three consecutive 401/403 responses from an endpoint — workspace or extension, any attempt — send system.webhook_auth_failing by email and in-app to the application's admins and, for an extension endpoint, the publisher's admins. Once per streak. Retries continue unchanged.
    • Scheduled signed probe. The install-time extension.ping now also runs against every active extension endpoint two minutes after each deploy and every six hours. A non-2xx answer sends system.extension_webhook_probe_failed to the same recipients, at most once per endpoint per day. Details on the extension events page .

      2026-09-10 — Signing-secret rotation with a grace window

      • Dual-signed deliveries. After POST /api/v1/webhook-endpoints/:id/rotate-secret the old secret keeps verifying for graceSeconds (default 24h, max 7d): every delivery carries two digests, v1=<outgoing>,v1=<new>. Before today the old secret died the instant you clicked rotate. graceSeconds: 0 keeps that behaviour for a leaked secret. The response and the installation secret read report previousSecretExpiresAt.
      • Extensions can rotate their own secret. New POST /api/v1/installations/:id/rotate-webhook-secret, same identity gate as the secret read. When the merchant rotates instead, the endpoint receives a signed extension.webhook_secret_rotated (no secret inside) so it can fetch the new one before the deadline. Delivered to the installation's own endpoint like extension.uninstalled.
      • Verifiers. @usethrottle/webhook-types and @usethrottle/extension-bridge accept multiple v1 digests (patch releases); the documented Node verifier snippet does too. The extension starter's verifier already did. A verifier that keeps only the last digest is unaffected: the new secret is always last.

        2026-09-10 — An example delivery for every event

        • Docs. The event payload reference now shows, under each of the 93 events, the exact envelope a delivery carries — fixed ids and timestamps, real nesting, real units — instead of only the key list. Items 1 and 2 from the first third-party extension team.
        • API. Each entry on GET /api/v1/event-types gains example, the same envelope. It is built by the send-test fixture with a fixed clock, so it is byte-stable across requests and a build step can pin it.
        • Not JSON Schema. The example is illustrative, not a validator. Optional fields may be absent on a real delivery and Throttle adds fields without notice (see the envelope evolution policy below); a schema derived from the example must not be strict.

2026-09-10 — Envelope evolution policy, after a strict-schema outage

  • Incident. From 03:50 UTC on 2026-09-08 an extension built from the Throttle extension starter rejected every delivery with 401 WEBHOOK_VERIFICATION_FAILED. No secret rotated and the signed bytes were the sent bytes: the starter's envelope schema was .strict(), the new environmentKind field failed the parse after the HMAC had matched, and the handler reported the parse failure under the signature code. Fixed in starter PR #11 ; every extension built before it needs the same change and a redeploy.
  • Policy, now written down. Throttle adds envelope and data fields without bumping version. Consumers must ignore or strip unknown keys. version changes only when an existing field is removed, renamed, or changes type, and never without notice. Verify the signature and parse the envelope as separate steps with separate failure codes.
  • Docs. The extension events page now lists environmentKind in its envelope, which the 2026-09-09 entry below had omitted.

2026-09-09 — Envelope says which environment kind; replays keep their timestamp

  • New envelope field environmentKind. production or non_production, on every outbound webhook and extension delivery. If your destination is a live system, drop non_production deliveries in one check instead of maintaining a denylist of test email prefixes. Additive; field order in the envelope otherwise unchanged.
  • Replays preserve createdAt. A replayed delivery (workspace endpoints and extension installations) now carries the original event's createdAt, not the replay time. Same id, same payload, same timestamp — the send time is the t= in the signature header.
  • GET /api/v1/event-types states units. A top-level conventions object says money is integer minor units, timestamps ISO 8601 UTC, ids opaque.
  • Docs. Identity tokens: unknown claims must be ignored (the claim set is additive). Webhook-secret endpoint: the response body is the secret — keep it out of error objects. Install-time ping: it is signed; a first 401 is a sequencing issue your extension can heal by fetching the secret.

2026-09-09 — Extensions are told when an installation ends

  • New event extension.uninstalled. Uninstalling an extension — from the dashboard, the API, or a staff takedown — now sends one signed delivery to that installation's webhook URL, carrying installationId, extensionId, applicationId, uninstalledAt and reason. It needs no subscription, scope, or API key on the extension's side, is sent after the installation reads uninstalled, and is retried like any other delivery. Before this, nothing told an extension its installation was over, so provider credentials it held outlived the install. Workspace endpoints may also subscribe to it (extensions:read). See the extension events reference .

2026-09-09 — Script external domains: one wildcard label, and CSP blocks are reported

  • externalDomains accepts *.example.com. One leading wildcard label, for vendor SDKs that publish *.vendor.com as their CSP guidance instead of a fixed host list. It becomes https://*.example.com in the sandbox CSP — every subdomain, never the bare domain, never *. The same rule is applied at write time, in the CSP, inside the frame, in marketplace preflight and in review observation. Bare hostnames are unchanged.
  • A CSP refusal is now a blocked outcome. The sandbox document forwards the browser's own violation report, so a request to an undeclared host counts as blocked in script health, and the script.blocked event names it: detail: { reason: "csp", directive, blockedUri }. Previously the only trace was a console line in the buyer's browser. Capped at five per frame.

2026-09-08 — Fulfillment events carry the buyer; shipped fires on create

  • Every fulfillment.* event now carries data.customer. Same shape and | null semantics as order.*, attached at delivery from the fulfillment's order; the embedded order also gains customerId. Until now the tracking number lived only on fulfillment.shipment.shipped and the buyer only on order.*, so a shipping notification could not be built from a single event.
  • fulfillment.shipment.shipped fires when a shipment is created with tracking. POST /api/v1/orders/{orderId}/fulfillments accepts an optional shipment block (same fields as PATCH .../shipment); a trackingNumber there fires the event right after fulfillment.created. Previously only the PATCH path fired it, so a shipment created with tracking never "shipped" as far as webhooks knew. Fires once per shipment; later tracking edits do not re-fire it.

August 2026 — previously unannounced contract changes

Recorded late. Each of these changed an existing endpoint or event contract and shipped without a changelog entry.

  • 2026-09-06 — Three script.* events and import.completed. script.loaded, script.blocked (detail.reason names the cause, e.g. consent) and script.error under the new application_scripts:read scope, alongside the scripts feature below. import.completed fires when a bulk import commit finishes.
  • 2026-08-29 — subscription.invoice_refunded. Fires when a subscription invoice is refunded.
  • 2026-08-23/24 — Seven new order and payment events. order.comped, order.comp_reversed, payment.recorded (08-23); order.held, order.hold_released, payment.expired, payment.processing (08-24). Every entry carries its read scope in GET /api/v1/event-types; an extension must hold that scope before it can subscribe.
  • 2026-08-24 — fulfillment.shipment.delivered means arrival, and carries the shipment. It fires when a shipment is recorded as arrived via POST /api/v1/orders/{orderId}/fulfillments/{id}/delivered, no longer when the shipment fulfillment is marked completed. Completing is carrier handoff; listen for fulfillment.completed if that is what you want. The payload gained shipment (now a required key) so the arrival carries its tracking details.
  • 2026-08-22 — order.completed renamed to order.fulfilled. Stored webhook and extension subscriptions were rewritten in place to the new name. If your subscription list looks different from what you registered, this is why; deliveries of the old name before that date were legitimate. order.closed's trigger text changed with it ("a fulfilled order is closed"); its behaviour did not.
  • 2026-08-11 — Extensions can read their own webhook signing secret. GET /api/v1/installations/{id}/webhook-secret, authenticated with the extension's launch (identity) token. Reinstalling keeps the existing endpoint and secret, and increments installSequence on the installation. The response body is the secret: do not attach it to error objects or logs.
  • August 2026 — GET /api/v1/event-types gained trigger, dataShape and requiredDataKeys. Rendered from the same reference as the webhooks reference page, so the two cannot disagree.

2026-09-05 — Run your own scripts on hosted checkout

  • Merchants can add their own JavaScript to hosted checkout. Throttle runs it in an isolated, null-origin sandboxed iframe on /c and /s (non-embed mode only) — no DOM access, sandbox tier only. See Your Own Scripts.
  • New /api/v1/application-scripts endpoints. Create, list, update, and soft-delete scripts, plus an environment-wide kill switch and a last-24h load-outcome health endpoint. Managed from the dashboard's Settings → Scripts, or directly via the API.
  • New public delivery endpoint. GET /api/v1/storefront/script-assets/:id/:sha256 serves script source by content hash — immutable, unauthenticated, and the one point of truth every delivery path (the checkout-web /es/<id>/<sha256>.js proxy and the /sandbox document route) resolves through.

2026-09-07 — Extension listing images over the API

  • POST /api/v1/extensions/icon-upload and POST /api/v1/extensions/screenshot-upload accept secret API keys. Both previously answered 403 clerk_required to anything but a dashboard session, which made image upload the one step of a marketplace submission that could not be scripted. Any credential holding extensions:write can now upload; the field name, size limits, and response shape are unchanged. See Publishing to the Marketplace.

2026-08-23 — company on customers

  • Customers carry a first-class company. POST /api/v1/customers and PATCH /api/v1/customers/:id accept it, and every customer response returns it (null when unset). Send null or an empty string to clear it. Existing records were backfilled from metadata.companyName and the default address's company, so a value you already stored there is preserved.
  • Orders and subscriptions expose the buyer's company. The embedded customer object on order and subscription responses now includes company alongside the name and email.

2026-08-07 — Stripe Connect via OAuth

  • Connecting Stripe no longer requires installing a Stripe App. A merchant can authorize Throttle through a standard Connect OAuth flow instead, with no keys pasted anywhere. The existing install-link path still works.

2026-08-03 — Production API keys read sk_live_

  • Keys minted for a production environment now carry sk_live_ / pk_live_. They previously read sk_production_ / pk_production_. Keys minted before this date remain valid indefinitely — nothing in the auth path parses the environment segment, so both prefixes authenticate. Non-production environments continue to use their own slug, e.g. sk_test_ or sk_uat_.
  • live, live-*, and production-* are reserved environment slugs. Creating a custom environment with one of these names is rejected, so a sandbox environment can never mint a key that looks live.

2026-08-02 — Hosted MCP server and OAuth 2.1

  • Throttle is now an OAuth 2.1 authorization server. PKCE and dynamic client registration, advertised at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource, with /oauth/authorize and /oauth/token. Consent is per application.
  • A hosted remote MCP endpoint at /mcp speaking Streamable HTTP, so an MCP client can reach Throttle without running anything locally.
  • @usethrottle/mcp 0.2.0 adds quote and money write tools, each idempotent and gated behind --allow-writes / --allow-live-writes. See MCP server.
  • The authorization-code TTL was raised from 60 seconds to 10 minutes on 2026-08-09, since a human completing a consent screen routinely takes longer than a minute.

2026-07-28 — MCP server and /whoami

  • @usethrottle/mcp 0.1.0 — a read-only Model Context Protocol server over stdio, with tools scope-filtered against the grants on your API key.
  • New: GET /api/v1/whoami resolves the calling credential to its workspace, application, and environment.

2026-07-27 — Webhook payloads carry customer identity

  • Every subscription.* payload now includes a customer object. Payloads previously carried only customerId, a Throttle UUID that means nothing in your system, so identifying the buyer cost a GET /customers/{id} per event.
  • Flat payment.* payloads that carry an orderId gain customer, customerId, and subscriptionId.
  • The customer object carries email, firstName, lastName, phone, and both external identifiers — externalId and externalCustomerId — because they are not the same field. It is attached at delivery and is absent when the customer row cannot be resolved, so treat it as nullable.
  • Webhook coverage lastEmittedAt is now ISO 8601.

2026-07-19 — Money-correctness: returns, order edits, cancel, currency

Returns & exchanges

  • Return refunds now reflect what the buyer actually paid. The refund for a returned line is its subtotal plus tax, minus discounts (including a proportional share of order-level code discounts), prorated exactly across partial-quantity returns. Previously refunds used bare unitPrice × quantity, under-refunding tax and over-refunding discounted items.

Orders

  • PATCH /orders/:id/line-items now recalculates tax. When tax is configured for the application, edited orders re-quote tax for the resulting item set (added items no longer land with taxAmount: 0) and the captured delta charge/refund includes the tax movement.
  • POST /orders/:id/cancel now settles payments. Open authorizations are always voided; pass refundCapturedPayments: true to also refund captured money. The response reports a paymentActions array.

Carts

  • Cart currency is validated against the application. POST /carts rejects a currency that differs from the application's configured per-environment currency (422 currency_mismatch); omitting currency now inherits the application currency instead of silently defaulting to USD.
  • Legacy deprecated discount types fail validation. Surviving free_shipping / buy_x_get_y rows (retired types) now fail checkout validation loudly instead of applying with a silent $0 effect while consuming a usage slot.

2026-07-04 — Embedded checkout: webhooks, receipts, prefill & retry fixes

Webhooks

  • payment.captured and payment.vaulted now deliver for embedded checkout. These events (and all webhooks from proxy/embed sessions, including order.created) were silently dropped because the emit omitted application context. They now reach every subscribed endpoint with the correct signature.

Emails

  • customer.payment_receipt now sends for embedded checkout captures. The synchronous embed capture path previously bypassed the internal event bus, so no receipt email fired for card/one-time or subscription checkouts.

Checkout

  • Buyer prefill now reaches the checkout UI. Passing a customer on session create now pre-fills the buyer's name and address in the hosted/embedded form (the prefill was being stripped from the public session response).
  • Failed payments are retryable. If a capture fails, returning to the same checkout session now retries cleanly instead of erroring; a duplicate submit returns the existing order without double-charging.

2026-07-04 — Abandoned-cart recovery works end to end

Checkout

  • Recovery links can now complete a purchase. Creating a checkout session against an abandoned cart now reopens it (status → open), so a buyer who returns via a cart.abandoned recovery link can finish checkout on that same cart. Previously the cart was frozen and completion dead-ended with cart … is in 'abandoned' status.

Webhooks & email

  • Guest carts get recovery emails. The abandoned-cart sweep now sends the customer.cart_abandoned recovery email to carts that captured only a customerEmail (no full customer record), not just carts linked to a customer.
  • Configure the recovery link. The recovery URL in the cart.abandoned webhook payload and the recovery email is built from your per-app cartRecoveryUrlTemplate (must contain {cartId}) — set it in the dashboard under Abandoned carts, or via PUT /api/v1/embed-config. Without it, recovery is webhook-only with a null URL.

2026-07-04 — Subscription checkout, typed request bodies, cart lifecycle

API & SDK

  • Checkout session request bodies are now documented. POST /api/v1/checkout/sessions and POST /api/v1/checkout/sessions/{id}/complete now publish their requestBody in the OpenAPI spec. @usethrottle/api-client@2.6.1 regenerates postApiV1CheckoutSessions / postApiV1CheckoutSessionsComplete with a typed body parameter — no more raw-fetch workaround for creating or completing a session. Request/response validation is unchanged.

Checkout

  • Plan-based & free-trial subscription checkouts can use an empty cart. When a checkout session carries a recurring block and its cart has no line items, Throttle now synthesizes a single subscription line item from the plan at completion (amount due today for an immediate charge, or $0 for a free trial), so the order converts cleanly. Previously an empty cart failed with cart_empty (“Cart cannot be converted to an order without line items”). recurring.create: 'auto' governs subscription creation after payment; it does not add cart items itself.

Webhooks

  • Checkout-session expiry reaches the cart. A checkout session now stamps its expiry window onto the parent cart, and when a session expires the cart is released — its status moves to abandoned and cart.abandoned fires (with the standard recovery payload). Previously the cart stayed open with no event.

2026-07-04 — Actionable unavailableReason on payment methods

API

  • GET /api/v1/checkout-sessions/{id}/payment-methods unavailableReason. When methods is empty because a payment provider is connected but can’t currently render (for example, not yet configured for the checkout’s environment), the response now includes an optional unavailableReason: { code, message }. Surface message to the buyer instead of a blank “no payment methods” state. The field is additive and only present on the empty path. The list itself now also reflects exactly what the embed will render, so the “available” view and the embed no longer diverge.

2026-06-21 — Cart email capture + richer cart.abandoned payload

API

  • Cart customerEmail. POST /api/v1/carts and PATCH /api/v1/carts/{id} accept an optional customerEmail — lightweight email capture without a full customer record. The cart response now also returns customerEmail and the stored shippingAddress / billingAddress.

Webhooks

  • Enriched cart.abandoned. The payload now includes shippingAddress and billingAddress (as stored on the cart, or null), and customer now represents a guest captured via the cart’s customerEmail as { id: null, email, firstName: null } — so anonymous carts with a captured email are recoverable. All fields remain additive.

SDK

  • @usethrottle/cart: CreateCartInput / UpdateCartInput / Cart gain customerEmail. @usethrottle/webhook-types: CartAbandonedData gains the address fields and a nullable customer id.

2026-06-20 — useThrottleCheckout hook

SDK

  • @usethrottle/checkout-react. New useThrottleCheckout hook that orchestrates a storefront checkout over a cart session: totals and selectedMethod bound to the cart, one-call selectMethod, a status state machine, automatic stale-cart recovery (rebuild + retry on cart_not_open), and createSession returning the checkoutSessionId for <PaymentEmbed>. See Cart sessions.

2026-06-20 — allowedMethods on payment-only embeds

API

  • Fail-loud. POST /api/v1/checkout-sessions/embed-token (payment-only) now rejects allowedMethods with 400 allowed_methods_unsupported instead of accepting and silently ignoring it. A payment-only embed renders the methods configured on your payment connection; the embed token has no method-restriction field. allowedMethods continues to filter the full hosted checkout (the /payment-methods catalog + payment tiles) — that flow is unchanged.

SDK

  • @usethrottle/checkout-sdk. createEmbedToken no longer accepts allowedMethods (it never applied to the payment-only embed). createSession still accepts it for the full checkout.

Docs

  • Documented the precedence between session allowedMethods and payment connection configuration. See Embedded Checkout.

2026-06-20 — Cancel a checkout session

API

  • DELETE /api/v1/checkout/sessions/{id} now cancels an in-flight session (previously a no-op). It is idempotent, marks the session cancelled, and re-opens an associated cart still in checkout status (never a terminal converted cart). A completed session returns 422 already_completed; an unknown session returns 404.

SDK

  • @usethrottle/checkout-sdk. New checkout.cancelSession(sessionId) method.

Docs

  • Clarified the session→cart lifecycle: creating a session does not move the cart out of open; the cart only becomes converted when the order is created at session completion. See Embedded Checkout.

2026-06-20 — Canonical cart address + typed cart errors

API

  • Canonical cart address. PATCH /api/v1/carts/{id} now validates shippingAddress / billingAddress at write time against one canonical shape (CartAddress: required addressLine1, city, countryCode). Non-canonical keys (line1, state, country, zip) are now rejected with a validation_error naming the camelCase replacement, instead of being stored verbatim and failing later at checkout with address_required.

SDK

  • @usethrottle/cart. Exports the canonical CartAddress type (used by carts.update and the cart response) and two typed lifecycle errors — CartNotOpenError (409 cart_not_open) and CartNotFoundError (404). Both extend ThrottleApiError, so existing checks keep working. See Errors.

Docs

  • Clarified that selecting a shipping method is a single atomic call returning the full recomputed cart, and that the cart (not a client-side copy) is the source of truth for the selected method and totals. See Cart API.

2026-06-20 — Abandoned-carts read APIs

API

  • New endpoints. GET /api/v1/abandoned-carts (cursor-paginated; each row carries customer, total, itemCount, abandonedAt, and recoveryStatus) and GET /api/v1/abandoned-carts/summary (abandonedCount, abandonedValue, recoveryEmailsSent over a trailing window). Both require the carts:read scope. Available in @usethrottle/api-client. See API reference.

2026-06-20 — Richer cart.abandoned webhook payload

Webhooks

  • Enriched payload. The cart.abandoned outbound event now carries the full recovery context in data: customer ( id, email, firstName; or null for anonymous carts), lineItems, currency, totals ( subtotal, taxTotal, shippingTotal, discountTotal, total), itemCount, and a recoveryUrl. This lets ESP integrations (e.g. Klaviyo) drive a recovery flow from the single webhook with no follow-up API call. See Webhooks.
  • Backward compatible. The change is purely additive — only cartId and sequence are guaranteed, so existing consumers are unaffected. The envelope version stays "1".
  • Typed. @usethrottle/webhook-types now types the enriched CartAbandonedData (new fields are optional). recoveryUrl is populated from the app's cartRecoveryUrlTemplate when set, otherwise null.

Embed config

  • Per-app abandonment threshold. PUT /api/v1/embed-config now accepts cartAbandonmentThresholdMinutes (and GET returns it): the minutes of inactivity before an open/checkout cart is treated as abandoned by the sweep. Range 15129600 (90 days). Pass null to clear; when unset, the platform default of 1440 (24h) applies. The value is per application and per environment. See API reference.

2026-05-11 — Team management & per-app roles

Workspace invitations

  • Two-tier role model. Workspaces now carry three roles: owner, workspace_admin, and member. Members get explicit per-application roles from admin, developer, finance, or viewer. See Team management and Permissions.
  • New invitations + members endpoints. POST /api/v1/workspaces/:workspaceId/invites, .../invites/:id/resend, .../invites/:id/revoke, GET .../members, GET .../members/me, PATCH .../members/:memberId, DELETE .../members/:memberId, DELETE .../members/:memberId/applications/:applicationId.
  • Strict email match on accept. POST /api/v1/invites/accept now requires the caller's Clerk verified primary email to match the invite token's email claim. Mismatch returns 403 invite/email_mismatch.
  • Permission introspection. GET /api/v1/auth/permissions returns the caller's effective workspaceRole and appRole plus the full static catalog. Use it to drive UI gating.
  • Auth context fields. Server-side handlers now see auth.workspaceRole, auth.appRole, and auth.workspaceMemberId on Clerk-authenticated requests. Legacy auth.role field preserved.

Emails

  • Four new templates seeded by @platform/emails: system.team_invite_resent, system.team_invite_accepted, system.team_access_revoked, system.team_role_changed.

Legacy compatibility

  • POST /api/v1/merchants/me/invites and its /resend, /revoke siblings continue to work — they delegate to the new team-service. Legacy clients sending { email, role: 'admin' } still receive role: 'admin' in the response envelope.

2026-05-04 — Collect flags, billing, metadata propagation

Embedded checkout

  • Collect flags shipped. POST /api/v1/checkout/sessions accepts a new collect: { shippingAddress: boolean; billingAddress: boolean } object on the request body. Defaults match historical behaviour (shippingAddress: true, billingAddress: false). See Collection Flags .
  • Billing address is now first-class. POST /api/v1/checkout-sessions/:id/complete accepts new billingAddress (same shape as shippingAddress) and billingSameAsShipping: boolean (default false). Required when collect.billingAddress is true on the session.
  • step: 'billing' postMessage event. The unified /s flow now emits an additional throttle.step.changed event with step: 'billing' when the buyer reaches the billing form.
  • field extras on address_required 422 responses. Validation errors at /complete now include a field key (e.g. "billingAddress" or "shippingAddress") so the iframe and API integrators can route the error to the right form section.
  • Pay button auto-disables until collect-flagged fields are complete. Parent-set submitDisabled still composes additively — see Parent Controls .
  • ?mode=payment-only deprecated. Still respected client-side for one release. New integrators should set collect: { shippingAddress: false, billingAddress: false } instead.

Discounts

  • Session-create discountCode. POST /api/v1/checkout/sessions accepts a new discountCode: string field. Validated synchronously; invalid codes return 422 discount_invalid. See Discounts.

Metadata + webhooks

  • Session metadata propagates to orders. User-attached metadata on a checkout session is merged into the order at conversion with precedence cart < session < {customerEmail}.
  • Reserved keys are stripped server-side. recurring, customer_prefill, mode, amount, currency, and externalCartId are removed from session metadata before persistence; use top-level request fields instead.
  • Session metadata caps. 50 keys / 10KB serialized. Over-size payloads return 422 metadata_too_large.
  • Webhook payloads now carry user metadata. order.created, payment.captured, and subscription.created include the merged data.metadata bag. See Metadata.