> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mapping.travel/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook subscriptions

> Register HTTPS endpoints to receive signed, retried webhook deliveries for mapping events. Replaces the legacy /api/v1/export-webhooks endpoint.

The webhook subscriptions API lets each organization register one or more HTTPS endpoints that receive signed, retried webhook deliveries for mapping events. This is the API behind the **Developers → Webhooks** page in the app.

For a conceptual overview see [Webhooks Overview](/webhooks/overview). For signature verification see [Verifying signatures](/webhooks/verifying-signatures). For delivery semantics see [Retries and delivery](/webhooks/delivery).

<Note>
  All endpoints require a bearer token in the `Authorization` header. The two new event types (`MAPPING_ERROR_RESOLVED`, `INVENTORY_DUPLICATE_RESOLVED`) require a Pro or above plan.
</Note>

***

## POST /api/v1/webhook-subscriptions

Create a subscription. Returns the freshly generated signing secret **exactly once** — store it before responding.

### Request body

<ParamField body="name" type="string">
  Human-readable label.
</ParamField>

<ParamField body="webhookUrl" type="string" required>
  HTTPS endpoint that will receive deliveries. Must use `https://`, must resolve to a non-private IP outside of dev.
</ParamField>

<ParamField body="events" type="string[]" required>
  Array of event types this subscription should receive. One or more of:

  * `MAPPING_COMPLETED` — all plans
  * `MAPPING_ERROR_RESOLVED` — Pro and above
  * `INVENTORY_DUPLICATE_RESOLVED` — Pro and above
</ParamField>

<ParamField body="apiKeyName" type="string">
  Header name attached to every request (e.g., `X-API-Key`).
</ParamField>

<ParamField body="apiKeyValue" type="string">
  Value for `apiKeyName`. Stored encrypted at rest.
</ParamField>

<ParamField body="additionalHeaders" type="object">
  Extra headers as a key-value map.
</ParamField>

<ParamField body="status" type="string" default="ACTIVE">
  `ACTIVE` or `INACTIVE`. Inactive subscriptions don't receive deliveries.
</ParamField>

### Response

<ResponseField name="id" type="string">
  Subscription UUID.
</ResponseField>

<ResponseField name="signingSecret" type="string">
  **Only returned on this initial response.** Store it now; we won't show it again. Used for HMAC verification — see [Verifying signatures](/webhooks/verifying-signatures).
</ResponseField>

<ResponseField name="signingSecretMasked" type="string">
  Masked form for UI display (e.g., `whsec_••••5d4d`). Returned on every read.
</ResponseField>

<ResponseField name="events" type="string[]">
  Echoed event subscriptions.
</ResponseField>

<ResponseField name="status" type="string">
  `ACTIVE` or `INACTIVE`.
</ResponseField>

<ResponseField name="deactivatedReason" type="string">
  `PLAN_DOWNGRADE` if auto-disabled by a plan downgrade. Otherwise null.
</ResponseField>

### Example

```bash theme={null} theme={null}
curl -X POST https://api.mapping.travel/api/v1/webhook-subscriptions \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Inventory ops",
    "webhookUrl": "https://yourapp.com/webhooks/mapping",
    "events": ["MAPPING_ERROR_RESOLVED", "INVENTORY_DUPLICATE_RESOLVED"],
    "status": "ACTIVE"
  }'
```

```json theme={null} theme={null}
{
  "id": "sub_…",
  "organizationId": "org_…",
  "name": "Inventory ops",
  "webhookUrl": "https://yourapp.com/webhooks/mapping",
  "events": ["MAPPING_ERROR_RESOLVED", "INVENTORY_DUPLICATE_RESOLVED"],
  "status": "ACTIVE",
  "deactivatedReason": null,
  "signingSecret": "whsec_e1c5d3a0…",
  "signingSecretMasked": "whsec_••••3a0e",
  "createdAt": "2026-06-02T00:00:00Z",
  "updatedAt": "2026-06-02T00:00:00Z"
}
```

***

## GET /api/v1/webhook-subscriptions

List subscriptions for the organization. `signingSecret` is **never** returned — only `signingSecretMasked`.

<ParamField query="activeOnly" type="boolean" default="false">
  When `true`, only `ACTIVE` subscriptions are returned.
</ParamField>

```bash theme={null} theme={null}
curl "https://api.mapping.travel/api/v1/webhook-subscriptions?activeOnly=true" \
  -H "Authorization: Bearer <token>"
```

***

## GET /api/v1/webhook-subscriptions/{id}

Retrieve one subscription. `signingSecret` is masked.

***

## PUT /api/v1/webhook-subscriptions/{id}

Replace the subscription. Same body schema as create. Returns `403` if any event in `events` is not permitted for the org's current plan.

***

## DELETE /api/v1/webhook-subscriptions/{id}

Permanently delete the subscription and its delivery history. Returns `204 No Content`.

***

## POST /api/v1/webhook-subscriptions/{id}/rotate-secret

Generate a new signing secret and invalidate the old one. Returns the new secret **once**.

```bash theme={null} theme={null}
curl -X POST https://api.mapping.travel/api/v1/webhook-subscriptions/<id>/rotate-secret \
  -H "Authorization: Bearer <token>"
```

```json theme={null} theme={null}
{
  "id": "sub_…",
  "signingSecret": "whsec_<new value>"
}
```

***

## GET /api/v1/webhook-subscriptions/{id}/deliveries

Recent delivery attempts for inspection and debugging.

<ParamField query="limit" type="integer" default="50">
  Max rows. Capped at 200.
</ParamField>

### Response

<ResponseField name="deliveries" type="object[]">
  <Expandable title="delivery properties">
    <ResponseField name="id" type="string">Delivery (attempt) UUID.</ResponseField>
    <ResponseField name="eventId" type="string">Logical event UUID — stable across retries; use as dedup key.</ResponseField>
    <ResponseField name="eventType" type="string">Event wire name.</ResponseField>
    <ResponseField name="status" type="string">`PENDING` | `SUCCESS` | `FAILED` | `DEAD`</ResponseField>
    <ResponseField name="attemptCount" type="integer">Number of attempts made so far.</ResponseField>
    <ResponseField name="lastResponseCode" type="integer">HTTP status of last attempt, if any.</ResponseField>
    <ResponseField name="lastError" type="string">Truncated error message of last failure.</ResponseField>
    <ResponseField name="nextAttemptAt" type="string">ISO timestamp of next scheduled retry.</ResponseField>
    <ResponseField name="createdAt" type="string">ISO timestamp the event was enqueued.</ResponseField>
    <ResponseField name="deliveredAt" type="string">ISO timestamp of the successful delivery, if any.</ResponseField>
  </Expandable>
</ResponseField>

***

## POST /api/v1/webhook-subscriptions/{id}/test

Enqueue a synthetic `webhook.test` event for the subscription. Useful to confirm your endpoint receives and verifies signed payloads end-to-end.

```bash theme={null} theme={null}
curl -X POST https://api.mapping.travel/api/v1/webhook-subscriptions/<id>/test \
  -H "Authorization: Bearer <token>"
```

```json theme={null} theme={null}
{ "deliveryId": "del_…" }
```

The synthetic event flows through the same scheduler + retry pipeline as real events. Check `/deliveries` for the result within \~30 seconds.

***

## Error codes

| Status | Code         | When                                                                                           |
| ------ | ------------ | ---------------------------------------------------------------------------------------------- |
| 400    | BAD\_REQUEST | Invalid URL, no events, quota exceeded, etc.                                                   |
| 401    | UNAUTHORIZED | Missing or invalid bearer token.                                                               |
| 403    | FORBIDDEN    | At least one requested event requires a plan upgrade. Body includes the offending event names. |
| 404    | NOT\_FOUND   | Subscription doesn't exist or belongs to a different org.                                      |

***

## Operational guards

* **Quota:** 10 active subscriptions per organization.
* **URL safety:** `https://` only; loopback and RFC 1918 hosts are rejected outside dev.
* **Payload cap:** 64 KB. Larger payloads get `{ truncated: true, dataRef }` and the data is queryable via API.
* **Backpressure:** when `PENDING` deliveries for one org exceeds 10,000, new events for that org are dropped silently and an hourly audit entry records the drop.
