# Webhooks

Apiera webhooks deliver real-time HTTP notifications when events occur in your organization. Instead of polling for
changes, your application receives a POST request for each event, enabling responsive integrations that react to
changes as they happen.

## Design philosophy

Apiera uses a **granular, signal-based** webhook architecture. Every operation across the API surface fires a
corresponding event. Not just top-level resource changes, but sub-resource operations too (e.g.
`product.attribute.linked`, `product.input_value.updated`).

**Granular events** let integrators know *what kind* of change occurred, not just *that something changed*. A coarse
`product.updated` event forces consumers to either fetch-and-diff or maintain complex routing logic. Granular events
allow selective subscriptions and clear intent.

**Thin payloads** contain only navigational properties: the UUIDs needed to construct the exact REST API call to fetch
current state. Delta payloads (before/after values) are deliberately avoided. While the infrastructure can provide
near-ordered delivery in many cases, delta payloads require *guaranteed* ordering to be safe. Out-of-order delivery of
before/after values can corrupt consumer state. Consumers always fetch current state from the API, which is
architecturally sound regardless of delivery order.

## How it works

1. **Subscribe** - Create a webhook subscription with a target URL and select which event types to receive.
2. **Receive** - When an event occurs, Apiera sends an HTTP POST to your endpoint with a signed JSON payload.
3. **Verify** - Validate the HMAC-SHA256 signature to confirm the request is authentic.
4. **Fetch** - Use the UUIDs in the payload to call the corresponding REST endpoint for current state.


## Setting up a subscription

### 1. Create a subscription


```bash
curl -X POST https://api.apiera.io/v1/webhook-subscriptions \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "My Integration",
    "targetUrl": "https://example.com/webhooks"
  }'
```

The response includes a `secret` field (format: `whsec_<base64>`). **Store this secret securely.** It is only returned
on creation and when explicitly rotated. You need it to verify incoming webhook signatures.


```json
{
  "uuid": "a1b2c3d4-...",
  "status": "active",
  "displayName": "My Integration",
  "targetUrl": "https://example.com/webhooks",
  "requestTimeoutMs": 10000,
  "secret": "whsec_dGhpcyBpcyBhIHNlY3JldA==",
  "createdAt": "2026-03-10T12:00:00Z",
  "updatedAt": "2026-03-10T12:00:00Z"
}
```

### 2. Subscribe to event types

List available event types, then subscribe to the ones you need:


```bash
# List all available event types
curl https://api.apiera.io/v1/webhook-event-types \
  -H "Authorization: Bearer {token}"

# Subscribe to an event type
curl -X POST https://api.apiera.io/v1/webhook-subscriptions/{subscriptionUuid}/event-types \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookEventTypeUuid": "event-type-uuid-here"
  }'
```

You only receive events for event types you have explicitly subscribed to.

### 3. Manage your subscription

| Action | Method | Endpoint |
|  --- | --- | --- |
| List subscriptions | GET | `/v1/webhook-subscriptions` |
| Get subscription | GET | `/v1/webhook-subscriptions/{uuid}` |
| Update subscription | PATCH | `/v1/webhook-subscriptions/{uuid}` |
| Delete subscription | DELETE | `/v1/webhook-subscriptions/{uuid}` |
| Activate / Deactivate | PATCH | `/v1/webhook-subscriptions/{uuid}/actions/lifecycle` |
| Rotate secret | PATCH | `/v1/webhook-subscriptions/{uuid}/actions/rotate-secret` |
| List subscribed event types | GET | `/v1/webhook-subscriptions/{uuid}/event-types` |
| Subscribe to event type | POST | `/v1/webhook-subscriptions/{uuid}/event-types` |
| Unsubscribe from event type | DELETE | `/v1/webhook-subscriptions/{uuid}/event-types/{eventTypeUuid}` |


### Subscription lifecycle

Subscriptions have three states:

| Status | Description |
|  --- | --- |
| **active** | Receives and delivers events normally. |
| **inactive** | Paused. No events are created or delivered. |
| **suspended** | Temporarily disabled by the system (e.g. persistent delivery failures). |


Transition between states using the lifecycle endpoint:


```bash
curl -X PATCH https://api.apiera.io/v1/webhook-subscriptions/{uuid}/actions/lifecycle \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{ "transition": "deactivate" }'
```

## Security

### Signature verification

Every webhook delivery is signed with your subscription's HMAC secret. Always verify the signature before processing
the payload.

Each request includes three headers:

| Header | Description |
|  --- | --- |
| `webhook-id` | Unique delivery identifier (UUID). |
| `webhook-timestamp` | Unix timestamp (seconds) when the webhook was sent. |
| `webhook-signature` | HMAC-SHA256 signature in the format `v1,<base64>`. |


The signature is computed over the string:


```
{webhook-id}.{webhook-timestamp}.{payload}
```

Where `{payload}` is the raw JSON request body.

**Verification steps:**

1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers.
2. Construct the signed content: `{webhook-id}.{webhook-timestamp}.{payload}`.
3. Compute HMAC-SHA256 using your secret (Base64-decode the secret after removing the `whsec_` prefix).
4. Compare the computed signature with the `webhook-signature` header value.
5. Optionally, reject requests where the timestamp is older than 5 minutes to prevent replay attacks.


See [Examples](/webhooks/examples) for complete verification code in multiple languages.

### Rotating secrets

If your secret is compromised, rotate it immediately:


```bash
curl -X PATCH https://api.apiera.io/v1/webhook-subscriptions/{uuid}/actions/rotate-secret \
  -H "Authorization: Bearer {token}"
```

The response contains the new `secret`. Update your verification code before the next delivery.

## Delivery headers

Every webhook POST request includes these headers:

| Header | Description |
|  --- | --- |
| `Content-Type` | Always `application/json; charset=utf-8`. |
| `webhook-id` | Unique delivery UUID. |
| `webhook-timestamp` | Unix timestamp (seconds). |
| `webhook-signature` | HMAC-SHA256 signature (`v1,<base64>`). |
| `X-Webhook-Event-Type` | The event type string (e.g. `product.created`). |
| `Deprecation` | *(Optional)* RFC 7234 date if the event type is deprecated. |
| `Sunset` | *(Optional)* RFC 7234 date when the event type will be removed. |


## Payload structure

All payloads are thin. They contain only the UUIDs needed to fetch current state via the REST API.


```json
{
  "productUuid": "550e8400-e29b-41d4-a716-446655440000"
}
```

The payload structure varies by event type but always maps directly to REST API path parameters. See [Events](/webhooks/events)
for details on event types and their payloads.

## Monitoring deliveries

Track delivery status and debug failures using the delivery endpoints:


```bash
# List deliveries for a subscription
curl https://api.apiera.io/v1/webhook-subscriptions/{uuid}/deliveries \
  -H "Authorization: Bearer {token}"

# Get a specific delivery
curl https://api.apiera.io/v1/webhook-subscriptions/{uuid}/deliveries/{deliveryUuid} \
  -H "Authorization: Bearer {token}"

# List delivery attempts
curl https://api.apiera.io/v1/webhook-subscriptions/{uuid}/deliveries/{deliveryUuid}/attempts \
  -H "Authorization: Bearer {token}"
```

Each delivery tracks:

| Field | Description |
|  --- | --- |
| `status` | `pending`, `delivered`, or `failed`. |
| `attempts` | Number of delivery attempts made. |
| `lastStatusCode` | HTTP status code from the most recent attempt. |
| `scheduledAt` | When the next retry is scheduled (if pending). |


Each attempt records the HTTP status code, response body (truncated to 2048 characters), and response time in
milliseconds.