Create a prorations extension with a scriptPrivate 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 stripe/stripe-cli/stripe |
| 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. and stripe-app.. 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.: Your extension’s entry point. Exports a default function that conforms to the extension point.ts src/index.: Starter unit tests.test. ts
stripe-app.: An updated app manifest with the extension metadata.yaml
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., 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 src/index.. 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,ProrateItemsInputfor 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 theMyProrationsConfigtype 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 generated/config. 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'; }
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 loginand upload and install the app there.
- To install the app in another sandbox: switch to the other sandbox by using
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.
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()ornew Function() - Timer functions such as
setTimeout(),setInterval(), andsetImmediate() - Global scope access via
globalorglobalThis - Process APIs such as
process.orexit() process.env console.. Use Workbench to view script run logs.log() - Embedded API keys or secrets. Use the Stripe secret store to manage sensitive values.
- Network access APIs such as
fetch(). UseendpointFetch()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.