Private preview
Create a prorations extension with a script Private preview
Define custom prorations logic for Stripe Billing by writing a script.
This guide describes how to create a prorations extension for Stripe Billing using a script. You write the script in TypeScript and it runs on Stripe’s managed runtime, packaged in a Stripe App. As an example, the extension in this guide customizes how proration amounts are calculated when subscriptions change mid-cycle using the prorations extension point. You can use the same steps for any of the other available extension points.
Get early access to extensions
Enter your email to request access.
Before you begin
Before you start creating an extension, make sure that you have:
| Prerequisite | Setup |
|---|---|
| Stripe account with access to the extensions private preview | If you don’t have access, sign up for early access. |
| Stripe CLI v1.12.4 or later, logged into your account | stripe version to check. Install or upgrade: brew upgrade the relevant part of the product |
| A sandbox (recommended for first-time setup) | Create one in the Dashboard if you don’t have one. |
| Node.js v22 or later | node --version |
| pnpm v10 (v11 is not supported) | pnpm --version |
| Stripe Apps CLI plugin v1.19.0 or later | stripe plugin install apps then confirm with stripe apps -v |
| Generate plugin v0.11.5 or later | stripe plugin install generate then confirm with stripe generate --version |
npm packages used by extensions
Create an app
Extensions are packaged within Stripe Apps. If you don’t already have an app, create one to contain your extension:
stripe generate app helloworld
cd helloworld
This creates the app with the workspace layout needed for extension development. Follow the prompts by entering the following information:
- ID : Accept the auto-generated app ID or create a custom one. Stripe identifies your app using this ID. Your app ID must be globally unique. You can’t change this after you first upload your app.
- Display name : Enter a display name. This is the name the Dashboard displays for your app. You can change the name later.
App directory file structure
Migrate an existing app
If you have an existing app created with stripe apps create, migrate it first:
cd my-existing-app
stripe apps migrate
After migration, you’ll see both stripe-app.json and stripe-app.yaml. The YAML file is the manifest file and is now the source of truth.
Generate the extension
Generate an extension from your app directory. The command takes the extension point ID, an extension identifier, and the implementation type:
stripe generate extension billing.prorations my-proration script
Note
To list all valid extension point IDs for generating a different extension type, run stripe generate info extension-point-ids.
Generating the extension adds the following folders and files to your app directory:
- extensions/ : A folder with a subdirectory named after your extension ID.
- src/index. ts : Your extension’s entry point. Exports a default function that conforms to the extension point.
- src/index. test. ts : Starter unit tests.
- stripe-app. yaml : An updated app manifest with the extension metadata.
your_app_directory/
├── extensions/
│ └── my-proration/
│ ├── src/
│ │ ├── index.ts # Script implementation
│ │ ├── index.test.ts # Tests
│ │ └── custom_input.schema.json # Custom input JSON Schema
│ ├── generated/
│ │ ├── config.schema.json # Generated config schema
│ │ └── config.ui.json # Generated config UI schema
│ ├── eslint.config.mts
│ ├── package.json
│ ├── tsconfig.json
│ └── tsconfig.build.json
├── custom-objects/ # Custom data types (optional)
├── ui/ # UI extensions (optional)
├── tools/
│ └── test.mts # Cross-workspace test runner
├── stripe-app.yaml
├── package.json
├── eslint.config.mts
├── vitest.config.mts
└── pnpm-workspace.yaml
You implement your extension by editing files in the extension’s src/. Run pnpm build, pnpm lint, and pnpm test from the app root directory to compile, typecheck, and run unit tests across all workspaces.
Files in generated/ are auto-generated from your extension’s Config TypeScript interface when you run pnpm build. They control the Dashboard configuration UI for the extension installer. Don’t edit these files directly, modify your Config interface and rebuild instead.
The generate command also creates a custom-objects/ workspace for defining custom data types. See Custom objects for details. You can leave this workspace empty if you don’t use custom objects.
The ui/ workspace is for building UI extensions. You can leave it empty if your app only uses script extensions.
The root pnpm test command runs tools/test.mts, which discovers and runs tests across all workspaces, using vitest for extensions and jest for UI views.
Write your custom logic
Before you write your custom logic, change into your extension directory and use pnpm run dev to watch for file changes and catch lint or test failures:
cd extensions/my-proration # Replace with your extension directory name
pnpm run dev
Enter Ctrl+C to quit the dev watcher when you’re done. You can set breakpoints in your IDE and debug both tests and extension logic, just like any other TypeScript project. Stripe runs the static analysis for you when you build or upload an app.
From your extension folder, open the relevant part of the product. This file contains stubbed methods with JSDoc annotations and links to relevant documentation. Replace the placeholder with your custom logic:
import type { Billing, Context } from '@stripe/extensibility-sdk';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface MyProrationsConfig extends Record<string, unknown> {}
export default class MyProrations implements Billing.Prorations<MyProrationsConfig> {
prorateItems(
_request: Billing.Prorations.ProrateItemsInput,
_config: MyProrationsConfig,
_context: Context
) {
// TODO: implement your proration logic here
return {
items: [],
};
}
}
Your extension must conform to the interface defined by the extension point. All arguments are passed by value. When you implement your custom logic, drop the underscore on any argument you reference.
Stripe provides three arguments at runtime:
- request : Input data for the method. The type varies by extension point — for example, ProrateItemsInput for the Proration extension point.
- context : Execution context for the current run, including which account is executing, whether it’s live mode, and the current clock time.
- config : Your custom configuration values. Define the MyProrationsConfig type to capture any values a script requires from your users. See Define configuration to add fields with validation.
Define configuration
Define configuration fields that your users set in the Stripe Dashboard when they use your extension. Add properties to your config interface with TSDoc annotations to control labels, validation, and field types. Stripe turns your TypeScript types into JSON schemas in the relevant part of the product when you build or upload your app.
You can use standard TypeScript types like string, number, and boolean. For Stripe-specific types like MonetaryAmount, Percent, Decimal, and Timestamp, import them from @stripe/extensibility-sdk. For more details on the configuration lifecycle and supported data types, see Define configuration.
The example below shows an extension’s MyProrationsConfig interface defined to include fields using supported data types. The TSDoc annotations set Dashboard labels ( @displayName), validation constraints ( @minimum, @maxLength), and default values ( @defaultValue).
import { type MonetaryAmount } from '@stripe/extensibility-sdk';
/**
* @displayName Proration calculator settings
*/
interface MyProrationsConfig extends Record<string, unknown> {
/**
* @displayName Maximum proration amount
*/
maxProrationAmount: MonetaryAmount;
/**
* @displayName Discount percentage
* @minimum 0
* @maximum 100
* @defaultValue 0
*/
discountPercent?: number;
/**
* @displayName Proration label
* @minLength 1
* @maxLength 50
*/
label: string;
/**
* @displayName Rounding method
*/
roundingMethod: 'up' | 'down' | 'nearest';
}
Optional Test your custom logic
Optional Build
Upload
Verify that you’re logged into the intended account from the Stripe CLI and the Dashboard. We recommend using a sandbox:
stripe login
This opens the Stripe Dashboard for authentication.
From your app’s root directory, upload your app’s source code to Stripe:
stripe apps upload
You are about to upload your app to Testing
Name: Acme Billing App
ID: com.example.acme-billing-app
Version: 0.0.1
✔ Built files
✔ Packaged files for upload
✔ Uploaded
🌐 Stripe needs to process your files before this version can be installed.
To see your upload, click Enter. (You can also go to Apps > Created apps in the Dashboard and click your app’s name and open the Versions tab.) When the review status is Ready to install, click Install and select where to install the app.
The installation location depends on where you uploaded the app:
- If you uploaded the app to live mode, you can install it in any sandbox.
- If you uploaded the app to a sandbox, you can install it in the same sandbox and in live mode.
- To install the app in another sandbox: switch to the other sandbox by using stripe login and upload and install the app there.
Update the app and extension version numbers as needed. To update an extension version, you must also update the app version. Stripe recommends semantic versioning. App uploads to live accounts might require additional review by Stripe. To iterate faster, use a sandbox.
Writing your proration logic
Proration logic depends on the type of prorations you’re handling: debit or credit. For both types, you must keep the signs the same as the original proration factor: positive for debits, negative for credits.
- Debit prorations : A debit proration is a charge for the remaining time on a subscription item before it next cycles. The proration factor your script returns is the fraction of the original billing period (represented by the priceIntervalDuration field) that the remaining time represents. The service period is from the time the update takes effect to the next time the item cycles, and its duration is equal to the original proration factor multiplied by the priceIntervalDuration .
- Credit prorations : A credit proration credits a portion of the remaining time charged after an item is updated (for example, to add more seats). The credit item will have a corresponding debit reflecting the original service period for the debit. The proration factor of a credit proration is negative and represents the fraction of the debit to credit back. For example, if a debit is for a 30 day period, and you intend to credit the remaining 6 days, the proration factor would be -0. 2 .
The line item period your script returns is used to render the interval on the invoice line items and should always have the same end date as the service period. The proration factor must equal the line item period divided by the basis period (either the service period or the corresponding debit service period).
The proration extension is commonly used to change the discrete unit of time used for prorations. By default, Stripe prorates to the nearest second but some users need to prorate to the day, week, or month. Ideally, the time slices you prorate for should divide evenly into your service periods. If you can’t evenly divide the time slices, make sure you consider your logic for fractional proration periods. For example, if you bill your customers on a monthly basis but prorate on a weekly basis, you need to decide how to allocate remaining money for two days in a 30-day month.
In unusual circumstances, it’s possible for the original proration factor to be greater than 1.0. If a customer updates their subscription to switch from a monthly price to a daily price with a backdated proration date, Stripe creates a single line item with a proration factor representing the time from the backdated date through the current period to catch the subscription up. For example, if you change a monthly subscription anchored at February 1 into a daily subscription on January 10 with a proration date on January 5, the original proration factor will be 6.0.
Upload status
After you upload your app, the Status field on the details page for Created apps displays values such as the following:
- Processing : Stripe is processing your app before this version can be installed.
- In review : Stripe is reviewing your app. Apps might remain in this state longer if you’re uploading to a live account.
- Changes requested : Click Changes requested to see the details. Resolve the changes in your app code, then upload your app again.
Install and activate
After you install the app, activate your extension in Billing customizations.
Handle runtime errors
In most cases, catch errors and provide fallback behavior. Throwing an exception halts the entire code execution associated with the script, so only throw when no other option exists.
Observe script runs
Use Workbench to see the details of script runs, such as run ID, input and output arguments, and whether any errors occurred. For more information, see View extension run details.
To receive notifications when an extension run fails, subscribe to the v2.extend.extension_run.failed event. Set up an event destination that subscribes to this event. You can also trigger a Workflow from this event.
Script runtime behavior
Most TypeScript features work on Stripe’s runtime, but the following patterns aren’t available. The build and upload steps catch these automatically:
- Code evaluation such as eval() or new Function()
- Timer functions such as setTimeout() , setInterval() , and setImmediate()
- Global scope access via global or globalThis
- Process APIs such as process. exit() or process. env
- console. log() . Use Workbench to view script run logs.
- Embedded API keys or secrets. Use the Stripe secret store to manage sensitive values.
- Network access APIs such as fetch() . Use endpointFetch() to invoke endpoints from a script .
Stripe doesn’t currently support third-party libraries as dependencies.
See also
- Learn how extensions work .
- Review distribution options to share your app.
- Learn how to store secrets for authorization.
