# Invoke endpoints from a script

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](https://docs.stripe.com/extensions/extension-points.md).

## Add an endpoint to the app manifest

1. Open the `stripe-app.yaml` file in your app’s root directory. The file includes an `extensions` key with an `id` that matches the extension ID you chose for the generate command. Inside that object, there’s a `methods` key.
2. Add an `endpoints` key with the type `custom_http` at the same level as `methods`.
3. Provide an `id` and the endpoint `url`.

```yaml
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_sandbox`.

## 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.yaml`. |
| `path` | string | Yes | URL path appended to the endpoint base URL. |
| `method` | - `'GET'`
- `'POST'`
- `'PUT'`
- `'DELETE'`
- `'PATCH'` | 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` | - string
- undefined | Response body as a JSON string. Parse it with `JSON.parse()` to access the data. |

```typescript
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.customInput` contains dynamic data mapped from the workflow trigger event, such as a customer name. You define these fields in `custom_input.schema.json` and reference them in `stripe-app.yaml` 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.schema.json`.

For more details, see [Custom actions](https://docs.stripe.com/workflows/custom-actions.md).

## 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](https://docs.stripe.com/stripe-apps/store-secrets.md) on the account that runs the script. Then add an `auth` key to the app manifest `stripe-app.yaml` that specifies the authorization type and required values.

### Configure token-based authorization

Provide the `secret_name` in the `auth` key.

```yaml
      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_name` and `secret_name` in the `auth` key.

```yaml
      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.

```typescript
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:

```bash
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.

```typescript
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.yaml`. |
| `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.

```typescript
{
  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.

```typescript
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_BAD_REQUEST` |
| 401 | `EXT_UNAUTHORIZED` |
| 403 | `EXT_NOT_ALLOWED` |
| 404 | `EXT_NOT_FOUND` |
| 408, 504 | `EXT_TIMEOUT` |
| 429 | `EXT_RATE_LIMIT` |
| 503 | `EXT_RESOURCE_UNAVAILABLE` |
| Other | `EXT_RUNTIME_ERROR` |

### 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.

```typescript
await withEndpointFetchMock(
  [
    {
      request: { endpoint: 'primary-api' },
      response: { status: 200, body: '{}' },
    },
    {
      request: { endpoint: 'analytics' },
      response: { status: 200, body: '{}' },
      optional: true,
    },
  ],
  async () => {
    await myExtension();
  }
);
```

## See also

- [Create an extension](https://docs.stripe.com/extensions/scripts/build-prorations-extension.md)
- [Extension points](https://docs.stripe.com/extensions/extension-points.md)
- [Store secrets](https://docs.stripe.com/stripe-apps/store-secrets.md)
