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-08-23 — company on customers
-
Customers carry a first-class
company.POST /api/v1/customersandPATCH /api/v1/customers/:idaccept it, and every customer response returns it (nullwhen unset). Sendnullor an empty string to clear it. Existing records were backfilled frommetadata.companyNameand the default address's company, so a value you already stored there is preserved. - Orders and subscriptions expose the buyer's company.
The embedded
customerobject on order and subscription responses now includescompanyalongside 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 readsk_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_orsk_uat_. -
live,live-*, andproduction-*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-serverand/.well-known/oauth-protected-resource, with/oauth/authorizeand/oauth/token. Consent is per application. - A hosted remote MCP endpoint at
/mcpspeaking Streamable HTTP, so an MCP client can reach Throttle without running anything locally. -
@usethrottle/mcp0.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/mcp0.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/whoamiresolves the calling credential to its workspace, application, and environment.
2026-07-27 — Webhook payloads carry customer identity
-
Every
subscription.*payload now includes acustomerobject. Payloads previously carried onlycustomerId, a Throttle UUID that means nothing in your system, so identifying the buyer cost aGET /customers/{id}per event. -
Flat
payment.*payloads that carry anorderIdgaincustomer,customerId, andsubscriptionId. -
The
customerobject carriesemail,firstName,lastName,phone, and both external identifiers —externalIdandexternalCustomerId— 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
lastEmittedAtis 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-itemsnow recalculates tax. When tax is configured for the application, edited orders re-quote tax for the resulting item set (added items no longer land withtaxAmount: 0) and the captured delta charge/refund includes the tax movement. -
POST /orders/:id/cancelnow settles payments. Open authorizations are always voided; passrefundCapturedPayments: trueto also refund captured money. The response reports apaymentActionsarray.
Carts
- Cart currency is validated against the application.
POST /cartsrejects a currency that differs from the application's configured per-environment currency (422currency_mismatch); omittingcurrencynow inherits the application currency instead of silently defaulting to USD. - Legacy deprecated discount types fail validation.
Surviving
free_shipping/buy_x_get_yrows (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.capturedandpayment.vaultednow deliver for embedded checkout. These events (and all webhooks from proxy/embed sessions, includingorder.created) were silently dropped because the emit omitted application context. They now reach every subscribed endpoint with the correct signature.
Emails
-
customer.payment_receiptnow 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
customeron 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
abandonedcart now reopens it (status → open), so a buyer who returns via acart.abandonedrecovery link can finish checkout on that same cart. Previously the cart was frozen and completion dead-ended withcart … is in 'abandoned' status.
Webhooks & email
- Guest carts get recovery emails.
The abandoned-cart sweep now sends the
customer.cart_abandonedrecovery email to carts that captured only acustomerEmail(no full customer record), not just carts linked to a customer. - Configure the recovery link.
The recovery URL in the
cart.abandonedwebhook payload and the recovery email is built from your per-appcartRecoveryUrlTemplate(must contain{cartId}) — set it in the dashboard under Abandoned carts, or viaPUT /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/sessionsandPOST /api/v1/checkout/sessions/{id}/completenow publish theirrequestBodyin the OpenAPI spec.@usethrottle/[email protected]regeneratespostApiV1CheckoutSessions/postApiV1CheckoutSessionsCompletewith 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
recurringblock 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$0for a free trial), so the order converts cleanly. Previously an empty cart failed withcart_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
statusmoves toabandonedandcart.abandonedfires (with the standard recovery payload). Previously the cart stayedopenwith no event.
2026-07-04 — Actionable unavailableReason on payment methods
API
-
GET /api/v1/checkout-sessions/{id}/payment-methodsunavailableReason. Whenmethodsis 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 optionalunavailableReason: { code, message }. Surfacemessageto 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/cartsandPATCH /api/v1/carts/{id}accept an optionalcustomerEmail— lightweight email capture without a full customer record. The cart response now also returnscustomerEmailand the storedshippingAddress/billingAddress.
Webhooks
- Enriched
cart.abandoned. The payload now includesshippingAddressandbillingAddress(as stored on the cart, ornull), andcustomernow represents a guest captured via the cart’scustomerEmailas{ id: null, email, firstName: null }— so anonymous carts with a captured email are recoverable. All fields remain additive.
SDK
-
@usethrottle/cart:CreateCartInput/UpdateCartInput/CartgaincustomerEmail.@usethrottle/webhook-types:CartAbandonedDatagains the address fields and a nullable customer id.
2026-06-20 — useThrottleCheckout hook
SDK
-
@usethrottle/checkout-react. NewuseThrottleCheckouthook that orchestrates a storefront checkout over a cart session: totals andselectedMethodbound to the cart, one-callselectMethod, astatusstate machine, automatic stale-cart recovery (rebuild + retry oncart_not_open), andcreateSessionreturning thecheckoutSessionIdfor<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 rejectsallowedMethodswith400 allowed_methods_unsupportedinstead 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.allowedMethodscontinues to filter the full hosted checkout (the/payment-methodscatalog + payment tiles) — that flow is unchanged.
SDK
-
@usethrottle/checkout-sdk.createEmbedTokenno longer acceptsallowedMethods(it never applied to the payment-only embed).createSessionstill accepts it for the full checkout.
Docs
-
Documented the precedence between session
allowedMethodsand 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 sessioncancelled, and re-opens an associated cart still incheckoutstatus (never a terminalconvertedcart). A completed session returns422 already_completed; an unknown session returns404.
SDK
-
@usethrottle/checkout-sdk. Newcheckout.cancelSession(sessionId)method.
Docs
-
Clarified the session→cart lifecycle: creating a session does not move
the cart out of
open; the cart only becomesconvertedwhen 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 validatesshippingAddress/billingAddressat write time against one canonical shape (CartAddress: requiredaddressLine1,city,countryCode). Non-canonical keys (line1,state,country,zip) are now rejected with avalidation_errornaming the camelCase replacement, instead of being stored verbatim and failing later at checkout withaddress_required.
SDK
-
@usethrottle/cart. Exports the canonicalCartAddresstype (used bycarts.updateand the cart response) and two typed lifecycle errors —CartNotOpenError(409cart_not_open) andCartNotFoundError(404). Both extendThrottleApiError, 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 carriescustomer,total,itemCount,abandonedAt, andrecoveryStatus) andGET /api/v1/abandoned-carts/summary(abandonedCount,abandonedValue,recoveryEmailsSentover a trailing window). Both require thecarts:readscope. Available in@usethrottle/api-client. See API reference.
2026-06-20 — Richer cart.abandoned webhook payload
Webhooks
- Enriched payload. The
cart.abandonedoutbound event now carries the full recovery context indata:customer(id,email,firstName; ornullfor anonymous carts),lineItems,currency, totals (subtotal,taxTotal,shippingTotal,discountTotal,total),itemCount, and arecoveryUrl. 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
cartIdandsequenceare guaranteed, so existing consumers are unaffected. The envelopeversionstays"1". - Typed.
@usethrottle/webhook-typesnow types the enrichedCartAbandonedData(new fields are optional).recoveryUrlis populated from the app'scartRecoveryUrlTemplatewhen set, otherwisenull.
Embed config
- Per-app abandonment threshold.
PUT /api/v1/embed-confignow acceptscartAbandonmentThresholdMinutes(andGETreturns it): the minutes of inactivity before an open/checkout cart is treated as abandoned by the sweep. Range15–129600(90 days). Passnullto clear; when unset, the platform default of1440(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, andmember. Members get explicit per-application roles fromadmin,developer,finance, orviewer. 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/acceptnow requires the caller's Clerk verified primary email to match the invite token'semailclaim. Mismatch returns403 invite/email_mismatch. - Permission introspection.
GET /api/v1/auth/permissionsreturns the caller's effectiveworkspaceRoleandappRoleplus the full static catalog. Use it to drive UI gating. - Auth context fields. Server-side handlers now see
auth.workspaceRole,auth.appRole, andauth.workspaceMemberIdon Clerk-authenticated requests. Legacyauth.rolefield 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/invitesand its/resend,/revokesiblings continue to work — they delegate to the new team-service. Legacy clients sending{ email, role: 'admin' }still receiverole: 'admin'in the response envelope.
2026-05-04 — Collect flags, billing, metadata propagation
Embedded checkout
- Collect flags shipped.
POST /api/v1/checkout/sessionsaccepts a newcollect: { 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/completeaccepts newbillingAddress(same shape asshippingAddress) andbillingSameAsShipping: boolean(defaultfalse). Required whencollect.billingAddressis true on the session. -
step: 'billing'postMessage event. The unified/sflow now emits an additionalthrottle.step.changedevent withstep: 'billing'when the buyer reaches the billing form. -
fieldextras onaddress_required422 responses. Validation errors at/completenow include afieldkey (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
submitDisabledstill composes additively — see Parent Controls . -
?mode=payment-onlydeprecated. Still respected client-side for one release. New integrators should setcollect: { shippingAddress: false, billingAddress: false }instead.
Discounts
- Session-create discountCode.
POST /api/v1/checkout/sessionsaccepts a newdiscountCode: stringfield. Validated synchronously; invalid codes return422 discount_invalid. See Discounts.
Metadata + webhooks
- Session metadata propagates to orders.
User-attached
metadataon a checkout session is merged into the order at conversion with precedencecart < session < {customerEmail}. - Reserved keys are stripped server-side.
recurring,customer_prefill,mode,amount,currency, andexternalCartIdare 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, andsubscription.createdinclude the mergeddata.metadatabag. See Metadata.