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

# Webhooks & Events

Receive real-time notifications when resources change in anny. Register a webhook subscription, and the platform will POST event payloads to your URL.

For general request conventions, see [JSON-API Conventions](https://docs.anny.co/developers/guides/json-api-conventions.md). For authentication, see [Authentication](https://docs.anny.co/developers/guides/authentication.md).

***

## Prerequisites

* A valid Bearer token with admin access
* An HTTPS endpoint capable of receiving POST requests
* Organization context (`?o={organization_id}`)

***

**Creating Webhooks via the Admin Dashboard**

The easiest way to set up webhooks is through the anny admin dashboard:

1. Navigate to **Organization Settings → API → Webhooks**
2. Click **Create Webhook**
3. Enter your HTTPS endpoint URL, select the events to subscribe to, and save

For a step-by-step walkthrough with screenshots, see the [help center article on webhooks](https://docs.anny.co/en/articles/349740-webhooks).

**Creating a Webhook Subscription via API**

Request path documentation: `paths/admin/webhooks.yaml`

```http theme={null}
POST /api/v1/webhook-subscriptions?o={org_id}

{
  "data": {
    "type": "webhook-subscriptions",
    "attributes": {
      "name": "My Integration",
      "url": "https://example.com/webhooks/anny",
      "events": ["bookings.created", "bookings.updated", "orders.created"]
    }
  }
}
```

### Attributes

| Attribute | Type | Required | Description |
| - | - | - | - |
| `name` | string | Yes | Descriptive label for the subscription |
| `url` | string | Yes | HTTPS endpoint to receive webhook POSTs |
| `events` | string\[] | Yes | Array of event identifiers to subscribe to |
| `custom_headers` | object | No | Additional headers sent with each webhook request |
| `is_active` | boolean | No | Defaults to `true`. Set to `false` to pause delivery. |

### Response

The response includes a `signing_key` — store this securely. It is auto-generated (64-character random string) and used to verify payload signatures.

```json theme={null}
{
  "data": {
    "type": "webhook-subscriptions",
    "id": "ws-uuid",
    "attributes": {
      "name": "My Integration",
      "url": "https://example.com/webhooks/anny",
      "events": ["bookings.created", "bookings.updated", "orders.created"],
      "signing_key": "a1b2c3...64chars",
      "is_active": true,
      "failure_count": 0,
      "created_at": "2026-03-25T10:00:00+00:00"
    }
  }
}
```

## Event Catalog

> **Bookings**
>
> | Event | Triggered when |
> | - | - |
> | `bookings.created` | A new booking is created |
> | `bookings.confirmed` | Status changes from `requested` to `accepted` |
> | `bookings.canceled` | Status changes to `canceled`, except from `reserved` |
> | `bookings.updated` | Booking attributes change (status, dates, etc.) |
> | `bookings.started` | Booking start time is reached |
> | `bookings.ended` | Booking end time is reached |
> | `bookings.checked-in` | Customer checks in |
> | `bookings.checked-out` | Customer checks out |
> | `bookings.deleted` | Booking is permanently deleted |
>
> `bookings.confirmed` and `bookings.canceled` are sent in addition to `bookings.updated`. Blockers do not send them. Dropping a cart reservation (`reserved` to `canceled`) does not send `bookings.canceled`.
>
> The admin event picker does not list these two events yet. Subscribe to them through the API. The API accepts every value of the webhook event list, including these.

> **Orders**
>
> | Event | Triggered when |
> | - | - |
> | `orders.created` | Checkout is completed and order is placed |
> | `orders.updated` | Order attributes change |
> | `orders.deleted` | Order is permanently deleted |

> **Invoices**
>
> | Event | Triggered when |
> | - | - |
> | `invoices.created` | Invoice is generated |
> | `invoices.finalised` | Invoice is locked and assigned a number |
> | `invoices.updated` | Invoice attributes change |
> | `invoices.deleted` | Invoice is deleted |

> **Customers**
>
> | Event | Triggered when |
> | - | - |
> | `customers.created` | Customer record is created |
> | `customers.updated` | Customer data changes |
> | `customers.deleted` | Customer is deleted |

> **Subscriptions (Plans)**
>
> | Event | Triggered when |
> | - | - |
> | `subscriptions.created` | Subscription is created |
> | `subscriptions.started` | Subscription period begins |
> | `subscriptions.updated` | Subscription attributes change |
> | `subscriptions.renewed` | Subscription renews for a new period |
> | `subscriptions.charged` | Subscription payment is processed |
> | `subscriptions.suspended` | Subscription is paused |
> | `subscriptions.canceled` | Subscription is cancelled |
> | `subscriptions.reactivated` | Suspended subscription resumes |
> | `subscriptions.ended` | Subscription expires |

> **Timeslots**
>
> | Event | Triggered when |
> | - | - |
> | `timeslots.created` | Timeslot is created |
> | `timeslots.updated` | Timeslot changes |
> | `timeslots.started` | Timeslot start time is reached |
> | `timeslots.ended` | Timeslot end time is reached |
> | `timeslots.deleted` | Timeslot is deleted |

> **Communities**
>
> | Event | Triggered when |
> | - | - |
> | `community-accounts.joined` | Member joins a community |
> | `community-accounts.left` | Member leaves a community |

***

## Payload Structure

Every webhook delivery is a POST request with `Content-Type: application/json`. The payload uses a **normalized flat JSON** format — not JSON-API.

```json theme={null}
{
  "event": "bookings.updated",
  "event_id": "evt-a1b2c3d4-...",
  "webhook_id": "ws-uuid",
  "triggered_at": "2026-03-25T10:00:00+00:00",
  "data": {
    "id": "b1",
    "type": "bookings",
    "start_date": "2026-04-01T10:00:00+02:00",
    "end_date": "2026-04-01T11:00:00+02:00",
    "status": "accepted",
    "resource": {
      "id": "r1",
      "type": "resources",
      "name": "Meeting Room A",
      "slug": "meeting-room-a"
    },
    "service": {
      "id": "s1",
      "type": "services",
      "name": "Standard Hourly"
    },
    "customer": {
      "id": "c1",
      "type": "customers",
      "first_name": "Jane",
      "last_name": "Doe"
    }
  }
}
```

### Key differences from JSON-API

| | JSON-API (management endpoints) | Webhook payload |
| - | - | - |
| Attributes | Nested under `data.attributes` | Flat on `data` top level |
| Relationships | Under `data.relationships` as ID references | Nested as full objects under their name |
| Side-loaded | `included` array | Not used — relations are inline |
| Content-Type | `application/vnd.api+json` | `application/json` |

### Included relations per entity

| Entity | Included relations |
| - | - |
| Bookings | `resource`, `resource.parent`, `service`, `customer`, `customer.address` |
| Orders | `customer`, `bookings` |
| Invoices | `customer`, `order` |
| Customers | `address` |
| Subscriptions | `customer`, `plan` |
| Timeslots | `resource`, `service` |
| Community Accounts | `customer`, `community` |

> [!NOTE]
>
> Included relations and fieldsets may vary by event. The table above shows typical inclusions.

***

## Signature Verification

Every webhook request includes a `Signature` header containing an HMAC SHA-256 hash of the request body, signed with your subscription's `signing_key`.

### Verifying the Signature

```text theme={null}
received_signature = request.headers["Signature"]
expected_signature = HMAC-SHA256(signing_key, request.body)

if received_signature != expected_signature:
    reject request (return 401)
```

> [!WARNING]
>
> Always verify the signature before processing the payload. This prevents spoofed requests.

***

## Retry Policy

If your endpoint doesn't respond with a `2xx` status code within **10 seconds**, the delivery is retried:

| Attempt | Timing |
| - | - |
| 1st | Immediate |
| 2nd | Exponential backoff |
| 3rd | Exponential backoff |

After **3 failed attempts** for a single event, that delivery is abandoned.

***

## Auto-Deactivation

If deliveries fail repeatedly across events, the subscription's `failure_count` increments. After **5 consecutive failures**, the subscription is automatically deactivated:

* `is_active` is set to `false`
* `failure_count` resets to `0`
* Organization admins receive a notification about the deactivation

To reactivate, update the subscription:

```http theme={null}
PATCH /api/v1/webhook-subscriptions/{id}?o={org_id}

{
  "data": {
    "type": "webhook-subscriptions",
    "id": "{id}",
    "attributes": {
      "is_active": true
    }
  }
}
```

***

## Managing Subscriptions

### List All Subscriptions

```http theme={null}
GET /api/v1/webhook-subscriptions?o={org_id}
```

### Update Events

```http theme={null}
PATCH /api/v1/webhook-subscriptions/{id}?o={org_id}

{
  "data": {
    "type": "webhook-subscriptions",
    "id": "{id}",
    "attributes": {
      "events": ["bookings.created", "bookings.updated", "customers.created"]
    }
  }
}
```

### Delete a Subscription

```http theme={null}
DELETE /api/v1/webhook-subscriptions/{id}?o={org_id}
```

***

## Best Practices

1. **Respond quickly** — return `200` immediately and process the payload asynchronously. The 10-second timeout is strict.
2. **Handle duplicates** — use `event_id` to deduplicate. The same event may be delivered more than once on retries.
3. **Verify signatures** — always validate the `Signature` header before trusting the payload.
4. **Subscribe selectively** — only subscribe to events you actually process to minimize traffic.
5. **Monitor `failure_count`** — periodically check subscription status to catch issues before auto-deactivation.

***

## Normalized JSON via Accept Header

Webhook management endpoints (and all other API `GET` endpoints) also support `Accept: application/json` to receive the same flat/normalized format used in webhook payloads.

```http theme={null}
GET /api/v1/webhook-subscriptions?o={org_id}
Accept: application/json
```

This returns attributes at the top level instead of nested under `data.attributes`, matching the webhook payload structure.

> `POST` and `PATCH` requests must still use `Content-Type: application/vnd.api+json` with standard JSON-API format.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.