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-10-07 — Native tax correctness
- A code's scope is recorded on the order (bug fix). Orders with a
discount code now carry
metadata.discountScope: the code, its type and value, its product scope and the order line ids it reduced. Line-item edits and filed Avalara documents net the code from those lines, so changing or deleting a code after an order was placed no longer changes that order's tax, the money an edit charges or refunds, or what is filed. Orders placed earlier read the live discount, as before. The key is managed by Throttle; a value you send is ignored. See Discounts. - The embed-token mint recalculates stale native tax (bug fix). A
payment-only embed used to charge the tax from the last calculation even when the
cart's address, lines, shipping or customer changed afterwards. With native tax,
GET /api/v1/checkout-sessions/:id/embed-tokennow recalculates the tax when any of them changed and mints the new total (nothing is recalculated when nothing changed); a cart with no address takes the session's. A failed recalculation answers409 tax_calculation_failedwith no token. See Embedded checkout.
2026-10-07 — Lost chargebacks have their own state
- A lost card chargeback is no longer an open dispute (behavior change).
It used to leave
disputed: trueforever, indistinguishable from a case still in progress. Now a loss setsdisputed: falseand records the result on two new payment fields,disputeOutcome("won"|"lost"|null) anddisputeClosedAt. A won or reversed chargeback records"won"; a new dispute resets both tonull.disputednow means exactly "open right now". - New order payment status
charged_back. An order with a lost chargeback rolls up to it (display label "Chargeback lost") instead of reading as paid. An open dispute on another payment still outranks it. Treat unknownpaymentStatusvalues as non-paid if you switch on the enum. - New webhook event
payment.dispute_lostwith{ paymentId, reason, amount, currency, closedAt }, fired once per loss. Requirespayment_disputes:read. See Disputes and chargebacks.
2026-10-07 — Change one subscription's card
- New route:
PATCH /api/v1/subscriptions/:id/payment-methodwith{ paymentMethodId }(scopesubscriptions:write) sets the saved card one subscription renews on. Only that subscription moves: setting the customer's default card still moves every subscription that was on the old default. Errors:409 payment_method_unusable(not this customer's active, in-date card vaulted for renewals, or from another application or environment),409 invalid_state(cancelled or completed),404 not_found. Firessubscription.updated; the same card again is a no-op with no event. See Managing Subscriptions . - Storefront plane:
PATCH /v1/storefront/me/subscriptions/:id/payment-method(buyer session, verified email) does the same for the signed-in buyer's own subscriptions. -
@usethrottle/subscriptions:setPaymentMethod(subscriptionId, paymentMethodId); the server proxy forwards the route for the buyer's own subscriptions. Subscription objects now keeppaymentMethodId, which the client used to drop. - MCP: new write tool
set_subscription_payment_method.
2026-10-05 — Payment correctness
- Renewals charge the subscription's own card (behavior change). The
old rule, "renewals always charge the default card", is reversed. Each
subscription now stores the saved card it renews on as
paymentMethodId: the card the buyer paid with at checkout (every subscription from one checkout stores the same card), ornullto renew on the customer's default card. Renewals, trial conversion, dunning retries, both retry-charge routes and immediate plan changes try it first, then the default card, then (only when there is no distinct default) the newest other card. A fallback card is charged only after a definitive decline: a charge with no final answer (in flight, a transport error, an unknown status) is never followed by a second charge, with or without a backup card. The renewal records asystem_errorwith the new reasoncharge_unverifiedand enters no dunning; later runs re-read that charge's transaction instead of charging again, and one still unresolved after about 12 hours moves the subscription to a blockingpayment_reviewstate (no further charges, merchant alerted). An immediate plan change answers409 payment_unverified; retrying the same change reconciles its original charge. A captured but unapplied change waits for that retry and moves topayment_review/charge_unresolvedat 12 hours. Review blocks charges with409 renewal_needs_attention. Immediate cancellation of a pending charge also creates review and alerts the merchant. Rejected renewal requests recordconfig_error, alert the merchant to check connector credentials/configuration, and retry hourly without dunning. Immediate changes report this as402 payment_failedwith messageprocessor_config_error. After a definitive decline, the same change on the same card within 24 hours replays the decline; a different card or a new change gets a new key. After a hard decline (expired, an invalid card number, lost or stolen) and a successful fallback, the subscription moves to the card that paid andsubscription.backup_pm_usedcarriesrepinned: true. Invoices print the card actually charged. Existing subscriptions were backfilled from their checkout card; where that card is no longer the customer's default, they were set to the current default instead, so no subscription's renewal card changed, and those whose checkout card could not be traced keepnull. The backfill emitted no events and did not changeupdatedAt.paymentMethodIdis on the subscription API, the storefront subscription views and everysubscription.*webhook row (@usethrottle/[email protected],@usethrottle/[email protected]). See Which card a renewal charges. - Changing a subscription's card. New buyer route
PATCH /api/v1/me/subscriptions/:id/payment-method{ paymentMethodId }(X-Throttle-PM-Token) moves one subscription to one of the buyer's cards and emitssubscription.updated; it answers409 payment_method_unusablefor a card that is not the buyer's or cannot be charged and409 invalid_statefor a cancelled or completed subscription. The hosted billing link uses it, so updating a card there moves only that subscription and no longer changes the default. Setting a new default card (buyer wallet, storefront or merchant) moves the subscriptions that were on the previous default and leaves subscriptions on another card where they are. Deleting a card a subscription uses is allowed: that subscription falls back to the default card, with nosubscription.updated. See Payment methods. - Merchant-initiated renewals. A renewal, trial conversion, dunning retry or
retry-charge on the subscription's own checkout card is sent to the payment provider
as a merchant-initiated recurring charge (
merchantInitiated,paymentSource: recurring,isSubsequentPayment). Charges on any other card, and plan change charges, keep the flags they had. - Payments and saved cards in webhooks use a public shape (behavior change,
@usethrottle/[email protected]). Payment and card objects in deliveries no longer carry the provider's raw transaction (processorResponse), the vault token (gr4vyPaymentMethodId,processorToken), the provider's buyer id (gr4vyBuyerId), the card fingerprint (cardFingerprint), internal fields (usdAmountMinor,notes,disputeReason) or a card's rawmetadata/processor. The documented top-level scalars are unchanged:payment.vaultedstill sendspaymentMethodIdandgr4vyBuyerId, andsubscription.create_failedandsubscription.trial_blockedkeep theirs. New exportsPublicPayment(26 fields) andPublicPaymentMethod(14 fields) keep an index signature, so existing code still compiles. The events API, delivery reads, extension deliveries, both replay routes and the email log'svariablesPayloadreturn the same shape, and stored history was cleaned: a stored delivery payload that was projected or cleaned no longer verifies against its originalX-Throttle-Signature(replays are signed again).subscription.completedsend-test payloads now includepaymentandorder, as real deliveries do. Platform-billingworkspace.payment_method.*summaries are unchanged. See Payments and saved cards in payloads. -
processorResponseis deprecated on REST and gone from MCP (@usethrottle/[email protected]). The payments API (/payments,/orders/:id/payments,/payments/:id/transactions) still returns it, but it may be removed in a future API version; read the payment's own fields. MCP tool results ( get_payment, search_payments, get_order, api_get) no longer include it or a card'smetadata.gr4vy_payment_method. The merchant card routes are unchanged. - Checkout never captures an amount the buyer did not authorise (behavior
change). Recurring payment-only embeds now authorise instead of capturing, so
every card checkout is checked before capture. When the cart total changed after the card
was authorised,
/completevoids the authorisation (or refunds an amount the embed already captured) and answers409 checkout_total_changedwithauthorizedAmount,currentAmount,currency,cartId,voidedand, after a capture,capturedAmount/refunded; the checkout page re-mints the card form so the buyer confirms the new total. WithorderTotal, an order from an earlier attempt already has a payment at another total; an earlier order with no money on it is now re-totalled to the current cart before the charge (no cancel email, no second discount redemption). New codes:409 payment_unverified(retryable: the provider has not confirmed the transaction; retry with the same transaction),409 transaction_not_for_session(not retryable: the transaction, or the Secure Fields card session, was not created for this checkout) and409 transaction_already_used(it paid another order, or was refunded or voided).409 payment_processingnow means only that a pending order and a processing payment exist. Server-authorize (Secure Fields) completes only against a card session minted for that checkout session. Proxy completions bind the transaction to the session's minted Gr4vy checkout session.capturedAmountis the amount the provider reports, and a deposit's capture is recorded as the deposit. See Completion errors. - $0 checkouts complete without a charge (bug fix). A one-off checkout whose
total is $0 (for example a 100% code) used to fail with a 502 and leave a pending order. It
now completes paid at $0 with a
captured$0 payment; on the Secure Fields and saved-card rails no processor call is made. A $0 recurring proxy session (a trial) completes at $0, and a $0 Net-N invoice is never sent to dunning. - Add-card sessions never capture (bug fix). The add-only card embed's
$1 verification is voided after the card is saved, and no order or payment is created.
/completeanswersstatus: "card_saved"withpaymentMethodId, or422 card_not_vaultedwhen the card could not be saved.throttle.completedprotocol v1.6 carriespaymentMethodId. - Calculated tax is charged after cart-level codes (bug fix, behavior
change). Native tax ignored a discount code applied to the whole cart and taxed
the full price. A cart-wide code now lowers every line's taxable amount in proportion,
and a product-scoped code only the lines it applies to; native tax, the Avalara quote and
the filed Avalara document agree, and the stateless quote nets its
discountsTotal. Rules that tax before discounts still tax the full price. Throttle now recalculates native tax at completion on the server-authorize, saved-card and Net-N rails (409 tax_calculation_failedwhen that fails). Forward only: orders placed before this change keep their tax basis when edited, and new orders with a discount carrymetadata.taxDiscountBasis: "net_of_cart_code". See Discounts. - A refused trial is refused at checkout (bug fix, behavior change). When a
card already had a trial on this merchant, the subscription used to be created
activewith a free first period. Checkout now answers409 trial_not_availablewith no order or subscription, andsubscription.trial_blockedstill fires, withsubscriptionId: nulland optionalcartId,checkoutSessionIdandplanReference(subscriptionIdandpaymentMethodIdare nullable in webhook-types 6.3.0). If two checkouts race with the same card, the subscription is createdtrialingwith its trial ending at once and is charged on the next renewal run. See Trial Fraud Protection. - Renewal emails state the amount including tax (bug fix). The renewal
receipt states the amount actually charged and the payment-failed email the amount
attempted, tax included, on every plan (an equal-payment plan said $20.00 when $21.65 was
charged). Merchant template overrides that read
chargedScheduledas "this is an installment plan" now seetrueon every renewal with a payment. - Times on hour-gap payments (
@usethrottle/[email protected]). InSubscriptionsPanela scheduled payment that follows a gap of hours shows its time (UTC, labelled) next to its date, and the order-confirmation, welcome and renewal-reminder emails show it the same way. Payments after a gap of days, weeks or months are unchanged.
2026-10-04 — Invoice and order follow-ups
- A Net-N invoice has one number (behavior change). The Net-N payment's
metadata.invoiceNumber(shown in invoice emails, the dashboard, receivables andinvoice.past_due) and the invoice'snumber(printed on the PDF) were minted separately and differed. New Net-N invoices carry the invoice'snumbereverywhere, built with your Net-N invoice prefix. Invoices already issued keep their earlier number asmetadata.legacyInvoiceNumberonce migrated;invoice.past_dueaddslegacyInvoiceNumber(null otherwise;@usethrottle/[email protected]). See Invoice numbers. - Recording a payment on a Net-N order pays the invoice (bug fix, behavior
change).
POST /api/v1/orders/:id/payments/recordcreated a separate payment and left the order's unpaid Net-N invoice open, so the money was counted twice once the invoice was later marked paid, and the dunning sweep kept sending overdue reminders andinvoice.past_duefor a paid order. Now an amount equal to the invoice settles it (the invoice payment is captured and returned, withmetadata.recordedPayment;payment.capturedfires instead ofpayment.recorded); a smaller amount is recorded as its own payment and the invoice bills the rest; a larger amount answers422 amount_exceeds_invoice, another currency422 currency_mismatch, and an invoice that changed mid-request (a concurrent payment or edit, or a double submit)409 invoice_changed, with nothing recorded. Orders without an unpaid Net-N invoice are unchanged. - A payment cannot be captured twice (bug fix).
POST /api/v1/payments/:id/capturechecked the status it had read, so two concurrent captures of the same payment (Mark paid twice) could both succeed and emitpayment.capturedtwice. The status write is now guarded; the second answers the existing409 payment_already_captured. - The buyer's invoice shows what it deducts (storefront,
@usethrottle/[email protected]).GET /v1/storefront/me/invoicesand/me/invoices/:idaddorderTotal,amountPaid,compWaivedandbalanceDue(null when the invoice bills the whole order), the same numbers the PDF prints, and now declare their response in the OpenAPI spec.<InvoiceDetail>shows Order total, Amount paid, Waived and Amount due for a Net-N balance invoice, and Amount paid with the Balance due on a deposit receipt. - Order reads declare the buyer data they return (documentation). Every order
read already returned
ipAddress,userAgent(the buyer's IP address and user agent recorded at checkout),clientContext(the checkout's attribution snapshot),applicationId,environmentId,cartId,externalId,externalCustomerIdandimportId, and line items returned their row columns, but the OpenAPI order schemas did not declare them. They are now declared on every order operation (and typed in@usethrottle/api-client). Nothing new is sent. See Buyer and source fields. - Cancelling an order voids a
pendingNet-N invoice (bug fix).POST /api/v1/orders/:id/cancel(and moving an order tocancelledby hand) voided onlyauthorizedpayments, so a manually raised Net-N invoice still atpendingstayed open on a cancelled order. It is now voided too and appears inpaymentActions; its invoice becomesvoid. A pending card payment is still left to the processor. - A deposit's card receipt bills the deposit (bug fix). On a deposit +
Net-N balance order the deposit's receipt (
sourceType: order) carried the whole order total and its PDF read TOTAL … PAID, as if the balance had been paid too. Itstotalis now the deposit;billingSnapshot.totalsaddsorderTotalandbalanceDue, and the PDF prints the balance under the order total with an Amount paid band. Receipts already issued keep their stored totals.
2026-10-04 — Net-N invoices follow their payment
- A Net-N invoice moves to
paidorvoid(bug fix). The customer invoice behind a Net-N payment stayedopenafter the payment was captured (marked paid) or voided (cancelled, comped, or edited to zero), and its PDF kept saying Due. It now becomespaidwhen the payment is captured andvoidwhen it is voided, and the next PDF download says Paid or Void. Both values were already part of the invoicestatusenum; no response shape changes. -
POST /api/v1/payments/:id/voidcancels apendingNet-N invoice (bug fix). It accepted onlyauthorizedpayments, so an unpaid Net-N invoice still atpendinganswered409 invalid_payment_state— and comping the order or editing it to zero reported a void failure and left the invoice open. Net-N holds nothing at a processor, so a pending invoice now voids like an issued one (payment.voided, invoicevoid). A pending card payment and a captured payment are still refused. - A Net-N invoice's
totalis the amount due (bug fix). On a deposit + Net-N balance order the balance invoice was raised for the whole order, so itstotal, its PDF and the buyer's account asked for the deposit again.totalis now the Net-N payment's amount; the lines and the order's subtotal/tax/shipping/discount stay, andbillingSnapshot.totalsaddsorderTotal,amountPaidandcompWaivedwhen the invoice bills less than the order. The PDF shows the deposit as paid and an Amount due. Invoices already issued keep their stored totals.
2026-10-04 — Editing an order bills its outstanding Net-N invoice
-
PATCH /api/v1/orders/:id/line-itemsmoves the outstanding Net-N invoice (bug fix). Editing an order whose balance is on an unpaid Net-N invoice used to change the order total and leave the invoice billing the old amount, so receivables, reminders and the invoice PDF all asked the buyer for the wrong sum. The invoice payment'samountnow follows what the buyer owes: an increase is added to it (no card is charged), a decrease taken off it, and only a decrease larger than the invoice carries is refunded from money already collected. An invoice left owing nothing is voided. The invoice keeps its number and due date; its PDF re-renders with the new amount on the next download. The buyer is not emailed automatically — resend the invoice to send them the new amount. - New response field:
adjustment.invoice.{ paymentId, invoiceNumber, previousAmount, newAmount, voided, voidError? }on the edit's response and on theorder.updatedwebhook it sends;nullwhen the order has no outstanding invoice.
2026-10-03 — Custom installment schedules
- Payments that differ. An installment plan can now charge different amounts
on different gaps — "$40 today, $15 36 hours later, $35 a month after". Send
recurring.scheduleon a cart or quote line, orscheduleonPOST /subscriptions. See Installment plans. - Behaviour change: discount codes on installment plans. A code applied to a cart with an installment line now applies to the whole plan and is spread across its payments. Before, it reduced only the first payment. Existing subscriptions are unchanged.
- New fields. Every subscription returns
paymentSchedule,scheduleTotalandnextPaymentAmount, in API responses, webhook payloads and events stored from this release on (GET /api/v1/events);subscription.renewedandsubscription.completedcarrypaymentIndex. New errors:invalid_scheduleandschedule_first_payment_mismatch. - Behaviour change: embedded completion re-checks subscription lines.
POST /api/v1/checkout-sessions/:id/completewith aprocessorTransactionIdthe embed only authorised now answers409 subscription_lines_changedand voids the authorisation when the cart's subscription lines changed after the session was created, for every subscription checkout, not only schedules. If the transaction's status cannot be confirmed yet it answers the retryable409 payment_processing. A transaction the embed already captured completes as before. See Embedded Checkout. - Behaviour change: the subscription invoice list records $0 periods.
GET /api/v1/subscriptions/:id/invoicesrows gainwaived. A waived period, or a renewal that cost nothing, now writes a $0paidrow withwaived: true, on every plan, equal plans and ongoing subscriptions included. Rows written before this release are unchanged. - The public quote link returns the schedule.
GET /api/v1/quote-links/:tokennow returns a schedule line'srecurring.schedule, and the hosted quote page lists every payment with its estimated date. - Security fix.
@usethrottle/subscriptions' proxy no longer lets a buyer pause, resume or cancel an installment plan. If you pass your ownauthorizeMutation, it must enforce this itself. - Packages. Minor releases:
api-client5.6.0,subscriptions3.3.0,cart3.12.0,quotes0.6.0,webhook-types6.1.0,auth0.8.0 andmcp0.4.0.
2026-10-03 — Editing a provider-taxed order re-quotes its tax
-
PATCH /api/v1/orders/:id/line-itemsre-quotes tax with the tax provider (bug fix). When the application calculates tax with a provider (taxMode: "app_based", e.g. Avalara), an edit used to keep the tax the order was created with, so a doubled quantity charged the goods but not their tax. The edit now asks the provider for the edited lines, at the order's own date, customer, exemption and per-order overrides, and the filed tax document is adjusted to match. - New error:
503 tax_provider_unavailable. With the strict tax fallback policy (the default), an edit the provider cannot answer is refused and the order is left unchanged: nothing is charged or refunded. Retry once the provider is reachable. An edit that cannot be taxed for a configuration reason answers422with the reason as its code (tax_origin_missing,tax_address_missing). - New response field:
warnings. With the lenient policy, an edit the provider cannot answer is taxed by your native tax rules, as at checkout, and the response carrieswarnings: [{ code: "provider_fallback", message }]. The field is absent when there is nothing to report. - The edit's response and errors are in the OpenAPI spec and
@usethrottle/api-client5.5.0. The operation used to document only a bare200, so the generatedpatchApiV1OrdersLineItemsreturnedany. It now declares the edited order, itsitems, the settledadjustment, the optionalwarnings, and every error status the edit can answer. See Error codes: order line-item edits . - The filed Avalara sale is adjusted in place. An edit to an order whose sale
is already filed re-files that sale with AvaTax's AdjustTransaction: same document
code and date, so it stays in its original period. The refund an edit issues no longer files
a separate return (it used to, measured against the edited order). Each adjustment appears
in
GET /api/v1/shipping-tax/documentsas aSalesInvoiceAdjustmentrow coded<sale code>-ADJ-<hash>; a failed one can be retried withPOST /api/v1/shipping-tax/documents/:id/retry. A sale AvaTax has locked (reported on a filed return) cannot be adjusted: the row fails with the reason and the filing alert fires.
2026-09-30 — Declined embed cards answer 402, checkout-react under vite dev
- A declined card at
/completeanswers402 payment_declined(bug fix). When the processor declined the payment embed's authorisation,POST /api/v1/checkout-sessions/:id/completeused to answer502 payment_capture_failedwith "Internal server error" and leave apendingpayment on the order after a successful retry. It now answers402 payment_declinedwith the processor's reason, like the server-authorize and saved-card rails, leaves no pending payment, and reopens the session for a retry. Nothing is charged either way. See Embedded Checkout. -
@usethrottle/checkout-react2.1.1 loads undervite dev(bug fix). Importing the package in the Vite dev server threw "Cannot read properties of undefined (reading 'SecureFields')" and left a blank page. The package now bundles the payment provider's Secure Fields SDK, so it works in Vite dev and build, Next.js and webpack with no configuration. -
quoteTokenis aqt_storefront quote token (docs).useCartSessionandCartSessionClientsaid they took apk_…token. They take a storefront quote token (qt_…, or a legacypk_…quote token). Apk_publishable API key is refused with401 invalid_quote_token, as it always was. See Cart Sessions. Patch releases:checkout-react2.1.1,cart3.11.1 andcheckout-sdk2.1.1.
2026-09-30 — Switched-off email templates stay off
- An
enabled: falsetemplate override now mutes every send (bug fix). Settingenabled: falsewithPUT /api/v1/applications/:applicationId/email/templates/:key, or switching the template off in the dashboard, used to mute only emails triggered by events. API-key notices, quote emails, renewal reminders, abandoned-cart recovery and webhook endpoint alerts still went out. They are now recorded in the email log assuppressed_optout(workspace_template) and not sent, including emails already queued when the template was switched off. The override applies only to its own application and environment. Test sends, buyer email-verification, password-reset and account-setup emails, and invoice resends you trigger yourself still send. Emails that belong to the workspace rather than an application (team and invitation emails, Throttle's billing emails to you, the daily ops summary and the webhooks failing digest) cannot be muted per application.
2026-09-29 — Several subscriptions in one checkout
- Subscription lines on carts and quotes (additive). A cart line or quote line
can carry
recurring; each becomes its own subscription, up to ten per cart, paid in one capture with one shipping fee. The same plan on two lines makes two subscriptions. See Mixed Carts. -
subscriptionIdseverywhere (additive). Orders, the checkout complete response,throttle.completed/onSucceeded(postMessage protocol v1.5), order and payment webhooks,quote.convertedand the storefront order reads list every linked subscription; each order line carriessubscriptionId.subscriptionIdon the order is now set only when there is exactly one, so it isnullon an order that created several.GET /api/v1/orders?subscriptionId=also matches the line link. The orders CSV export gains asubscription_idscolumn, and abandoned carts gainsubscriptionLineCount. - Subscriptions keep their line's tax category (additive). A subscription
gains a read-only
taxCategory, frozen from themetadata.taxCategoryof the line that created it, and every renewal is quoted and filed with it. Order lines now declare theirrecurringterms in the OpenAPI spec. - Existing mixed checkouts get one invoice and one email (behavior change). A
session
recurringintent on a cart with goods now produces one order invoice listing every line, and one combined order confirmation, sent after payment, instead of a confirmation plus a welcome email. Single-plan checkouts are unchanged. - Subscription lines never ship (behavior change). This includes the plan line
a session
recurringintent adds. Subscription lines no longer count toward shipping weight, item count, subtotal rules or free-shipping thresholds, so a plan-only checkout is charged no shipping. A subscription line is one that becomes a subscription: a line withrecurring, or that plan line. A plaintype: 'subscription'line with neither ships as it always did, and itsrequiresShippingis honoured. - The address step follows the cart (behavior change). On a checkout with
subscriptions, an omitted
collectnow asks for an address when the cart holds a shippable one-time line, and skips it otherwise. Previously a recurring session skipped it even when the cart held goods. Acollectobject that leaves outshippingAddressstill sets it totrue. - A checkout with no shipping step asks for a billing address when tax needs one
(behavior change). When
collect.shippingAddressresolves tofalseand the application uses a tax provider with an address trigger and thestrictfallback policy, an omittedcollect.billingAddressnow defaults totrue, and ashipping_addresstrigger taxes the billing address. Previously such a checkout could not complete (tax_address_missing). A partialcollectobject no longer setsbillingAddress: falsefor you. - A blocked trial writes no paid first-period invoice (behavior change). When trial eligibility is blocked, the subscription starts active without its trial; nothing was charged for it at checkout, so no paid first-period invoice or PDF is written and its first charge is its first renewal.
- The unified hosted page shows the recurring disclosure (behavior change).
/snow prints the subscription-terms disclosure for a sessionrecurringintent, the same paragraph/calready showed. - Stricter checks (behavior change). Adding a line with
recurringto a cart managed by an external cart provider now answers422 recurring_not_supported_for_cart(wasmode_not_supported). Completing a session whose cart lines carry subscriptions with Net 30 answers422 recurring_terms_unsupported. Order reads and payment webhooks count the legacymetadata.subscription_idpointer only when it is a valid UUID. - An existing order can read
subscriptionId: null(behavior change). An older order whose metadata pointer names a different subscription than itssubscriptionIdcolumn now lists both insubscriptionIds, and itssubscriptionIdreadsnull. - Session intents without
amountare refused on mixed carts (behavior change). A sessionrecurringintent with noamount, on a cart that holds other lines or on any external-provider cart, used to be accepted and renewed at the whole cart total. It now answers422 recurring_amount_required, unlesscreateismanual. A line added to the cart after the session was created is refused with the same code before any money moves. - New error codes.
too_many_subscriptions,recurring_source_conflict,recurring_not_supported_for_cart,recurring_amount_required(a session intent with no amount on a cart with other lines — previously the renewal silently became the whole cart total),recurring_terms_unsupported,subscription_lines_changed(also athrottle.errorcode),recurring_line_immutable,invalid_plan,invalid_trial_days, andinvalid_line_itemfor a free-trial line with tax or a discount. - Packages.
@usethrottle/webhook-types6.0.0 (therecurringfields are objects, not booleans;subscription.create_failedgainslineItemId;WebhookOrderRow.subscriptionIdsandQuoteConvertedEventData.subscriptionIdsare required, so code that builds these payloads, such as test fixtures, must set them); minor releases, published 2026-09-30:cart3.11.0,checkout-sdk2.1.0,checkout-react2.1.0,subscriptions3.2.0,quotes0.5.0,mcp0.3.0,auth0.7.0 andapi-client5.4.0 (regenerated from the production OpenAPI spec).
2026-09-29 — Stalled-renewal reason, send-test filtering, chained-change credit, hosted-cart subscriptions
- New
renewalIssue.reasonvalueperiod_not_advanced(additive). A subscription whose last payment is recorded after its billing period's end used to be skipped by the renewal job silently, indefinitely. It now carries asystem_errorrenewal issue with this reason, counted per tick and alerted once at the 3rd. The card is never charged and status and dunning do not change; it clears like any other system error. -
POST /api/v1/webhook-endpoints/{id}/testhonours the endpoint's subscribed events. An endpoint with a non-empty subscription list now accepts a test only for a type in that list; any other type is400 validation_errornaming the allowed types. An endpoint subscribed to all events accepts any type. - Chained immediate plan or seat changes credit the whole period. A second
immediate change in the same billing period now credits the full value of the period
(cash paid plus the credit applied to produce it, capped at the plan price), so the buyer
no longer pays twice for the same time. Refunds and prorated cancellations still return
cash only. A change the credit covers in full now records a
$0paid subscription invoice carrying the credit increditAppliedCents(visible onGET /api/v1/subscriptions/{id}/invoices) instead of leaving no record of it. See Managing Subscriptions . - Checkout sessions created from an existing cart keep the plan's recurring
intent (bug fix, plus two new refusals). A session created from a cart that
already holds a plan line — Throttle's hosted
/cart/<id>page, an abandoned-cart recovery link, or acartIdhand-off — carried no recurring intent, so the buyer could pay installment 1 of a plan whose subscription was never created. The session now carries the intent over from an earlier same-plan session on the cart, or rebuilds it from the plan line, and keeps the shipping step on mixed carts. When the terms cannot be determined it is refused with422 subscription_terms_undeterminablebefore anything is charged. A recurring hand-off with no buyer email, and none to inherit from the earlier session, is refused with422 recurring_customer_required. See Starting checkout from an existing cart . - Three routes production already served are now documented (additive).
GET /api/v1/shipping-tax/capabilities(which tax capabilities the application is running with),POST /v1/storefront/me/orders/{id}/cancel, andPOST /v1/storefront/me/orders/{id}/return-request(buyer order cancellation and return requests; see Storefront auth). - Packages.
@usethrottle/api-client5.3.0 adds the routes above and theperiod_not_advancedreason;@usethrottle/subscriptions3.1.0 adds the reason;@usethrottle/auth0.6.1 hides Pause ontrialingsubscriptions in the buyer portal. - New guides: Custom cadence, Installment plans.
2026-09-28 — Metered usage lines, stuck-renewal visibility, paid-based credits
- Metered usage bills as its own line item type (additive). A renewal order
that billed usage now carries
usageline items alongside the plan line, needing no fulfilment (deliveryMethod: "none"); the order's owntypestaysrecurring.usageis written by the renewal job only — it appears in order responses, never in a request body: carts, quotes, and order line-item edits still reject it. - New read-only
renewalIssuefield on subscriptions (additive). Set only by the renewal job;PATCH /api/v1/subscriptions/{id}with it is400, and it is absent from the buyer-facing storefront response. A stablereasoncode (tax_quote_failed,internal_error, orrecord_failed) comes with a fixedmessagesentence — never raw error text.kind: "system_error"counts consecutive renewal failures and alerts (one in-app admin notification, one Sentry error) at the 3rd, with no status or dunning change.kind: "orphan_charge"means a renewal charge succeeded but could not be recorded; it alerts immediately and pauses every renewal path on that subscription — the cron, merchantretry-charge, merchantwaive-period, an immediatechange-planorchange-quantity, and the buyer's own retry-charge all return409 renewal_needs_attentionuntil Throttle support resolves it. See Lifecycle and States . - Plan-change, seat-change, and prorated-cancel credits are now based on what was
actually paid (behavior change). Credits and refunds are capped at the net of the
current period's paid (or partially-refunded)
subscription_invoicesrow, not always the plan price. A waived, free, or fully-credited period credits0. A prorated cancel (refund: "prorated"onPOST /api/v1/subscriptions/{id}/cancel) with no payment on the current period now cancels with a$0refund instead of refunding the plan price. A buyer who paid full price sees no change. Subscriptions with no invoice history at all (created before invoice recording, June 2026) keep the old plan-price behavior. - Fixed: an invalid custom cadence answered
400 validation_errorinstead of the documented400 invalid_interval(bug fix). An out-of-vocabularyinterval/intervalUnit, or anintervalCountof 0, negative, or fractional, onPOST /api/v1/subscriptions,.../change-plan, or the checkoutrecurringintent now reaches the cadence validator and answers the documented400 invalid_interval, matching the contract described in the 2026-09-26 entry below. -
@usethrottle/subscriptions3.0.0 (breaking).changePlan()now performs a real plan change — prorated and charged immediately wheneffective: 'now', returning the updated subscription withprorationwhen the server computed one — instead of silently being a term-only PATCH with no proration and no charge. Migration: use the client's newupdate(id, input)for the old behaviour. Also addschangeQuantity(),retryCharge(),waivePeriod(),recordUsage(), andlistUsage();SubscriptiongainsquantityandrenewalIssue. - Packages.
@usethrottle/api-client5.1.0 types theusageline item type and the read-onlyrenewalIssuefield (with itsreasoncodes).
Upgrade recommended: @usethrottle/auth
0.5.0 shows Pause, Resume and Cancel on installment plans, which the API refuses; 0.6.0
hides them and adds installment progress. Update with pnpm add @usethrottle/auth@^0.6.0.
2026-09-26 — Custom billing cadence and installment plans
- Any cadence, not just five intervals (contract change). Send
interval(the existing named values, now includingcustom), orintervalUnit(hour | day | week | month) +intervalCountfor any cadence from 1 hour to 1 year, onPOST /api/v1/subscriptions,.../change-plan, and the checkoutrecurringintent. The server canonicalizes to the largest exact unit and names it when a named interval matches (every 14 days →biweekly). Conflicting input is400 interval_conflict; out-of-bounds or malformed input is400 invalid_interval. Every subscription response now carriesinterval,intervalUnit, andintervalCount. - Installment plans (contract change). Set
totalPayments(2–60) on create or on the checkoutrecurringintent to make a subscription a fixed number of payments that ends itself. The paid signup is payment 1; every later charge and every waived period counts (refunds don't); the plan becomes the new terminal statuscompletedthe moment payment N succeeds — not at the end of the period it covers — and emits the newsubscription.completedevent. Merchants who gate access onstatus = activemust treatcompletedas paid in full. No trial together with it (400 invalid_combination); its amount, cadence, quantity, and plan are locked (409 installment_plan_locked);totalPaymentscannot be changed after creation (400 total_payments_immutable). The merchant can pause, resume, and cancel one; a buyer session cannot (403 installment_plan_merchant_only) — the buyer portal shows progress only. - New status
unpaid. Accepted by the status schema and both published SDK type unions for forward compatibility; not written by the subscription renewal engine today. - Fixed a month-end billing drift bug. Month-based cadences are now computed from a fixed billing anchor instead of chaining from the previous period, so a subscription anchored on Jan 31 correctly renews Feb 28 (29 in a leap year), then Mar 31 — it no longer drifts onto the wrong day after crossing a short month.
- A subscription that falls behind is billed once, not caught up. A subscription more than one period behind (an outage, a stalled retry) is now charged once, for whichever grid period contains the moment of the charge; the periods it missed are skipped rather than billed back-to-back, on every cadence, named ones included.
- Dunning retries now scale with cadence. Retry
nwaitsmin(n × cadence, [1, 3, 7][n − 1] days)— weekly and longer cadences are unchanged at 1 / 3 / 7 days; shorter custom cadences retry proportionally faster. The pre-renewal reminder email now fires only for cadences of 7 days or longer. -
payment.vaultedandsubscription.create_failedpayloads (additive). Theirrecurringobject now includesintervalUnit,intervalCount, andtotalPaymentswhen the session specified a custom cadence or an installment plan. - React package (
@usethrottle/subscriptions). NewcadenceNoun,cadenceLabel, andinstallmentProgresshelpers, theinstallmentlist filter, and the newSubscriptionfields above. - Upgrade note. Integrators on an SDK version released before this date — for
example
@usethrottle/subscriptions1.x or@usethrottle/auth0.5.x — keep working against the new API without crashing, but degrade: a custom cadence renders as an unlabeled raw value (e.g."/ custom") instead of a formatted cadence, an installment plan'scompletedstatus is unlabeled, and an old buyer-portal build may still show a Cancel button on an installment plan — pressing it now gets403 installment_plan_merchant_onlyinstead of succeeding. Upgrade to@usethrottle/subscriptions2.0.0,@usethrottle/checkout-sdk2.0.0,@usethrottle/checkout-react2.0.0,@usethrottle/api-client5.0.0, and@usethrottle/auth0.6.0 to see custom cadences and installment plans labeled correctly. - Package versions. Majors:
@usethrottle/subscriptions2.0.0,@usethrottle/checkout-sdk2.0.0,@usethrottle/checkout-react2.0.0,@usethrottle/api-client5.0.0,@usethrottle/webhook-types5.0.0. Minors:@usethrottle/auth0.6.0,@usethrottle/cart3.10.0,@usethrottle/cli2.9.0,@usethrottle/quotes0.4.0. Patches:@usethrottle/mcp0.2.1,@usethrottle/extension-bridge1.1.1. The majors also carry the earlier unreleased changes listed in each package's CHANGELOG.
2026-09-25 — Automation connectors get their own OAuth audience
- OAuth (contract change). Each OAuth client is registered for one
resource. AI assistants (ChatGPT, Claude) get tokens for
https://mcp.usethrottle.dev/mcp, usable only on the MCP server. Automation connectors (Zapier, Make) get tokens forhttps://api.usethrottle.dev, usable on the REST API and refused on/mcpwith401 oauth_token_wrong_audience. Aresourceparameter naming the other one returnsinvalid_target. - Customer personal data. A connector may be granted
customer_pii:read; with it, responses carry customer names, emails, phones and addresses as they do for an API key holding that scope. An AI assistant still cannot be granted it. Without it, creating, changing or replaying a webhook endpoint over OAuth returns403 oauth_customer_pii_required.
2026-09-25 — OAuth tokens work only through the MCP server
- OAuth (contract change). An AI assistant's OAuth access token is
refused on the REST API with
401 oauth_token_wrong_audience; it authenticates only athttps://mcp.usethrottle.dev/mcp, the audience it was issued for. API keys and dashboard sessions are unaffected. - Hosted MCP over OAuth. Each tool returns a fixed list of fields.
Buyer-typable text (line-item and quote-item names, SKUs, reference ids, company,
address lines other than the country), external ids, free-form fields and webhook
endpoint URLs are withheld. api_get and get_top_products are not offered. File downloads return
403 oauth_file_download_forbidden, and lookups by external id return403 oauth_external_id_forbidden.
2026-09-23 — OAuth connections never see shopper personal data
- OAuth responses (response change, OAuth tokens only). Every
response to an OAuth access token now nulls a shopper's email, phone and
first/last name wherever they appear: the
customeron orders and subscriptions, cart and order addresses, invoice billing snapshots, quotecustomerEmailand comment authors, eventdataandpreviousData, webhook-delivery payloads and audit-log metadata. Addresses keep city, state, postal code, country and company. A payment'sprocessorResponseand a quote'sshareTokenarenull.GET /v1/customerswithqmatches external ids only, and itsemailfilter is ignored. API keys and dashboard sessions see no change. - OAuth token endpoint. A code or refresh token presented by a client it was not issued to is now refused without being consumed, so the real client can still use it. Two concurrent refreshes of one token no longer both succeed.
- Hosted MCP.
scopes_supportedand each tool'ssecuritySchemesnow include the scopes a composite tool uses for its sections (for examplewebhooks:readfor get_integration_status), limited to scopes an OAuth grant can hold. api_get refuses paths that climb out of/api/v1/.
2026-09-23 — OAuth tokens bound to the MCP resource; hosted server strictly read-only
- OAuth (contract change). Access tokens now carry
aud=https://mcp.usethrottle.dev/mcp(the protected resource, per RFC 8707) instead of the client id, and a token with any other audience is rejected. The authorize and token endpoints accept an optionalresourceparameter and answerinvalid_targetwhen it names anything else. Tokens minted before this change stop verifying; a connected client refreshes and continues. The token endpoint also now refuses a code or refresh token that was issued to a different client than the one authenticating. - OAuth scopes.
scopes_supportedon/.well-known/oauth-protected-resourcelists only the read scopes the hosted tools use. Existing grants are unchanged. - Hosted MCP. Every tool in
tools/listnow declaressecuritySchemes(OAuth 2, with the scopes it needs), andseed_test_datano longer registers on the hosted server. It is still available on the local CLI. See MCP server. - Events and webhook deliveries (response change).
GET /v1/eventsandGET /v1/webhook-deliveries(list and by id) now null the nestedcustomeremail, name and phone in the payload for a caller withoutcustomer_pii:read, the same ruleGET /v1/customersalready applied. Keys holding that scope, and dashboard sessions, see the payload unchanged. The stored and delivered webhook body is not affected.
2026-09-21 — Order confirmation emails restored; receipts list what was bought
- Fix (emails). Since 2026-08-10 an order placed through checkout sent
neither the buyer's
customer.order_confirmationnor the merchant'sadmin.new_orderemail. Checkout inserts the order as a draft, which the email router skips on purpose, and the change that madeorder.createdexactly-once removed the second emission those emails had been riding on. They now send when the order leaves draft. No event or webhook contract changed:order.createdis still emitted once. Orders placed in the gap are not emailed retroactively. - Payment receipt.
customer.payment_receiptnow lists the items bought, shows the order number as its reference instead of an internal id, and omits the Card row when the card is not known rather than printing it blank. If you have overridden this template, your override is unchanged;order.itemsandorder.orderNumberare now available to it.
2026-09-21 — Forgot password now works for guests and imported customers
- Storefront auth (behaviour change, no contract change). A customer with
no account — they checked out as a guest, or were imported from another platform — used
to get nothing from
POST /v1/storefront/auth/forgot-password: the response saidsentand no email went out. They now get an account-setup email (new editable templatecustomer.account_setup). Its link goes to your existing reset page with&setup=1appended, and redeeming it throughauth/reset-passwordcreates the account, already verified, with the buyer's past orders visible. It emitscustomer.registeredandcustomer.email_verified. The 202 response is unchanged and still identical for every address. Nothing to change on your side; see Storefront auth.
2026-09-21 — Migration guide: api-client 2.x to 4.x
- Docs. Upgrading
api-client from 2.x to 4.x lists every breaking change between
2.18.0and4.3.0, taken from a diff of the two published packages. Two are worth reading even if you are not upgrading yet:getApiV1OrdersandgetApiV1WebhookDeliveriesgained filters in the middle of their positional argument lists, so existing calls shift and some still compile; andpostApiV1OrdersCompletenow returns 404 on every client version, because the endpoint was removed with thecompletedstatus.
2026-09-21 — Known customers can pay on the /c embed; route prop on the React embeds
- Fix (hosted checkout). A one-off payment on the
/cembed failed for any session that belonged to a known customer: the payment form showed a generic failure and no order was created. Guest sessions and the/sembed were not affected. Fixed; nothing to change on your side. -
@usethrottle/checkout-react(additive).<PaymentEmbed>and<CheckoutEmbed>acceptroute="c" | "s". The components build their own iframe URL and always loaded/c, even when you had chosen/swithbuildCheckoutEmbedUrlon your server. The default is still"c", so existing integrations do not move; passroute="s"to load the unified embed. Card-vault (add-a-card) sessions are only served by/c.
2026-09-21 — processorPaymentMethodId on payment and session responses
- API (additive). Responses that return a payment or a checkout session
(
GET /api/v1/orders/{id}/payments,GET /api/v1/payments/{id},GET /api/v1/checkout/sessions/{id}and the other payment routes) now includeprocessorPaymentMethodId: the processor's id for the saved payment method, ornullwhen none was saved. The same value was already present under an older, undocumented key that does not follow theprocessor*naming used everywhere else. That older key is still returned for now and will be removed in a later release, announced here first. If you read it, switch toprocessorPaymentMethodId.
2026-09-21 — Wildcard origins for preview deployments (non-production)
- API.
allowedOriginsonPUT /api/v1/embed-configandPATCHapplication accepts a leftmost-label wildcard such ashttps://*.preview.example.comin non-production environments. Production still takes exact origins only, and a wildcard over a shared or public suffix (*.vercel.app) is refused everywhere. Existing exact entries behave exactly as before. See Preview origins. - API.
GET /api/v1/checkout-sessions/{id}/embed-configaccepts an optionalparentOriginquery parameter and then addsparentOriginAllowedto the response. Without it the response is unchanged.
2026-09-21 — Completing a session with paymentMethod "card" is now refused
- API (behaviour change).
POST /api/v1/checkout/sessions/{id}/completeandPOST /api/v1/checkout-sessions/{id}/completenow rejectpaymentMethod: "card"with422 payment_method_not_supported, before any order is created. Previously the request returned 200 and left an order inprocessingwith a payment that was never authorized: nothing had been charged. Card payments complete withpaymentMethod: "embedded"and theprocessorTransactionIdfrom the payment embed.cardis unchanged inallowedMethodsand onPOST /api/v1/carts/{id}/checkout. - Docs. The complete-session examples on the Embedded Checkout and
Collection Flags pages showed
"card"(and one a value the API never accepted). They now show the payload the hosted checkout actually sends.
2026-09-20 — Read a checkout session back from the SDK
-
@usethrottle/checkout-sdk. Newcheckout.getSession(sessionId)method, returning the session with its currentstatus. The endpoint (GET /api/v1/checkout/sessions/{id}) already existed; the SDK could cancel a session but not read one. No API change.
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}/statusstill accepts any of the seven statuses.
2026-09-11 — Mint an update-card link from the API
- API.
POST /api/v1/payment-methods/client-tokenaccepts an optionalsubscriptionId. When given, the token names that subscription, lives 14 days, and the response addsupdateCardUrl, 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 gainslinks.updateCardandsubscription.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, andPOST /api/v1/me/subscriptions/{id}/retry-chargecharges a past-due subscription immediately, answeringcharged. Both authenticate withX-Throttle-PM-Token, the same buyer credential as the/me/payment-methodswallet, which may now carry asubscriptionIdclaim. 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/subscribenow advances the workspace'slifecycleStagetoproductionalong 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 stayedsandboxand 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
lineItemIdswith quantities toPOST /api/v1/orders/{id}/fulfillments, which the API has always accepted (and capped with409 over_fulfillment); the dashboard used to send an empty list, sopartially_fulfilledwas 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 inprocessingorpartially_fulfilledwith 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}/resumenow clearscancelAtPeriodEndon a subscription in any non-cancelled status. It used to answer400 invalid_subscription_statefor apast_dueortrialingsubscription, even though/cancelwithatPeriodEnd: trueaccepts 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}/deliverednow moves apendingorprocessingshipment fulfillment tocompletedbefore recording the arrival, emittingfulfillment.completedand thenfulfillment.shipment.delivered, and rolling the order up tofulfilled. A fulfillment created with a tracking number used to sit atpendingwith a delivery date on it, and its order stayedprocessing. 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
lastPaymentAtfrom the moment it is created, alongside the paid period-1 invoice it has always recorded. It used to staynulluntil 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 againstcurrentPeriodEnd, so renewals are unaffected.2026-09-11 — Merchants can decline a quote request
- New.
PATCH /api/v1/quotes/{id}acceptsstatus: 'declined'with an optionaldeclinedReason. Legal fromrequested,under_review,revision_requested, andproposed; terminal apart from archive. The buyer receivescustomer.quote_declinedwith the reason, the timeline records a merchant decline, andquote.declinedfires withdeclinedBy: '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-billinganswers409 client_owns_workspaceonce 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-handoffcancels an outstanding client ownership invite and makes the acting partner member the owner again. Answers409 handoff_not_revocableonce 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/referralscarrieshandoffState:not_started(the partner still owns the client workspace),invited(the client owner has been invited and has not accepted), orclaimed. The existingstatusfield is conversion state and does not change at handoff. Items are now ordered newest first.
- New.
- Fixed. A subscription created active with a signup charge now carries
- Fixed.
- Fixed.
2026-09-11 — Billing writes are owner-only for dashboard sessions
- Enforced. The permission catalog has always declared
workspace:billingas 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 get403 permission_deniedonPOST /api/v1/billing/subscribe,/cancel,/resume, and on writes to/api/v1/billing/payment-methodsunless 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 ofGET /api/v1/workspaces, onGET /api/v1/billing/state, and onworkspaceinGET /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/403responses from an endpoint — workspace or extension, any attempt — sendsystem.webhook_auth_failingby 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.pingnow also runs against every active extension endpoint two minutes after each deploy and every six hours. A non-2xx answer sendssystem.extension_webhook_probe_failedto 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-secretthe old secret keeps verifying forgraceSeconds(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: 0keeps that behaviour for a leaked secret. The response and the installation secret read reportpreviousSecretExpiresAt. - 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 signedextension.webhook_secret_rotated(no secret inside) so it can fetch the new one before the deadline. Delivered to the installation's own endpoint likeextension.uninstalled. - Verifiers.
@usethrottle/webhook-typesand@usethrottle/extension-bridgeaccept multiplev1digests (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-typesgainsexample, 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.
- Dual-signed deliveries. After
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 newenvironmentKindfield 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
datafields without bumpingversion. Consumers must ignore or strip unknown keys.versionchanges 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
environmentKindin 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.productionornon_production, on every outbound webhook and extension delivery. If your destination is a live system, dropnon_productiondeliveries 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'screatedAt, not the replay time. Same id, same payload, same timestamp — the send time is thet=in the signature header. -
GET /api/v1/event-typesstates units. A top-levelconventionsobject 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
401is 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, carryinginstallationId,extensionId,applicationId,uninstalledAtandreason. It needs no subscription, scope, or API key on the extension's side, is sent after the installation readsuninstalled, 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
-
externalDomainsaccepts*.example.com. One leading wildcard label, for vendor SDKs that publish*.vendor.comas their CSP guidance instead of a fixed host list. It becomeshttps://*.example.comin 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
blockedoutcome. The sandbox document forwards the browser's own violation report, so a request to an undeclared host counts as blocked in script health, and thescript.blockedevent 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 carriesdata.customer. Same shape and| nullsemantics asorder.*, attached at delivery from the fulfillment's order; the embeddedorderalso gainscustomerId. Until now the tracking number lived only onfulfillment.shipment.shippedand the buyer only onorder.*, so a shipping notification could not be built from a single event. -
fulfillment.shipment.shippedfires when a shipment is created with tracking.POST /api/v1/orders/{orderId}/fulfillmentsaccepts an optionalshipmentblock (same fields asPATCH .../shipment); atrackingNumberthere fires the event right afterfulfillment.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 andimport.completed.script.loaded,script.blocked(detail.reasonnames the cause, e.g.consent) andscript.errorunder the newapplication_scripts:readscope, alongside the scripts feature below.import.completedfires 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 inGET /api/v1/event-types; an extension must hold that scope before it can subscribe. - 2026-08-24 —
fulfillment.shipment.deliveredmeans arrival, and carries the shipment. It fires when a shipment is recorded as arrived viaPOST /api/v1/orders/{orderId}/fulfillments/{id}/delivered, no longer when the shipment fulfillment is marked completed. Completing is carrier handoff; listen forfulfillment.completedif that is what you want. The payload gainedshipment(now a required key) so the arrival carries its tracking details. - 2026-08-22 —
order.completedrenamed toorder.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 incrementsinstallSequenceon the installation. The response body is the secret: do not attach it to error objects or logs. - August 2026 —
GET /api/v1/event-typesgainedtrigger,dataShapeandrequiredDataKeys. 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
/cand/s(non-embed mode only) — no DOM access, sandbox tier only. See Your Own Scripts. -
New
/api/v1/application-scriptsendpoints. 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/:sha256serves script source by content hash — immutable, unauthenticated, and the one point of truth every delivery path (the checkout-web/es/<id>/<sha256>.jsproxy and the/sandboxdocument route) resolves through.
2026-09-07 — Extension listing images over the API
-
POST /api/v1/extensions/icon-uploadandPOST /api/v1/extensions/screenshot-uploadaccept secret API keys. Both previously answered403 clerk_requiredto anything but a dashboard session, which made image upload the one step of a marketplace submission that could not be scripted. Any credential holdingextensions:writecan 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/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.