@stripe/extensibility-sdk
v1.1.0
Published
Stripe Apps Extensibility SDK - API objects, standard library, and ESLint rules
Maintainers
Keywords
Readme
@stripe/extensibility-sdk
Runtime SDK for Stripe script extensions. Provides TypeScript extension interfaces, a
Stripe-safe arbitrary-precision Decimal type, scalar primitives, and the wire-format
transformation framework used by the platform dispatcher.
Subpath exports
| Import path | Use for |
| ----------------------------------------- | ------------------------------------------------ |
| @stripe/extensibility-sdk | Decimal, scalar types, Ref, wire errors |
| @stripe/extensibility-sdk/extensions | Implementing extension interfaces |
| @stripe/extensibility-sdk/internal | Internal registry metadata (used by build tools) |
| @stripe/extensibility-sdk/jsonschema | JSON Schema types and helpers |
| @stripe/extensibility-sdk/config-values | Configuration value types and helpers |
Extension interfaces
Each extension interface is a TypeScript interface your default-export class must implement.
The Config type parameter is the shape of your extension's configuration object.
Billing
Billing.Bill.DiscountCalculation
Computes discounts applied to a bill.
import type { Billing, Context } from '@stripe/extensibility-sdk/extensions';
interface MyConfig extends Record<string, unknown> {
maxDiscountPercent: number;
}
export default class MyExtension implements Billing.Bill.DiscountCalculation<MyConfig> {
computeDiscounts(
request: Billing.Bill.DiscountCalculation.DiscountableItem,
config: MyConfig,
context: Context
): Billing.Bill.DiscountCalculation.DiscountResult {
const { grossAmount } = request;
const discountAmount = grossAmount.amount
.mul(config.maxDiscountPercent)
.div(100, 8, 'half-even');
return {
discount: {
amount: { amount: discountAmount, currency: grossAmount.currency },
},
};
}
}Key types in the Billing.Bill.DiscountCalculation namespace:
| Type | Description |
| ------------------ | ------------------------------------------------------------------------- |
| DiscountableItem | Request: line items, gross amount, customer, billing reason, subscription |
| DiscountResult | Response: discount with MonetaryAmount |
| LineItem | A single line with subtotal, quantity, price, period |
| Price | Price with scheme, tiers, recurring config |
| BillingReason | Enum: 'subscription_cycle', 'manual', etc. |
| AnyTimeRange | Discriminated union: oneTime | timeRange |
| MonetaryAmount | { amount: Decimal; currency: Currency } |
Billing.CustomerBalanceApplication
Computes how much of a customer's credit balance to apply to a bill.
import type { Billing, Context } from '@stripe/extensibility-sdk/extensions';
export default class MyExtension implements Billing.CustomerBalanceApplication<
Record<string, unknown>
> {
computeAppliedCustomerBalance(
request: Billing.CustomerBalanceApplication.CustomerBalanceApplicationInput,
_config: Record<string, unknown>,
_context: Context
): Billing.CustomerBalanceApplication.CustomerBalanceApplicationResult {
// Apply at most 50% of the bill total from the customer balance
const halfTotal = request.totalAmount.amount.div(2, 8, 'half-even');
const applied = request.customerBalance.amount.lt(halfTotal)
? request.customerBalance.amount
: halfTotal;
return {
appliedCustomerBalance: { amount: applied, currency: request.totalAmount.currency },
};
}
}Key types:
| Type | Description |
| ---------------------------------- | ------------------------------------------------------------------ |
| CustomerBalanceApplicationInput | { totalAmount: MonetaryAmount; customerBalance: MonetaryAmount } |
| CustomerBalanceApplicationResult | { appliedCustomerBalance: MonetaryAmount } |
Billing.InvoiceCollectionSetting
Overrides collection settings (payment method, auto-advance, etc.) for an invoice.
import type { Billing, Context } from '@stripe/extensibility-sdk/extensions';
export default class MyExtension implements Billing.InvoiceCollectionSetting<
Record<string, unknown>
> {
collectionOverride(
request: Billing.InvoiceCollectionSetting.InvoiceCollectionRequest,
_config: Record<string, unknown>,
_context: Context
): Billing.InvoiceCollectionSetting.InvoiceCollectionResponse {
const isSubscription = request.parent.type === 'subscription';
return {
autoAdvance: isSubscription,
};
}
}Key types:
| Type | Description |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| InvoiceCollectionRequest | Request: collection settings, parent, customer |
| InvoiceCollectionResponse | Response: optional autoAdvance override |
| CollectionSettings | Current settings with autoAdvance, collectionMethod, paymentMethods |
| ParentType | 'subscription' | 'contract' | 'quote' | 'billing_cadence' | 'subscription_schedule' | 'standalone' |
| CollectionMethod | 'charge_automatically' | 'send_invoice' |
| PaymentMethodType | 'card' | 'sepa_debit' | … |
Billing.Prorations
Computes proration adjustments when subscription items change mid-period.
import type { Billing, Context } from '@stripe/extensibility-sdk/extensions';
export default class MyExtension implements Billing.Prorations<Record<string, unknown>> {
prorateItems(
request: Billing.Prorations.ProrateItemsInput,
_config: Record<string, unknown>,
_context: Context
): Billing.Prorations.ProrateItemsResult {
return {
items: request.items.map((item) => ({
key: item.key,
prorationFactor: item.currentProrationFactor,
lineItemPeriod: item.servicePeriod,
})),
};
}
}Key types:
| Type | Description |
| -------------------- | ----------------------------------------------------------------- |
| ProrateItemsInput | { items: ProratableItem[] } |
| ProrateItemsResult | { items: ProratedItem[] } |
| ProratableItem | Line item with key, type, price, proration factor, service period |
| ProratedItem | Output: key, proration factor, line item period |
| ItemType | 'credit' | 'debit' |
Core
Core.Workflows.CustomAction
Implements a custom action callable from Stripe workflows. execute is required;
getFormState is optional and drives dynamic form rendering.
import type { Core, Context } from '@stripe/extensibility-sdk/extensions';
interface MyConfig extends Record<string, unknown> {
webhookEndpoint: string;
webhookPath: string;
}
export default class MyExtension implements Core.Workflows.CustomAction<MyConfig> {
async execute(
request: Core.Workflows.CustomAction.ExecuteCustomActionRequest,
config: MyConfig,
_context: Context
): Promise<Core.Workflows.CustomAction.ExecuteCustomActionResponse> {
const { customInput } = request;
await endpointFetch({
endpoint: config.webhookEndpoint,
path: config.webhookPath,
method: 'POST',
body: JSON.stringify(customInput),
});
return {};
}
async getFormState(
request: Core.Workflows.CustomAction.GetFormStateRequest,
_config: MyConfig,
_context: Context
): Promise<Core.Workflows.CustomAction.GetFormStateResponse> {
return { values: request.values, config: {} };
}
}Key types:
| Type | Description |
| ----------------------------- | ---------------------------------------------------------------------- |
| ExecuteCustomActionRequest | { customInput: Record<string, unknown> } |
| ExecuteCustomActionResponse | Record<string, never> (empty object) |
| GetFormStateRequest | { values: Record<string, unknown>; changedField?: string } |
| GetFormStateResponse | { values: Record<string, unknown>; config: Record<string, unknown> } |
Both execute and getFormState may return synchronously or as Promise.
Context
Every extension method receives a Context as its third argument:
import type { Context } from '@stripe/extensibility-sdk/extensions';
function handleContext(context: Context) {
console.log(context.type); // e.g. "core.workflows.custom_workflow_action"
console.log(context.id); // unique call ID, for debugging
console.log(context.livemode);
console.log(context.stripeContext); // set for cross-account calls
console.log(context.clockTime); // optional SDK-provided clock timestamp
}Stdlib
Decimal
Arbitrary-precision decimal arithmetic backed by big.js. All monetary values in
extension interface requests and responses use Decimal.
import { Decimal } from '@stripe/extensibility-sdk';
const a = Decimal.from('10.50');
const b = Decimal.from(3);
a.add(b); // Decimal('13.50')
a.sub(b); // Decimal('7.50')
a.mul(b); // Decimal('31.50')
a.div(b, 8, 'half-even'); // Decimal('3.50000000')
a.round({ precision: 2, direction: 'half-even' }); // Decimal('10.50')
a.round('penny'); // Decimal('10.50') — preset shorthand
a.eq(Decimal.from('10.50')); // true
a.lt(b); // false
a.lte(b); // false
a.gt(b); // true
a.gte(b); // true
a.toString(); // '10.50'
a.toFixed(4, 'half-even'); // '10.5000'Rounding directions: 'ceil' · 'floor' · 'round-down' · 'round-up' ·
'half-up' · 'half-down' · 'half-even'
Rounding presets: 'penny' (2 dp, half-even) · 'dollar' (0 dp, half-even)
div() and toFixed() always require an explicit precision and rounding direction —
there are no implicit defaults to prevent silent precision loss.
Scalar types
| Type | Description | Constructor |
| ----------------- | -------------------------- | ------------------------------------ |
| Integer | Whole number (no decimals) | Integer.from(n, direction) |
| PositiveInteger | Non-negative integer (≥ 0) | PositiveInteger.from(n, direction) |
These are branded types — they carry a compile-time brand so TypeScript rejects raw
number or string where the stricter type is required.
import { Integer, PositiveInteger } from '@stripe/extensibility-sdk';
const count = Integer.from(42, 'round-down'); // Integer
const qty = PositiveInteger.from(3, 'round-down'); // PositiveIntegerRef
A typed reference to a Stripe object (ID + optional resolved value).
import type { Ref } from '@stripe/extensibility-sdk';
import type { Customer } from '@stripe/extensibility-api-objects';
type CustomerRef = Ref<Customer>;Wire errors
Thrown by the platform dispatch wrapper when wire-format data is invalid.
| Class | When thrown |
| ---------------- | --------------------------------------------------------- |
| WireReadError | Invalid or missing field in the incoming wire request |
| WireWriteError | Invalid or missing field in the outgoing SDK response |
import { WireReadError, WireWriteError } from '@stripe/extensibility-sdk';In production, the platform catches and surfaces these as structured errors. In tests,
assert against them directly by invoking the generated interface-specific
$platformWrap... helper for the interface under test. For example:
// Pseudocode: replace `$platformWrap...` with the generated wrapper for your interface.
expect(() => $platformWrap...(MyImpl, badInput, {}, ctx)).toThrow(WireReadError);License
MIT
