Webhooks

Subscribe a URL to an event (in the console, with POST /v1/webhooks as in the example, or through the n8n nodes) and Webkio sends it every such event, the moment it happens.

How a delivery works

An HTTPS POST with a JSON body: the record in data, the site it happened on in project. Deliveries go out within about a minute of the event.

HeaderWhat it holds
X-Webkio-EventThe event, e.g. order.paid.
X-Webkio-DeliveryThis delivery's id. A retry keeps it, so you can tell a retry from a new delivery.
X-Webkio-TimestampUnix seconds when it was sent. Part of the signature.
X-Webkio-Signaturev1= and the hex HMAC-SHA256 of {timestamp}.{raw body}, keyed with the webhook's signing secret.
X-Webkio-Test1 on a test sent from the console. Absent otherwise.
  • Answer with any 2xx within 10 seconds. Do slow work after you answer.
  • Anything else is retried, up to 6 tries over about half an hour. A delivery you accepted is never sent again.
  • The body's id is the same every time the same record is sent (a retry, or a Resend from the console): use it to ignore duplicates.
  • Answer 410 Gone and the webhook removes itself.
  • After 6 failed attempts in a row the account owner is emailed a warning (at most once a day for each webhook). After 30 the webhook is switched off and the owner is emailed again; turn it on again from the console once your endpoint works. A webhook you pause there gets nothing until you resume it, and misses what happens meanwhile.

Verify the signature

Recompute the signature from the raw body and the timestamp header, compare it in constant time, and refuse a timestamp older than five minutes, so a captured delivery cannot be replayed. The secret is shown once, when the webhook is created or its secret is rotated.

// Node.js (Express). Read the RAW body: the signature covers the exact bytes we sent.
const crypto = require('crypto');
const express = require('express');

// secrets: the webhook's signing secret, or two while you change it (current and new).
function verifyWebkio(rawBody, timestamp, signature, secrets) {
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute window
  if (typeof signature !== 'string') return false;
  return [].concat(secrets).some((secret) => {
    const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
    return signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  });
}

// The secret; while you change it, both, separated by a comma.
const secrets = process.env.WEBKIO_WEBHOOK_SECRET.split(',');

const app = express();
app.post('/webkio', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyWebkio(req.body.toString('utf8'), req.get('X-Webkio-Timestamp'), req.get('X-Webkio-Signature'), secrets);
  if (!ok) return res.sendStatus(401);
  const delivery = JSON.parse(req.body);
  // ... handle delivery.event / delivery.data, then answer quickly:
  res.sendStatus(200);
});

Changing the secret without missing a delivery. Rotate signing secret in the console creates a new one while deliveries stay signed with the current one. Set your endpoint to accept both (the examples take them comma-separated), check it with Send test, which signs with the new secret during the change, then choose Finish. A webhook added by Zapier, Make or n8n keeps the secret its integration was given.

Events and example payloads

Each body below is an example. To see one with your own data, use GET /event-types/{event}/samples or Send test in the console.

New order

order.created

An order is placed in your shop. Card payments may still be pending.

{
    "id": "order.created_Ab12Cd",
    "event": "order.created",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "id": "Ab12Cd",
        "number": "ORD-260914-0001",
        "status": "pending",
        "type": "shipping",
        "customer": {
            "name": "Sofia Almeida",
            "email": "sofia@example.com",
            "phone": "+44 20 7946 0958"
        },
        "currency": "GBP",
        "subtotal": 42.5,
        "shipping": 4.95,
        "tip": null,
        "discount": null,
        "tax": 0,
        "total": 47.45,
        "payment_method": "card",
        "shipping_method": "Standard",
        "shipping_address": {
            "line1": "1 High Street",
            "city": "London",
            "postcode": "EC1A 1AA",
            "country": "GB"
        },
        "coupon_code": null,
        "note": null,
        "scheduled_for": null,
        "items": [
            {
                "name": "Linen apron",
                "sku": "APR-01",
                "quantity": 2,
                "price": 21.25,
                "total": 42.5
            }
        ],
        "created_at": "2026-09-14T09:30:00Z"
    }
}

Order paid

order.paid

Payment for an order is confirmed.

{
    "id": "order.paid_Ab12Cd",
    "event": "order.paid",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "status": "paid",
        "id": "Ab12Cd",
        "number": "ORD-260914-0001",
        "type": "shipping",
        "customer": {
            "name": "Sofia Almeida",
            "email": "sofia@example.com",
            "phone": "+44 20 7946 0958"
        },
        "currency": "GBP",
        "subtotal": 42.5,
        "shipping": 4.95,
        "tip": null,
        "discount": null,
        "tax": 0,
        "total": 47.45,
        "payment_method": "card",
        "shipping_method": "Standard",
        "shipping_address": {
            "line1": "1 High Street",
            "city": "London",
            "postcode": "EC1A 1AA",
            "country": "GB"
        },
        "coupon_code": null,
        "note": null,
        "scheduled_for": null,
        "items": [
            {
                "name": "Linen apron",
                "sku": "APR-01",
                "quantity": 2,
                "price": 21.25,
                "total": 42.5
            }
        ],
        "created_at": "2026-09-14T09:30:00Z"
    }
}

New booking

booking.created

A customer books an appointment.

{
    "id": "booking.created_Bk34Ef",
    "event": "booking.created",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "id": "Bk34Ef",
        "service": "Haircut and finish",
        "date": "2026-09-20",
        "time": "14:30",
        "status": "confirmed",
        "customer": {
            "name": "Sofia Almeida",
            "email": "sofia@example.com",
            "phone": "+44 20 7946 0958"
        },
        "amount_due": 45,
        "currency": "GBP",
        "payment_status": "unpaid",
        "address": null,
        "notes": "First visit",
        "created_at": "2026-09-14T09:30:00Z"
    }
}

New rental reservation

rental.reserved

A customer reserves a rental item.

{
    "id": "rental.reserved_Rv56Gh",
    "event": "rental.reserved",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "id": "Rv56Gh",
        "reference": "RSV-1042",
        "item": "Volkswagen Golf",
        "status": "pending",
        "customer": {
            "name": "Sofia Almeida",
            "email": "sofia@example.com",
            "phone": "+44 20 7946 0958"
        },
        "pickup": {
            "date": "2026-09-21",
            "time": "10:00",
            "location": "Airport desk"
        },
        "return": {
            "date": "2026-09-24",
            "time": "10:00",
            "location": "Airport desk"
        },
        "days": 3,
        "total": 180,
        "deposit": 200,
        "currency": "GBP",
        "payment_status": "unpaid",
        "notes": null,
        "created_at": "2026-09-14T09:30:00Z"
    }
}

New subscriber

subscriber.created

Someone joins your email list.

{
    "id": "subscriber.created_Sb78Ij",
    "event": "subscriber.created",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "id": "Sb78Ij",
        "email": "sofia@example.com",
        "name": "Sofia Almeida",
        "status": "active",
        "tags": [
            "newsletter"
        ],
        "created_at": "2026-09-14T09:30:00Z"
    }
}

New property enquiry

property_enquiry.created

A visitor enquires about a property listing.

{
    "id": "property_enquiry.created_Pe90Kl",
    "event": "property_enquiry.created",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "id": "Pe90Kl",
        "type": "viewing",
        "status": "new",
        "customer": {
            "name": "Sofia Almeida",
            "email": "sofia@example.com",
            "phone": "+44 20 7946 0958"
        },
        "message": "Could I view it on Saturday morning?",
        "listing": {
            "id": "Ls11Mn",
            "title": "3 bed terrace, Southville"
        },
        "created_at": "2026-09-14T09:30:00Z"
    }
}

New product review

review.submitted

A customer reviews a product.

{
    "id": "review.submitted_Rw22Op",
    "event": "review.submitted",
    "project": {
        "id": "xYz12AbC",
        "name": "Your Shop",
        "timezone": "Europe/London"
    },
    "data": {
        "id": "Rw22Op",
        "rating": 5,
        "title": "Lovely quality",
        "content": "Washed well and still looks new.",
        "status": "pending",
        "verified_purchase": true,
        "customer": {
            "name": "Sofia Almeida",
            "email": "sofia@example.com"
        },
        "product": {
            "id": "Pr33Qr",
            "name": "Linen apron"
        },
        "created_at": "2026-09-14T09:30:00Z"
    }
}