Skip to content
Create account or Sign in
The Stripe Docs logo
/
Ask AI
Create accountSign in
Get started
Payments
Revenue
Platforms and marketplaces
Money management
Developer resources
APIs & SDKsHelp
Overview
Versioning
Changelog
Upgrade your API version
Upgrade your SDK version
Essentials
SDKs
API
Testing
Stripe CLI
Tools
Stripe Dashboard
Stripe Projects
Workbench
Developers Dashboard
Stripe for Visual Studio Code
Terraform
Stripe Discord server
Features
Workflows
Batch jobs
Event destinations
Stripe health alertsStripe SignalsFile uploads
AI tools
Agent pluginsModel Context ProtocolAgent skillsStripe Directory
Extend Stripe
Overview
Build Stripe apps
Use apps from Stripe
Build extensions
    How extensions work
    Build a prorations extension with a script
    Build a custom action with a script
    Build a custom action with a remote function
    Extension points
    Scripts
    Custom actions
Custom objects
Security and privacy
Security
Activity logsStripebot web crawler
Privacy
Partners
Partner ecosystem
Partner certification
United States
English (United States)
  1. Home/
  2. Developer resources/
  3. Build extensions
Private preview

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.

Email
Submit
Privacy policy

Before you begin

Before you start creating an extension, make sure that you have:

PrerequisiteSetup
Stripe account with access to the extensions private previewIf you don’t have access, sign up for early access.
Stripe CLI v1.12.4 or later, logged into your accountstripe 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 laternode --version
pnpm v10 (v11 is not supported)pnpm --version
Stripe Apps CLI plugin v1.19.0 or laterstripe plugin install apps then confirm with stripe apps -v
Generate plugin v0.11.5 or laterstripe 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 src/index.ts. 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 generated/config.schema.json 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'; }

OptionalTest your custom logic

OptionalBuild

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.

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.
Was this page helpful?
YesNo
  • Need help? Contact Support.
  • Chat with Stripe developers on Discord.
  • Check out our changelog.
  • Questions? Contact Sales.
  • LLM? Read llms.txt.
  • Powered by Markdoc
On this page