Skip to main content

The Webkio API and webhooks

9 min read Updated Sep 14, 2026

Connect your Webkio account to Zapier, Make, n8n or your own code: have new orders, bookings and reservations sent to a URL the moment they happen, and let other tools add contacts, email subscribers and blog post drafts, or look up your orders.

The API and webhooks are part of every plan with integrations (Starter and up).

For developers, developer.webkio.com has the interactive API reference - try any endpoint with your own account, no key needed - and the developer console for your keys, webhooks, delivery log and recent requests. You are signed in there with your Webkio login.

Create an API key

Go to Settings > API & webhooks, or the developer console, and create a key. Name it after where you will use it ("Zapier", "Order sync script"), so you know which one to revoke later.

The key is shown once. Webkio keeps only a fingerprint of it, so if you lose it, revoke it and create another. A key acts as you, with access to all your sites: keep it out of shared documents and code repositories. Only the account owner can create keys.

Revoking a key stops it at once, and removes the webhooks it created.

Making requests

Base URL: https://api.webkio.com/v1. Every endpoint is also in the API reference, with its parameters and responses.

Send the key in the Authorization header:

curl https://api.webkio.com/v1/me \
  -H "Authorization: Bearer wk_your_key_here"

Responses are JSON: {"success": true, "data": ...} when it worked. Each account can make up to 200 requests a minute.

Method Path What it does
GET /v1/me The account the key belongs to, with its timezone. Integrations use it to test the connection.
GET /v1/projects Your sites, with their ids and timezones.
GET /v1/event-types The events a webhook can subscribe to.
GET /v1/event-types/{type}/samples Your newest records for an event type, in exactly the shape a webhook delivers. Optional project_id, and limit up to 10.
GET /v1/webhooks Your webhooks.
POST /v1/webhooks Subscribe a URL to an event.
GET /v1/webhooks/{id} One webhook.
DELETE /v1/webhooks/{id} Remove a webhook.
GET /v1/contacts Find contacts, on one site or by email.
POST /v1/contacts Create a contact, or update the one with the same email.
GET /v1/contacts/{id} One contact.
POST /v1/subscribers Add someone to a site's email subscribers, and to a list.
GET /v1/email-lists Your email lists.
POST /v1/blog-posts Create a blog post draft.
GET /v1/orders Find orders by number, email, status or site.
GET /v1/orders/{id} One order.
GET /v1/openapi.json This API in OpenAPI 3.1, for your own tools. No key needed.

Times and time zones

Every timestamp is ISO 8601 in UTC, for example 2026-09-14T09:30:00Z, so it sorts and compares the same for everyone. To show a local time, convert it: /v1/me gives your account's timezone, and each site (in /v1/projects and in every webhook's project) gives its own, such as Europe/London. The date and time a customer picked for a booking or a rental are already in the site's local time.

Pages

Lists come a page at a time. Ask for up to 100 with limit (25 if you leave it out). The response's meta.has_more says whether there is more; if there is, pass meta.next_cursor as cursor to get the next page.

Errors

A request that did not work answers with a 4xx or 5xx status and:

{"success": false, "error": {"code": "invalid_target_url", "message": "Webhook URLs must start with https://"}}

Use code in your code: it never changes. message is for people and may be reworded.

Code Meaning
missing_api_key, invalid_api_key No key, or one that is unknown or revoked.
account_unavailable, plan_required The account is suspended, or its plan does not include integrations (or the CRM, for contacts).
plan_limit_reached A limit of your plan is reached: new subscribers this month, or blog posts on a site.
rate_limited Over 200 requests a minute. Retry-After says when to come back.
not_found, resource_not_found No such endpoint, or no such record on your account.
unknown_event, invalid_target_url, invalid_project Something in a new webhook is not right; the message says what.
invalid_list The list_id is not one of that 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.
validation_failed A field is missing or not right; the message says which.
webhook_limit_reached Your account already has 50 webhooks.
invalid_cursor The cursor was not one the API gave out.

Adding and finding things

In a request body, a field you leave out or send empty changes nothing. project_id is one of the ids from /v1/projects.

Contacts

POST /v1/contacts creates a contact, or updates the one with the same email on that site. Your site's own orders, bookings and forms add to the same contacts, matched on email.

curl https://api.webkio.com/v1/contacts \
  -H "Authorization: Bearer wk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "2bsxS7dB", "email": "[protected]", "name": "Sofia Almeida", "tags": ["wholesale"]}'

It answers 201 with a new contact and 200 with an updated one. name, phone and notes replace what is stored; tags are added to the contact's own and never removed. Find someone with GET [protected]. Contacts are part of the CRM, so these need a plan that includes it.

Email subscribers

POST /v1/subscribers with project_id and email, and optionally name, tags and a list_id from GET /v1/email-lists, adds someone to a site's email subscribers. They are subscribed at once, as when you add someone in the dashboard, so only send people who agreed to hear from you.

Someone already subscribed is not added twice: they join the list, get the tags and keep their status, so an address that unsubscribed stays unsubscribed. A new subscriber counts toward your plan's monthly limit and starts your welcome automation, if you have one.

Blog post drafts

POST /v1/blog-posts with project_id and title, and optionally content (HTML), excerpt, tags, slug, meta_title, meta_description and featured_image (a URL), creates a draft. Nothing is published through the API: open the post in the dashboard to check it and publish it.

Orders

GET /v1/orders finds orders, newest first, by number, customer email, status or project_id. Each order reads exactly as the order.created and order.paid webhooks send it.

Events

Event Sent when
order.created An order is placed. Card payments may still be pending.
order.paid Payment for an order is confirmed.
booking.created A customer books an appointment.
rental.reserved A customer reserves a rental item.
subscriber.created Someone joins your email list.
property_enquiry.created A visitor enquires about a property.
review.submitted A customer reviews a product.

Webhooks

Add one in Settings > API & webhooks, or through the API:

curl https://api.webkio.com/v1/webhooks \
  -H "Authorization: Bearer wk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"event": "order.paid", "target_url": "https://example.com/webkio"}'

Add "project_id" to hear about one site only; leave it out to hear about every site on your account. The response includes the webhook's signing secret, shown once.

The URL must start with https:// and point to a public address. Registering the same URL for the same event again returns the existing webhook rather than a second one.

What a delivery looks like

Webkio sends a POST with a JSON body:

{
  "id": "order.paid_1NAVdi4x",
  "event": "order.paid",
  "project": {"id": "2bsxS7dB", "name": "Your Shop", "timezone": "Europe/London"},
  "data": {
    "id": "1NAVdi4x",
    "number": "ORD-260914-0001",
    "status": "paid",
    "customer": {"name": "Sofia Almeida", "email": "[protected]", "phone": null},
    "currency": "GBP",
    "total": 42.5,
    "items": [{"name": "Linen apron", "sku": null, "quantity": 2, "price": 21.25, "total": 42.5}],
    "created_at": "2026-09-14T09:30:00Z"
  }
}

The top-level id is the same every time the same record is sent, so you can use it to skip one you have already handled. A delivery describes the record as it is when it is sent. Call GET /v1/event-types/{type}/samples to see every field of an event with your own data.

Each request carries these headers:

Header Contains
X-Webkio-Event The event, for example order.paid.
X-Webkio-Delivery A unique id for this delivery.
X-Webkio-Timestamp When it was sent, in Unix seconds.
X-Webkio-Signature v1= and then the HMAC-SHA256, in hex, of {timestamp}.{body} made with your signing secret.

Checking the signature

Work out the signature yourself and compare. Refuse a request whose timestamp is more than five minutes old, so a captured request cannot be sent again later. Sign the raw body, before you parse it.

// Node.js
const crypto = require('crypto');

function isFromWebkio(rawBody, headers, secret) {
  const ts = headers['x-webkio-timestamp'];
  const sent = headers['x-webkio-signature'] || '';
  const expected = 'v1=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && sent.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected));
}
// PHP
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$valid = hash_equals($expected, $signature) && abs(time() - (int) $timestamp) < 300;

Answering, retries and switching off

  • Answer with any 2xx status within 10 seconds, and do slow work after you have replied.
  • Anything else, or no answer, is tried again five more times over about half an hour.
  • Answer 410 Gone to unsubscribe: the webhook is removed.
  • A webhook that keeps failing is switched off, and shows as Off in Settings. Fix the URL, then add it again.

Deliveries are sent within about five minutes of the event.

Was this helpful?