Invoke endpoints from a scriptPrivate preview
Make HTTP requests to external endpoints from your scripts.
You can enable HTTP requests to an external endpoint from your script. Verify that your chosen extension point supports HTTP requests by checking the extension point specification page.
Add an endpoint to the app manifest
- Open the
stripe-app.file in your app’s root directory. The file includes anyaml extensionskey with anidthat matches the extension ID you chose for the generate command. Inside that object, there’s amethodskey. - Add an
endpointskey with the typecustom_at the same level ashttp methods. - Provide an
idand the endpointurl.
extensions: - id: //your extension ID name: //your extension name methods: [] endpoints: - id: com.my_script.send_notifications type: custom_http managed_sandbox: url: https://your-url
To use the endpoint with live mode accounts, use live instead of managed_.
Call the endpoint from your script
In your source file, use endpointFetch to call the endpoint. Make sure the endpoint value matches the id you specified in the manifest. This example uses a custom workflow action, but endpointFetch works the same way in any extension point that supports it.
Mark your execute method as async and use await with endpointFetch to ensure the HTTP request completes before your script returns.
endpointFetch parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
endpoint | string | Yes | The endpoint id declared in stripe-app.. |
path | string | Yes | URL path appended to the endpoint base URL. |
method |
| Yes | HTTP method for the request. |
body | string | No | JSON-stringified request body. |
headers | Record<string, string> | No | Additional HTTP headers to include in the request. |
Response
On success, endpointFetch returns an object with the following properties:
| Property | Type | Description |
|---|---|---|
ok | boolean | true for successful responses. |
status | number | HTTP status code (200-299). |
body |
| Response body as a JSON string. Parse it with JSON. to access the data. |
export default class MyCustomAction implements Extend.Workflows.CustomAction<MyCustomActionConfig> { async execute( request: Extend.Workflows.CustomAction.ExecuteCustomActionRequest, _config: MyCustomActionConfig, _context: Context ) { const customInput = request.customInput as Record<string, unknown>; await endpointFetch({ endpoint: 'com.my_script.send_notifications', path: '/api/notifications', method: 'POST', body: JSON.stringify({ message: `Payment received from ${customInput.name}`, }), }); return {}; } getFormState( _request: Extend.Workflows.CustomAction.GetFormStateRequest, _config: MyCustomActionConfig, _context: Context ) { return { values: _request.values, config: {}, }; } }
The request object contains input specific to the extension point. In this custom workflow action example, request. contains dynamic data mapped from the workflow trigger event, such as a customer name. You define these fields in custom_ and reference them in stripe-app. under the execute method.
The config object contains static values set once when the extension is configured, such as a notification channel. You define these fields in config..
For more details, see Custom actions.
Runtime considerations
endpointFetch throws an error for non-2xx responses and network failures. Wrap calls in a try-catch block to handle errors. Scripts that invoke endpoints have a 30-second timeout.
Configure authorization
You can use token-based authorization or header-based authorization. First, create a secret on the account that runs the script. Then add an auth key to the app manifest stripe-app. that specifies the authorization type and required values.
Configure token-based authorization
Provide the secret_ in the auth key.
endpoints: - id: com.my_cool_script.endpoint_token type: custom_http managed_sandbox: url: https://example.com/api auth: secret_name: endpoint_bearer_token type: bearer_token
Configure header-based authorization
Provide the header_ and secret_ in the auth key.
endpoints: - id: com.my_cool_script.endpoint_header type: custom_http managed_sandbox: url: https://example.com/api auth: secret_name: endpoint_header_secret type: header header_name: X-Foo-Header
Test endpoint calls
Script extensions generated from the Stripe CLI come with vitest set up. You should add unit tests to validate all aspects of your script’s desired behavior. In particular, use withEndpointFetchMock from the test helpers package to verify your endpointFetch calls without making real HTTP requests.
import { withEndpointFetchMock } from '@stripe/extensibility-test-helpers/endpoint-fetch';
The endpoint fetch test helper requires @stripe/extensibility-test-helpers version 1.3.0 or later. To upgrade, run this command from the root of your app:
pnpm upgrade '@stripe/extensibility-test-helpers@^1.3.0'
Write a test
Declare an array of stubs, then run your extension code inside the withEndpointFetchMock callback. Each stub pairs a request pattern with a canned response you define.
it('sends a notification to the CRM', async () => { await withEndpointFetchMock( [ { request: { endpoint: 'com.my_script.send_notifications', method: 'POST', path: '/api/notifications', }, response: { status: 200, body: JSON.stringify({ delivered: true }), }, }, ], async () => { const result = await myExtension({ name: 'Jenny Rosen' }); expect(result).toEqual({}); } ); });
The mock matches each endpointFetch call against your stubs in declaration order, with the first match winning. After the callback returns, the mock fails the test if any mandatory stub was never called.
Match request fields
All request fields are optional. When you specify multiple fields, all must match (AND logic). Omitted fields match anything.
| Field | Matches against |
|---|---|
endpoint | The endpoint id from stripe-app.. |
method | HTTP method (GET, POST, PUT, DELETE, PATCH). |
path | URL path suffix. |
bodyParameters | Values of top-level fields in the JSON body, as strings. Nested and array-valued fields are compared in their JSON form, without spaces. Examples: string-value, 17, true, ["value1","value2"], {"key1":"value1"}. |
headers | Request headers (case-insensitive keys). |
Each field accepts a plain string (shorthand for exact equality) or a matcher object with one operator.
| Operator | Type | Description |
|---|---|---|
equalTo | string | Exact equality. |
matches | string or RegExp | Value must match the pattern (unanchored). |
contains | string | Value must contain this substring. |
doesNotMatch | string or RegExp | Value must not match the pattern (unanchored). |
absent | true | Field must not be present. |
Matchers equalTo, matches, contains, and doesNotMatch accept an optional caseInsensitive: true flag. If your script makes multiple endpointFetch calls, you select which response stub is used for each mocked call through request matchers. Even if your script makes only one call, narrowing the request pattern verifies your assumptions about that call, so the test doesn’t silently pass against the wrong request.
{ request: { endpoint: 'com.my_script.send_notifications', method: { equalTo: 'POST' }, path: { contains: '/api/notifications' }, bodyParameters: { channel: { matches: /^test_channel_[123]/, caseInsensitive: true }, messageLength: '1500', thread: { absent: true }, }, }, response: { status: 200, body: JSON.stringify({ delivered: true }), } }
Handle errors
The mock replicates production behavior. For non-2xx responses, withEndpointFetchMock throws a MockEndpointFetchError with the same shape as the production EndpointFetchError, so your error-handling code works without modification.
it('handles rate limiting', async () => { await withEndpointFetchMock( [ { request: { endpoint: 'com.my_script.send_notifications' }, response: { status: 429, body: JSON.stringify({ error: 'rate_limited' }) }, }, ], async () => { await expect(myExtension({ name: 'Jane Diaz' })).rejects.toThrow(); } ); });
| Status | Error code |
|---|---|
| 400 | EXT_ |
| 401 | EXT_ |
| 403 | EXT_ |
| 404 | EXT_ |
| 408, 504 | EXT_ |
| 429 | EXT_ |
| 503 | EXT_ |
| Other | EXT_ |
Mark stubs as optional
By default, every stub must be called during the test, or the test will fail. Set optional: true for calls that might not happen.
await withEndpointFetchMock( [ { request: { endpoint: 'primary-api' }, response: { status: 200, body: '{}' }, }, { request: { endpoint: 'analytics' }, response: { status: 200, body: '{}' }, optional: true, }, ], async () => { await myExtension(); } );