# Webkio API reference

Read a Webkio account's sites, orders and contacts; add contacts, email subscribers and blog post drafts; and subscribe URLs to its events.

**Blank fields**: in a request body, a field you leave out or send empty changes nothing.

**Authentication**: an API key (Settings > API & webhooks, or the developer console) as `Authorization: Bearer wk_...`.

**Times**: every timestamp is ISO 8601 in UTC. The account and each site carry a `timezone` (IANA) for display; booking and reservation dates and times are the site's local ones.

**Lists** are cursor-paginated: `limit` (1-100, default 25) and `cursor` (the previous page's `meta.next_cursor`) until `meta.has_more` is false.

**Errors** are `{"success": false, "error": {"code": "...", "message": "..."}}`. Branch on the code:
- `missing_api_key`: No key was sent.
- `invalid_api_key`: The key is unknown or revoked.
- `console_token_expired`: The developer site's temporary token has expired; reload the explorer.
- `account_unavailable`: The account is deleted, suspended or banned.
- `plan_required`: The account's plan does not include integrations, or the feature the endpoint uses (the CRM, the blog).
- `plan_limit_reached`: A limit of the plan is reached: new subscribers this month, or blog posts on a site.
- `read_only_demo`: The demo account cannot change anything.
- `rate_limited`: Over 200 requests a minute. Retry-After says when to come back.
- `not_found`: No such endpoint.
- `resource_not_found`: No such record on this account.
- `unknown_event`: The event type is not one GET /v1/event-types lists.
- `invalid_target_url`: The webhook URL is not https, not public, or too long.
- `invalid_project`: project_id is not one of this account's sites.
- `invalid_list`: list_id is not one of the site's email lists.
- `email_rejected`: Webkio does not send email to that address: it bounced or complained before, or it is a throwaway address.
- `webhook_limit_reached`: The account already has 50 webhooks.
- `invalid_cursor`: The cursor was not one this API gave out.
- `validation_failed`: The request was understood but not accepted; the message says why.
- `server_error`: Something failed on our side.

200 requests a minute per account. Guide: https://webkio.com/docs/api-and-webhooks

Base URL: `https://api.webkio.com/v1`

## Account

Who the key belongs to, and their sites.

### GET /v1/me

**The account the key belongs to** Integrations call this to test a connection.

Answers:

- `200` The account.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/projects

**The account's sites**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Sites, oldest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/openapi.json

**This description**

No key needed.

Answers:

- `200` The OpenAPI document.

## Webhooks

Event types, sample data, and subscriptions.

### GET /v1/event-types

**The events a webhook can subscribe to** The whole list; it is short and not paginated.

Answers:

- `200` Every event type.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/event-types/{type}/samples

**Newest records for an event type** Your own newest records, in exactly the shape a webhook delivers them.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `type` | path | one of order.created, order.paid, booking.created, rental.reserved, subscriber.created, property_enquiry.created, review.submitted | yes |  |
| `project_id` | query | string | no | Only this site. |
| `limit` | query | integer | no |  |

Answers:

- `200` Newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/webhooks

**The account's webhooks**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### POST /v1/webhooks

**Subscribe a URL to an event** The URL must be https and public. Subscribing the same URL to the same event again returns the existing webhook. The signing secret is returned only here.

| Body field | Type | Required | Description |
|---|---|---|---|
| `event` | one of order.created, order.paid, booking.created, rental.reserved, subscriber.created, property_enquiry.created, review.submitted | yes |  |
| `target_url` | string (uri) | yes |  |
| `project_id` | string | no | Only this site. Leave out for every site. |

Answers:

- `201` The webhook, with its secret.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/webhooks/{id}

**One webhook**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` The webhook (without its secret).
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### DELETE /v1/webhooks/{id}

**Remove a webhook**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` Removed.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Contacts

The CRM's people, matched on email within a site. Plans with the CRM.

### GET /v1/contacts

**Find contacts** Newest first. Pass `email` to find one person.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `project_id` | query | string | no | Only this site. |
| `email` | query | string (email) | no | Only the contact with this email. Not case-sensitive. |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Contacts, newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### POST /v1/contacts

**Create or update a contact** Matched on `email` within the site: an email the site does not know creates a contact (201), a known one updates it (200). `name`, `phone` and `notes` replace what is stored; `tags` are added to the contact's own.

| Body field | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | The site, from GET /v1/projects. |
| `email` | string (email) | yes | What the contact is matched on. |
| `name` | string | no |  |
| `phone` | string | no |  |
| `notes` | string | no | Replaces the contact's notes. |
| `tags` | list of string or string | no | A list, or one text with commas between the tags. |

Answers:

- `200` The updated contact.
- `201` The new contact.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/contacts/{id}

**One contact**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` The contact.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Email

Email subscribers and lists.

### POST /v1/subscribers

**Add an email subscriber** Adds the address to the site's subscribers, active at once, and to `list_id` when sent. Only send people who agreed to hear from the site. Someone already subscribed is not added twice (200): they join the list, gain the tags and keep their status, so an address that unsubscribed stays unsubscribed. A new subscriber (201) counts toward the plan's monthly limit and starts the site's welcome automation.

| Body field | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | The site, from GET /v1/projects. |
| `email` | string (email) | yes |  |
| `name` | string | no |  |
| `tags` | list of string or string | no | A list, or one text with commas between the tags. |
| `list_id` | string | no | Also add them to this list, from GET /v1/email-lists. |

Answers:

- `200` Already subscribed.
- `201` The new subscriber.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/email-lists

**Email lists**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `project_id` | query | string | no | Only this site. |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Lists, oldest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Blog

Blog post drafts, to publish from the dashboard.

### POST /v1/blog-posts

**Create a blog post draft** Always a draft: it is published from the dashboard, where it is checked before it goes live. The slug is made from the title unless you send one, and kept unique on the site. `content` is HTML; scripts and unsafe markup are removed.

| Body field | Type | Required | Description |
|---|---|---|---|
| `project_id` | string | yes | The site, from GET /v1/projects. |
| `title` | string | yes |  |
| `content` | string | no | HTML. Scripts and unsafe markup are removed. |
| `excerpt` | string | no |  |
| `slug` | string | no | Made from the title when left out. |
| `tags` | list of string or string | no | A list, or one text with commas between the tags. |
| `meta_title` | string | no |  |
| `meta_description` | string | no |  |
| `featured_image` | string (uri) | no |  |

Answers:

- `201` The draft.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Orders

Orders, read only.

### GET /v1/orders

**Find orders** Newest first. Each order reads as the order webhooks send it.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `project_id` | query | string | no | Only this site. |
| `number` | query | string | no | The order number, exactly. |
| `email` | query | string (email) | no | The customer's email. Not case-sensitive. |
| `status` | query | one of pending, paid, processing, shipped, delivered, refunded, cancelled | no |  |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Orders, newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/orders/{id}

**One order**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` The order.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### PATCH /v1/orders/{id}

**Move an order's status on** Moves the order and does what goes with it: tracking details are saved, shipping is timestamped, a cancellation puts the stock back, and the customer is emailed - the same as a change made in the dashboard. An order is marked **paid** by the payment flow and **refunded** by the refund action, so neither is settable here.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

| Body field | Type | Required | Description |
|---|---|---|---|
| `status` | one of processing, shipped, delivered, cancelled | yes |  |
| `tracking_number` | string | no | Saved against the order; usually sent with `shipped`. |
| `tracking_url` | string (uri) | no |  |

Answers:

- `200` The updated order.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Products


### GET /v1/products

**Find products** Newest first. Includes products that have been switched off, so a catalogue sync can withdraw them too - filter with `active` if you only want the ones on sale. Stock lives on the variant for anything sold by size or colour, so read `variants[].stock` when `variants` is not empty.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `project_id` | query | string | no | Only this site. |
| `sku` | query | string | no | The SKU, exactly. How you find the product behind an order line. |
| `type` | query | one of physical, digital, service, bundle | no |  |
| `category_id` | query | string | no |  |
| `active` | query | boolean | no | Omit for both. |
| `search` | query | string | no | Matches name or SKU. |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Products, newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/products/{id}

**One product**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` The product.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### PUT /v1/products/{id}/stock

**Set what is in stock** States the real count rather than adjusting by a difference, because every caller retries: a repeated `-3` destroys the figure, a repeated `= 42` is simply true again. Pass `variant_id` for anything sold by size or colour. The owner is warned when a product drops INTO its low-stock window, and not again while it stays there, so a nightly sync does not email them nightly. Refused on a product that does not track stock.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

| Body field | Type | Required | Description |
|---|---|---|---|
| `quantity` | integer | yes | Whole number, zero or more. |
| `variant_id` | string | no | Required for a product with variants, since stock lives there. |

Answers:

- `200` The updated product.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Bookings


### GET /v1/bookings

**Find bookings** Newest first. Each booking reads as the booking.created webhook sends it. from/to filter the APPOINTMENT date, not when it was made.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `project_id` | query | string | no | Only this site. |
| `email` | query | string (email) | no | The customer's email. Not case-sensitive. |
| `status` | query | one of pending, confirmed, cancelled, completed, no-show | no |  |
| `from` | query | string (date) | no | Appointments on or after this date (YYYY-MM-DD). |
| `to` | query | string (date) | no | Appointments on or before this date (YYYY-MM-DD). |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Bookings, newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/bookings/{id}

**One booking**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` The booking.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### PATCH /v1/bookings/{id}

**Change a booking's status** Moves the booking and does everything that goes with it: a cancellation refunds a card payment where the policy allows, and the customer is emailed - the same as a change made in the dashboard.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

| Body field | Type | Required | Description |
|---|---|---|---|
| `status` | one of pending, confirmed, cancelled, completed, no-show | yes |  |

Answers:

- `200` The updated booking.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Reservations


### GET /v1/reservations

**Find rental reservations** Newest first. Each reservation reads as the rental.reserved webhook sends it. from/to filter the PICKUP date, not when the reservation was taken.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `project_id` | query | string | no | Only this site. |
| `reference` | query | string | no | The reservation reference, exactly. |
| `email` | query | string (email) | no | The customer's email. Not case-sensitive. |
| `status` | query | one of pending, confirmed, cancelled, completed | no |  |
| `from` | query | string (date) | no | Pick-ups on or after this date (YYYY-MM-DD). |
| `to` | query | string (date) | no | Pick-ups on or before this date (YYYY-MM-DD). |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no | The previous page's meta.next_cursor. |

Answers:

- `200` Reservations, newest first.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### GET /v1/reservations/{id}

**One reservation**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

Answers:

- `200` The reservation.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

### PATCH /v1/reservations/{id}

**Change a reservation's status** Moves the reservation and does everything that goes with it: a cancellation refunds a card payment where the policy allows, releases the deposit hold, and emails the customer - the same as a change made in the dashboard.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes |  |

| Body field | Type | Required | Description |
|---|---|---|---|
| `status` | one of pending, confirmed, cancelled, completed | yes |  |

Answers:

- `200` The updated reservation.
- `401` missing_api_key, invalid_api_key or console_token_expired.
- `403` account_unavailable, plan_required, plan_limit_reached or read_only_demo.
- `404` resource_not_found, unknown_event or not_found.
- `422` unknown_event, invalid_target_url, invalid_project, invalid_list, email_rejected, webhook_limit_reached, invalid_cursor or validation_failed.
- `429` rate_limited: over 200 requests a minute. Retry-After says when to come back.

## Webhook events

Sent to a URL subscribed with `POST /v1/webhooks`. Payloads, headers and the signature check are in the webhooks document.

- `order.created`: An order is placed in your shop. Card payments may still be pending. Sent to every URL subscribed to `order.created`, signed with that subscription's secret.
- `order.paid`: Payment for an order is confirmed. Sent to every URL subscribed to `order.paid`, signed with that subscription's secret.
- `booking.created`: A customer books an appointment. Sent to every URL subscribed to `booking.created`, signed with that subscription's secret.
- `rental.reserved`: A customer reserves a rental item. Sent to every URL subscribed to `rental.reserved`, signed with that subscription's secret.
- `subscriber.created`: Someone joins your email list. Sent to every URL subscribed to `subscriber.created`, signed with that subscription's secret.
- `property_enquiry.created`: A visitor enquires about a property listing. Sent to every URL subscribed to `property_enquiry.created`, signed with that subscription's secret.
- `review.submitted`: A customer reviews a product. Sent to every URL subscribed to `review.submitted`, signed with that subscription's secret.
