# Manage Stripe webhook

```yaml
capability_id: stripe.webhook.write
display_name: Manage Stripe webhook
ui_capability_key: manage_stripe_webhook
scope: stripe:webhook:write
endpoint:
  method: POST
  path: /stripe/webhook/setup
operation: stripe.webhook.setup
billable: true
environments:
  - production
  - staging
  - development
```

## Purpose and authorization

`stripe.webhook.write` authorizes an authenticated Gateman application to
configure one supported Stripe webhook destination for its own application.
The application supplies the callback URL and Stripe event subscriptions;
Gateman uses the application's configured Stripe credentials to perform the
supported endpoint setup.

This capability requires the `stripe:webhook:write` scope and is deliberately
narrower than `stripe:use`. It does not grant native Stripe API relay access,
general Stripe resource management, or access to another application's
resources.

The callback URL belongs to the receiving application. Stripe sends webhook
deliveries directly to that URL. Gateman does not receive, relay, process,
deduplicate, or verify those deliveries. The receiving application must
validate Stripe signatures and process events itself. An application that
needs the corresponding signing secret can use the separate
[`stripe.webhook.read`](https://docs.getgateman.com/capabilities/stripe.webhook.read/)
capability when granted its independent scope.

## Implementation contract

The request is an authenticated Contract v1 request:

```http
POST /stripe/webhook/setup
Content-Type: application/json
X-GM-App: app_example
X-GM-Ts: 1791748800
X-GM-Sig: <Contract-v1-signature>
X-GM-Idempotency-Key: optional-gateman-key
```

`X-GM-Idempotency-Key` is optional. If supplied, it is part of the signed
Contract v1 request material; it is not a Gateman replay or billing ledger for
this capability.

The JSON body accepts exactly these three fields:

```json
{
  "environment": "production",
  "callback_url": "https://example.com/webhooks/stripe",
  "events": [
    "checkout.session.completed",
    "payment_intent.succeeded"
  ]
}
```

Unknown fields are rejected. The endpoint does not accept an arbitrary Stripe
endpoint ID, a submitted signing secret, a Stripe account selector, or other
provider-management parameters.

### `environment`

The public environment label is required and is trimmed and lowercased. It
must be one of `production`, `staging`, or `development`.

| Public label | Stripe mode | Stripe API credential | Webhook secret slot |
| --- | --- | --- | --- |
| `production` | Live | `STRIPE_SECRET_KEY` | `STRIPE_WEBHOOK_SECRET` |
| `staging` | Test | `STRIPE_SECRET_KEY_TEST` | `STRIPE_WEBHOOK_SECRET_TEST` |
| `development` | Test | `STRIPE_SECRET_KEY_TEST` | `STRIPE_WEBHOOK_SECRET_TEST` |

`staging` and `development` are interchangeable public labels for the same
logical Gateman test environment. They share one tracked Stripe test endpoint
association and do not provision independent test endpoints. The public label
is retained in the successful response; it does not change the underlying
Stripe live/test mode.

Staging and development are real Stripe test-mode operations, not local
simulation or capture modes.

### `callback_url`

`callback_url` must be a string that:

- is no longer than 2,048 bytes;
- has no leading or trailing whitespace;
- contains no ASCII whitespace or control character, including DEL;
- parses as a URL with an `https` scheme;
- has a nonempty valid hostname; and
- does not contain embedded username or password credentials.

Query strings and fragments are accepted by this validation. Gateman does not
verify DNS, public reachability, private-network status, or whether the
application actually handles the callback.

### `events`

`events` must be a nonempty JSON array. Each element must be a string that is
nonempty, no longer than 255 bytes, and contains no ASCII whitespace or
control character. Duplicate event names are removed and the remaining names
are sorted lexicographically before configuration and in the success response.

The wildcard event name `*` is accepted by Gateman's request validation.
Stripe remains responsible for accepting or rejecting the selected event
configuration and for defining supported event names.

## Authentication and authorization

The application must use [Contract
v1](https://docs.getgateman.com/contract-v1/) and be granted exactly the
capability needed for this operation: `stripe:webhook:write`.

Write does not require `stripe:webhook:read`, and Read does not require Write.
Neither scope grants `stripe:use`. Use the [Official PHP
client](https://docs.getgateman.com/canonical-client-php/), [Official Node.js
client](https://docs.getgateman.com/canonical-client-node/), [Official Python
client](https://docs.getgateman.com/canonical-client-python/), or [Official Go
client](https://docs.getgateman.com/canonical-client-go/), or implement the
same Contract v1 signing rules directly.

The application must sign the exact JSON bytes sent in the request body. Do
not replace the Contract v1 headers with a bearer token or an `Authorization`
header of the application's own.

## Setup and reconciliation behaviour

Gateman exposes one supported live association and one supported test
association per application. It does not provide general-purpose listing,
adoption, deletion, rotation, or multi-endpoint management.

### No tracked endpoint

Gateman creates a Stripe webhook endpoint with the supplied callback URL and
canonical event list. Stripe returns a signing secret on creation; Gateman
stores that secret in the selected live or test webhook-secret slot.

### Existing tracked endpoint

Gateman checks the previously tracked endpoint. If its callback URL or event
subscriptions differ, Gateman updates that endpoint. An ordinary update does
not replace the existing stored signing secret.

If the tracked endpoint already matches the requested callback URL and events,
Gateman may complete without modifying Stripe. The request remains an admitted
`stripe.webhook.setup` operation.

If Stripe reports that the previously tracked endpoint no longer exists,
Gateman creates a replacement endpoint and stores the newly returned signing
secret in the selected slot. The previous stored value is therefore replaced
for that live/test mode.

Alternating between `staging` and `development` does not by itself make an
otherwise valid test endpoint appear unconfigured or create a second test
endpoint.

## Manual webhook configuration

Customers may create a Stripe webhook endpoint directly in Stripe and store
its signing secret in Gateman. Manual storage requires only the secret value;
it does not require an endpoint URL or endpoint ID. The supported names are:

- `STRIPE_WEBHOOK_SECRET` for live/production;
- `STRIPE_WEBHOOK_SECRET_TEST` for test/staging/development.

A manually stored secret can exist without a Gateman-tracked endpoint. Write
does not automatically adopt a manually created Stripe endpoint based on its
URL, and Gateman cannot identify an endpoint from a secret value alone.

When Write creates an endpoint, the Stripe-issued secret overwrites the
corresponding stored slot. When Write updates an existing endpoint or finds
that its configuration is unchanged, the existing stored secret is preserved.
Applications do not need Write in order to use the separate Read capability.

## Success response

A successful setup returns HTTP `200`:

```json
{
  "ok": true,
  "environment": "production",
  "endpoint_id": "we_example",
  "callback_url": "https://example.com/webhooks/stripe",
  "events": [
    "checkout.session.completed",
    "payment_intent.succeeded"
  ],
  "configured": true
}
```

The fields mean:

| Field | Meaning |
| --- | --- |
| `ok` | The setup operation completed successfully. |
| `environment` | The accepted, normalized public environment label supplied by the caller. |
| `endpoint_id` | The Stripe webhook endpoint configured by this association. |
| `callback_url` | The callback URL configured at Stripe. |
| `events` | The deduplicated, sorted event subscriptions configured at Stripe. |
| `configured` | `true` indicates successful setup. |

The response does not include the signing secret. Retrieve it separately with
`POST /stripe/webhook-secret` using
[`stripe.webhook.read`](https://docs.getgateman.com/capabilities/stripe.webhook.read/)
when the application has `stripe:webhook:read`.

## Stripe credentials and provider boundary

The application must have the appropriate Stripe secret API credential
configured in Gateman:

- `STRIPE_SECRET_KEY` for `production` live-mode requests;
- `STRIPE_SECRET_KEY_TEST` for `staging` and `development` test-mode requests.

Gateman contacts the real Stripe API with the selected credential. A missing
Stripe API credential, Stripe rejection, or Stripe transport failure is
returned as the generic provisioning-unavailable error described below. A
Stripe Connect account is not selected or configured by this capability.

Gateman's supported endpoint configuration is not a general Stripe webhook
management service. Stripe remains authoritative for provider permissions,
endpoint state, and event support.

## Idempotency and repeated calls

Three mechanisms must be kept separate:

1. `X-GM-Idempotency-Key` is an optional outer Contract v1 header. It is
   signed, but Write does not maintain a replay ledger or use it to make an
   invocation free.
2. Endpoint creation uses Stripe provider-side idempotency for the supported
   creation request. This is distinct from a Gateman result ledger.
3. Gateman reconciliation checks the tracked endpoint and compares its
   callback URL and event list. An unchanged admitted request can make no
   Stripe mutation, while a changed request updates the tracked endpoint.

Repeated admitted setup calls consume separate Gateman operations. The outer
idempotency header does not guarantee an unbilled replay or exactly-once
creation across arbitrary failures. Do not blindly retry an uncertain
provider mutation.

## Operation accounting, limits and concurrency

The operation is billable as `stripe.webhook.setup`. Each admitted Write invocation consumes one operation, including unchanged configurations and invocations that subsequently fail. Requests rejected before operation admission are not billed. Monthly allowance, concurrency, and accounting availability can prevent admission. Provider and transport errors after admission are not free.

The public retry behavior is:

- Gateman provider-concurrency rejection: HTTP `429` with `Retry-After: 1`;
- provider-concurrency guard unavailable: HTTP `503`;
- setup contention after admission: HTTP `503` with `Retry-After: 1`;
- monthly operation allowance exhausted: HTTP `403` with the shared allowance
  fields;
- accounting unavailable: HTTP `503`.

These Gateman admission responses are distinct from a Stripe HTTP `429`. See
[Operation accounting](https://docs.getgateman.com/operation-accounting/) and
[Rate limits and concurrency](https://docs.getgateman.com/rate-limits-and-concurrency/)
for shared rules.

## Errors and responses

Unless noted otherwise, errors are JSON objects with `ok: false` and an
`error` string. Shared Contract v1 authentication errors and application
authorization errors use the status and response contract documented in
[Authentication](https://docs.getgateman.com/authentication/) and [Errors and
responses](https://docs.getgateman.com/errors-and-responses/).

| Condition | HTTP response | Public body or handling |
| --- | ---: | --- |
| Invalid JSON or a non-object JSON body | `400` | `{"ok":false,"error":"Invalid JSON"}` |
| Unknown request field | `400` | `{"ok":false,"error":"This endpoint accepts only environment, callback_url and events"}` |
| Missing or unsupported environment | `400` | `ok: false` with the validation error, such as `Application environment is required` or `Unsupported application environment`. |
| Invalid callback URL | `400` | `ok: false` with `Invalid callback URL` or `Callback URL must be a valid HTTPS URL`. |
| Invalid event selection | `400` | `ok: false` with the event validation error. |
| Contract v1 authentication failure | `401` | Shared authentication response; no setup operation is admitted. |
| Missing `stripe:webhook:write` authorization | `401` or `403` | Shared authorization response; no setup operation is admitted. |
| Gateman provider-concurrency limit | `429` | `{"ok":false,"error":"Too many concurrent provider requests"}` with `Retry-After: 1`. |
| Provider-concurrency guard unavailable | `503` | `{"ok":false,"error":"Provider concurrency guard unavailable"}`. |
| Monthly operation allowance exhausted | `403` | Shared `monthly_operation_limit_reached` response; no operation is admitted. |
| Operation accounting unavailable | `503` | `{"ok":false,"error":"Operation accounting unavailable"}`. |
| Setup already in progress for the same app/mode | `503` | `{"ok":false,"error":"Stripe webhook setup is already in progress"}` with `Retry-After: 1`. The admitted operation is finalized as failed. |
| Stripe credential missing, Stripe API rejection, transport failure, missing required stored secret, conflicting tracked state, or local persistence failure | `502` | `{"ok":false,"error":"Stripe webhook setup unavailable"}`. Provider error bodies and storage diagnostics are not passed through. |

Do not confuse the Gateman-generated provider-concurrency `429` with a
provider-side Stripe `429`; provider failures are returned through the generic
setup-unavailable contract. A `502` after provider execution is not proof that
Stripe did not act, so applications must avoid blind retries and reconcile
according to their provider and application policy.

## Retrieving and using the signing secret

Use the separate `stripe.webhook.read` capability at
`POST /stripe/webhook-secret` to retrieve the current application-scoped
signing secret. Read requires `stripe:webhook:read`; Write does not imply it.
See the [Stripe webhook Read reference](https://docs.getgateman.com/capabilities/stripe.webhook.read/)
for that response and its secret-handling rules.

The receiving application, not Gateman, validates Stripe's signature on the
raw webhook request, applies replay and event-deduplication controls, and
processes payment or business state. Keep the signing secret out of source,
logs, browser code, URLs, and user-visible errors.

## Implementation outcome

A correctly integrated application authenticates with Contract v1, has
`stripe:webhook:write`, and calls Write only when it needs to establish or
reconcile its configured Stripe callback and event subscriptions. It knows the
returned live or test Stripe endpoint identifier and callback URL, can obtain
the current signing secret independently if granted Read, and verifies Stripe
events in its own webhook handler. A later Write call can reconcile supported
callback or event changes without assuming that Gateman receives or processes
the resulting deliveries.

## Implementation instructions for agents

When implementing an integration:

1. Use Contract v1 and grant only `stripe:webhook:write` for endpoint setup;
   grant `stripe:webhook:read` separately when secret retrieval is required.
2. Send only `environment`, `callback_url`, and `events` to
   `POST /stripe/webhook/setup`, using `production` for Stripe live mode and
   either `staging` or `development` for the shared Stripe test mode.
3. Use an HTTPS callback owned by the receiving application and a nonempty
   list of Stripe event names. Do not send an endpoint ID, signing secret, or
   unsupported Stripe management parameter.
4. Sign the exact request bytes with Contract v1. Do not treat
   `X-GM-Idempotency-Key` as a replay ledger or provider idempotency guarantee.
5. Write may create, update, leave unchanged, or replace the tracked endpoint.
   A successful response confirms the resulting endpoint configuration; do
   not infer which action occurred from `configured: true`, and do not invent
   an action or status response field.
6. Protect the signing secret. Retrieve it through Read and verify Stripe
   signatures in the application's webhook receiver; Gateman does not receive
   or verify those deliveries.
7. Account for one `stripe.webhook.setup` operation per admitted invocation,
   including unchanged configurations and admitted failures. Follow
   documented `Retry-After` behavior and do not blindly retry uncertain
   provider mutations.
8. Initiate configuration when required by the application. Do not call Write
   during every checkout, webhook delivery, page load, or routine request.

## Reusable AGENTS.md block

```markdown
<!-- gateman:begin capability=stripe.webhook.write -->
# Configure a Stripe webhook with Gateman

- Capability: `stripe.webhook.write`
- Scope: `stripe:webhook:write`
- Reference:
  https://docs.getgateman.com/capabilities/stripe.webhook.write/
- Endpoint: `POST /stripe/webhook/setup`
- Operation: `stripe.webhook.setup` (one billable operation per admitted
  invocation)
- Required JSON fields: `environment`, `callback_url`, and `events`; unknown
  fields are rejected.
- `production` uses Stripe live credentials; `staging` and `development` are
  interchangeable labels for one Stripe test-mode association.
- Use an HTTPS callback owned by the receiving application and a nonempty list
  of Stripe event names. Do not submit an endpoint ID, signing secret, or
  unsupported provider parameters.
- Write creates or reconciles the supported tracked endpoint. Creation stores
  and overwrites the selected live/test signing-secret slot; ordinary update
  and unchanged reconciliation preserve the existing stored secret.
- Write does not adopt arbitrary Stripe endpoints, manage multiple endpoints,
  or imply `stripe:webhook:read`, `stripe:use`, or any webhook delivery
  processing.
- Grant `stripe:webhook:read` separately when the application needs
  `POST /stripe/webhook-secret`; protect the returned secret and verify Stripe
  signatures in the application's own webhook receiver.
- Sign the exact request bytes with Contract v1. `X-GM-Idempotency-Key` is not
  a replay ledger or guaranteed unbilled retry. Follow documented
  `Retry-After` responses, account for admitted failures, and do not blindly
  retry uncertain provider mutations.
- Initiate setup only when configuration is required, not during every
  checkout, page load, webhook delivery, or routine request.
<!-- gateman:end capability=stripe.webhook.write -->
```

## Related references

- [Contract v1](https://docs.getgateman.com/contract-v1/)
- [Authentication](https://docs.getgateman.com/authentication/)
- [Environments](https://docs.getgateman.com/environments/)
- [Use Stripe native relay](https://docs.getgateman.com/capabilities/stripe.use/)
- [Read Stripe webhook secret](https://docs.getgateman.com/capabilities/stripe.webhook.read/)
- [Operation accounting](https://docs.getgateman.com/operation-accounting/)
- [Rate limits and concurrency](https://docs.getgateman.com/rate-limits-and-concurrency/)
- [Idempotency](https://docs.getgateman.com/idempotency/)
- [Errors and responses](https://docs.getgateman.com/errors-and-responses/)
- [Webhooks and event handling](https://docs.getgateman.com/webhooks-and-event-handling/)
- [GetGateman for WordPress](https://docs.getgateman.com/wordpress/)
- [Official PHP client](https://docs.getgateman.com/canonical-client-php/)
- [Official Node.js client](https://docs.getgateman.com/canonical-client-node/)
- [Official Python client](https://docs.getgateman.com/canonical-client-python/)
- [Official Go client](https://docs.getgateman.com/canonical-client-go/)
