# My Webhooks

Register HTTPS endpoints to be told when GunSpec data changes, instead of polling for it. Subscribe to events such as `firearm.created` or `firearm.updated` and the changed record is delivered to your server in the same shape the API returns it, so a local mirror can be kept current without a second call. Requires Studio tier or higher.

Source: https://docs.gunspec.io/en/api/me-webhooks

```mermaid
sequenceDiagram
  participant G as GunSpec
  participant Y as Your endpoint
  participant Q as Your queue
  G->>Y: POST with the event, signed
  Y->>Y: Check the signature<br/>over the raw bytes
  Y-->>G: 2xx, before doing the work
  Y->>Q: Hand the event on
  Q->>Q: Seen this X-Webhook-Id before?
  Note over Q: a retry carries the same id,<br/>so handling it twice is safe
```

## List Webhook Endpoints

`GET /v1/me/webhooks`

Auth: API key required  
Tier: Explorer+

Returns the webhook endpoints registered on your account.

### Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `page` | integer | no | Page number, from 1 to 10,000 |
| `per_page` | integer | no | Items per page (max 100) |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | WebhookEndpoint[] | One page of results |
| `data[].id` | string | Opaque endpoint id. Address it at `/v1/me/webhooks/{endpointId}`. |
| `data[].url` | string | Absolute, directly fetchable URL. |
| `data[].events` | string[] | Event types this endpoint is subscribed to. The full list is on the `POST` operation. |
| `data[].description` | string \| null | Prose summary of the record, where one has been written. |
| `data[].active` | boolean | False while the endpoint is paused. A paused endpoint receives nothing and is not retried. |
| `data[].createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data[].updatedAt` | string | When the endpoint was last changed, ISO 8601. |
| `pagination.page` | number | Current page number |
| `pagination.per_page` | number | Items per page |
| `pagination.total` | number | Total matching records (Builder and above) |

```json
{
  "success": true,
  "data": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "url": "https://example.com/hooks/gunspec",
      "events": [
        "firearm.created"
      ],
      "description": null,
      "active": true,
      "createdAt": "2026-08-23T02:00:00.000Z",
      "updatedAt": "2026-08-23T02:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "per_page": 20,
    "total": 42,
    "totalPages": 3
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Requires **Studio** tier or higher.
- Webhook secrets are never returned in list or get responses for security.

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/me/webhooks' \
  --header 'X-API-Key: your_key'
```

## Create Webhook Endpoint

`POST /v1/me/webhooks`

Auth: API key required  
Tier: Explorer+

Registers an HTTPS endpoint to receive event deliveries. The response includes a signing secret; it is shown once and cannot be retrieved later.

### Request body (application/json)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Where deliveries are POSTed. A public https hostname on port 443: no IP address, no credentials in the URL, and not a gunspec.io or workers.dev host. Redirects are not followed, so register the final URL. At most 2,048 characters. |
| `events` | string[] | yes | Event types this endpoint is subscribed to. The full list is on the `POST` operation. |
| `description` | string | no | Prose summary of the record, where one has been written. |

```json
{
  "url": "https://myapp.example.com/webhooks/gunspec",
  "events": ["firearm.created", "firearm.updated", "firearm.deleted"],
  "description": "Sync firearm catalog to our search index"
}
```

### Response 201 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | WebhookEndpointCreated | The response payload |
| `data.id` | string | Opaque endpoint id. Address it at `/v1/me/webhooks/{endpointId}`. |
| `data.url` | string | Absolute, directly fetchable URL. |
| `data.events` | string[] | Event types this endpoint is subscribed to. The full list is on the `POST` operation. |
| `data.description` | string \| null | Prose summary of the record, where one has been written. |
| `data.active` | boolean | False while the endpoint is paused. A paused endpoint receives nothing and is not retried. |
| `data.createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data.updatedAt` | string | When the endpoint was last changed, ISO 8601. |
| `data.secret` | string | HMAC signing secret. Returned only here, on creation. No other endpoint will show it again. |

```json
{
  "success": true,
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "url": "https://example.com/hooks/gunspec",
    "events": [
      "firearm.created"
    ],
    "description": null,
    "active": true,
    "createdAt": "2026-08-23T02:00:00.000Z",
    "updatedAt": "2026-08-23T02:00:00.000Z",
    "secret": "whsec_..."
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 413 | `PAYLOAD_TOO_LARGE` | Request body too large |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Requires **Studio** tier or higher.
- The URL must be HTTPS. Plain HTTP URLs are rejected.
- Supported events: `firearm.created`, `firearm.updated`, `firearm.deleted`, `firearm.variant.updated`, `firearm.source.changed`, `firearm.confidence.changed`, `manufacturer.created`, `manufacturer.updated`, `manufacturer.deleted`, `caliber.created`, `caliber.updated`, `caliber.deleted`, `category.created`, `category.updated`, `category.deleted`, `image.created`, `image.deleted` and `catalog.resynced`.
- `firearm.variant.updated` carries the same payload as `firearm.updated` and is sent only for records that have a parent, so a family can be tracked without taking the whole catalog's traffic. `firearm.source.changed` and `firearm.confidence.changed` say which kind of change it was and arrive alongside `firearm.updated`.
- `catalog.resynced` replaces a flood: when more records change at once than can be described individually, such as a re-derivation of the whole catalog, one event is sent instead of thousands, and the right response is to re-sync rather than to apply per-record updates. It reaches every endpoint subscribed to any event, ticked or not.
- Catalog events carry the record in the same shape `GET /v1/firearms/{id}` returns it, `version` included, so a cache can be updated without a second call. A deletion carries the id alone.
- Deliveries also carry `X-Webhook-Id` (the event, stable across retries, so dedupe on it), `X-Webhook-Event`, and `X-Webhook-Delivery` (this attempt). A non-2xx response is retried three times, 1, 5 and 15 minutes apart.
- The `secret` is shown **only** in this create response. It cannot be retrieved later, so store it immediately. Delete and recreate the endpoint to rotate it.
- Verify deliveries with the `X-Webhook-Signature` header. See the **Verifying deliveries** note on the *Test* endpoint for the exact scheme.

### Example

```bash
curl --request POST \
  --url 'https://api.gunspec.io/v1/me/webhooks' \
  --header 'X-API-Key: your_key' \
  --header 'Content-Type: application/json' \
  --data '
{
  "url": "https://example.com/hooks/gunspec",
  "events": [
    "firearm.created"
  ],
  "description": "Production ingest"
}
'
```

## Get Webhook Endpoint

`GET /v1/me/webhooks/{endpointId}`

Auth: API key required  
Tier: Explorer+

Returns one of your webhook endpoints.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpointId` | string | yes | Webhook endpoint id |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | WebhookEndpoint | The response payload |
| `data.id` | string | Opaque endpoint id. Address it at `/v1/me/webhooks/{endpointId}`. |
| `data.url` | string | Absolute, directly fetchable URL. |
| `data.events` | string[] | Event types this endpoint is subscribed to. The full list is on the `POST` operation. |
| `data.description` | string \| null | Prose summary of the record, where one has been written. |
| `data.active` | boolean | False while the endpoint is paused. A paused endpoint receives nothing and is not retried. |
| `data.createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data.updatedAt` | string | When the endpoint was last changed, ISO 8601. |

```json
{
  "success": true,
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "url": "https://example.com/hooks/gunspec",
    "events": [
      "firearm.created"
    ],
    "description": null,
    "active": true,
    "createdAt": "2026-08-23T02:00:00.000Z",
    "updatedAt": "2026-08-23T02:00:00.000Z"
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Requires **Studio** tier or higher.
- You can only retrieve webhook endpoints registered to your own API key.

### Example

```bash
curl --request GET \
  --url 'https://api.gunspec.io/v1/me/webhooks/7c9e6679-7425-40de-944b-e07fc1f90ae7' \
  --header 'X-API-Key: your_key'
```

## Update Webhook Endpoint

`PUT /v1/me/webhooks/{endpointId}`

Auth: API key required  
Tier: Explorer+

Updates the URL, subscribed events, description, or active state of one of your endpoints.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpointId` | string | yes | Webhook endpoint id |

### Request body (application/json)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | no | Where deliveries are POSTed. A public https hostname on port 443: no IP address, no credentials in the URL, and not a gunspec.io or workers.dev host. Redirects are not followed, so register the final URL. At most 2,048 characters. |
| `events` | string[] | no | Event types this endpoint is subscribed to. The full list is on the `POST` operation. |
| `description` | string | no | Prose summary of the record, where one has been written. |
| `active` | boolean | no | False while the endpoint is paused. A paused endpoint receives nothing and is not retried. |

```json
{
  "events": ["firearm.created", "firearm.updated", "firearm.deleted"],
  "active": false
}
```

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | WebhookEndpoint | The response payload |
| `data.id` | string | Opaque endpoint id. Address it at `/v1/me/webhooks/{endpointId}`. |
| `data.url` | string | Absolute, directly fetchable URL. |
| `data.events` | string[] | Event types this endpoint is subscribed to. The full list is on the `POST` operation. |
| `data.description` | string \| null | Prose summary of the record, where one has been written. |
| `data.active` | boolean | False while the endpoint is paused. A paused endpoint receives nothing and is not retried. |
| `data.createdAt` | string | When the record was first added, `YYYY-MM-DD HH:MM:SS` in UTC. |
| `data.updatedAt` | string | When the endpoint was last changed, ISO 8601. |

```json
{
  "success": true,
  "data": {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "url": "https://example.com/hooks/gunspec",
    "events": [
      "firearm.created"
    ],
    "description": null,
    "active": true,
    "createdAt": "2026-08-23T02:00:00.000Z",
    "updatedAt": "2026-08-23T02:00:00.000Z"
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 413 | `PAYLOAD_TOO_LARGE` | Request body too large |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Requires **Studio** tier or higher.
- Set `active: false` to pause deliveries without deleting the endpoint configuration.
- At least one field (`url`, `events`, or `active`) must be provided.

### Example

```bash
curl --request PUT \
  --url 'https://api.gunspec.io/v1/me/webhooks/7c9e6679-7425-40de-944b-e07fc1f90ae7' \
  --header 'X-API-Key: your_key' \
  --header 'Content-Type: application/json' \
  --data '
{
  "url": "https://example.com/hooks/gunspec",
  "events": [
    "firearm.created"
  ],
  "description": "Production ingest",
  "active": true
}
'
```

## Delete Webhook Endpoint

`DELETE /v1/me/webhooks/{endpointId}`

Auth: API key required  
Tier: Explorer+

Removes a webhook endpoint. Past delivery history is retained.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpointId` | string | yes | Webhook endpoint id |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.deleted` | boolean | Always true: the endpoint is gone once this returns. |
| `data.id` | string | Id of the deleted endpoint. |

```json
{
  "success": true,
  "data": {
    "deleted": true,
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Requires **Studio** tier or higher.
- Deletion is permanent and cannot be undone.
- In-flight webhook deliveries at the time of deletion may still be attempted once.

### Example

```bash
curl --request DELETE \
  --url 'https://api.gunspec.io/v1/me/webhooks/7c9e6679-7425-40de-944b-e07fc1f90ae7' \
  --header 'X-API-Key: your_key'
```

## Test Webhook Endpoint

`POST /v1/me/webhooks/{endpointId}/test`

Auth: API key required  
Tier: Explorer+

Sends a signed test event to the endpoint so you can verify your receiver and signature check before relying on real events.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `endpointId` | string | yes | Webhook endpoint id |

### Response 200 (application/json)

| Field | Type | Description |
| --- | --- | --- |
| `data` | object | The response payload. |
| `data.delivered` | boolean | Whether your endpoint answered with a 2xx. |
| `data.httpStatus` | integer \| null | The status your endpoint returned. Null when the request never got a response: DNS failure, timeout, TLS error. |
| `data.error` | string \| null | Why the calculation could not be completed, or null when it succeeded. |

```json
{
  "success": true,
  "data": {
    "delivered": true,
    "httpStatus": 200,
    "error": null
  }
}
```

### Errors

| Status | Code | Message |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Validation error |
| 401 | `UNAUTHORIZED` | API key required |
| 403 | `FORBIDDEN` | Valid key, not permitted |
| 404 | `NOT_FOUND` | Resource not found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
| 500 | `INTERNAL_ERROR` | Unexpected server error |

### Notes

- Requires **Studio** tier or higher.
- The test payload is a synthetic `test.ping` event and will not affect your application data.
- A `delivered: false` result with status 422 means your server responded but with a non-2xx code.
- Connection timeouts after 10 seconds are reported as delivery failures.
- **Payload envelope**: every delivery (test or real) is a JSON body shaped `{ "id": "evt_...", "type": "firearm.updated", "created_at": "<ISO 8601>", "data": { ... } }`. `data` is the affected record. The request also carries `Content-Type: application/json` and `User-Agent: GunSpec-Webhooks/1.0`.
- **Verifying deliveries**: each request includes an `X-Webhook-Signature: t=<unix_timestamp>,v1=<hex>` header. Recompute `v1` as the lowercase hex `HMAC-SHA256` of the string `t.rawRequestBody` (the timestamp, a literal dot, then the raw body) keyed with your endpoint `secret`, then compare in constant time. Reject the delivery if it does not match, or if `t` is more than ~5 minutes old (replay protection).
- **Always hash the raw request body** before any JSON parsing or re-serialisation, otherwise the signature will not match.
- **Delivery and retries**: a 2xx response marks the delivery successful. Non-2xx responses and timeouts are retried up to 3 times with a `1m`, `5m`, `15m` backoff, after which the delivery is marked failed.

### Example

```bash
curl --request POST \
  --url 'https://api.gunspec.io/v1/me/webhooks/7c9e6679-7425-40de-944b-e07fc1f90ae7/test' \
  --header 'X-API-Key: your_key'
```
