Build on Linesteward

Read calls, messages and bookings, book times, and get events at your own address, with an API key.

Updated 2026-10-28

Getting started

The API lets your own software read your calls, messages and bookings, make and cancel bookings, keep your business details and facts up to date, and get events at your own web address. Every account can use it.

  1. Make a key. Sign in to the app as an owner or admin, open Settings, then Developers, and press New API key. Give it a name (for example "Website bookings") and only the scopes it needs.
  2. Copy the key. It starts with ls_live_ and is shown once. We keep only a fingerprint of it, so we can't show it again: if you lose it, revoke it and make a new one.
  3. Make your first request. List your businesses:
curlcurl "https://linesteward.com/v1/businesses" -H "Authorization: Bearer $API_KEY"

The answer is JSON: {"data": [{"id": "...", "name": "...", ...}]}. Every address in this guide starts with https://linesteward.com/v1.

Authentication

Send the key in the Authorization header of every request: Authorization: Bearer ls_live_.... Use HTTPS only. A key belongs to your account: it can reach every business in it, and nothing else. Another account's ids answer 404, as if they did not exist.

  • Keep keys on your server. Never put one in a web page, a mobile app or a public code repository.
  • Give each tool its own key, with the fewest scopes it needs, so you can revoke one without stopping the others.
  • In Settings, Developers you can see when each key was last used and its last requests, and revoke it at once.

Scopes

Each key has one or more scopes. A request without the scope it needs gets 403 insufficient_scope. The reference shows the scope of every endpoint.

ScopeLets the key
calls:readRead calls: times, outcome, summary, the caller's number and, when asked for, the transcript.
messages:readRead the messages the receptionist took.
bookings:readRead bookings.
bookings:writeMake and cancel bookings.
businesses:readRead the account's businesses: details, opening hours, greeting.
businesses:writeChange a business's details, hours and greeting, and add a business (within the plan).
knowledge:writeReplace the facts the receptionist answers from, and upload documents.
webhooks:writeRead, set and delete the webhook each business sends its events to.

Rate limits

Each key may make 60 requests a minute and 10,000 a day (the day starts at midnight UTC). Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds). Over the limit you get 429 rate_limited with a Retry-After header: wait that many seconds. Many requests with a wrong key from one address are blocked for 10 minutes; a valid key is never blocked.

Instead of asking for new calls every few seconds, use a webhook.

Pagination

Lists (calls, messages, bookings) are newest first. Ask for up to 100 with limit (default 25). When there is more, the answer has next_cursor; pass it as ?cursor= with the same filters for the next page. On the last page next_cursor is null.

curlcurl "https://linesteward.com/v1/messages?status=open&limit=100&cursor=NEXT_CURSOR" -H "Authorization: Bearer $API_KEY"

Errors

Every error has the same shape, with a code your software can check and a message a person can read:

json{"error": {"code": "not_found", "message": "No such booking."}}
StatusCodeWhen
400invalid_requestA value was not accepted, or a cursor or time is not valid.
401unauthorizedNo key, or the key is wrong or revoked.
403insufficient_scopeThe key lacks the scope this endpoint needs.
403 / 409plan_limitYour plan has no room for another business.
404not_foundNot found, or not in your account (we never say which).
409slot_unavailableThat booking time is not free; free_times lists the nearest free times.
409skill_off booking_system_connected not_supported too_late_to_cancel line_not_readyThe business is not set up for this; the message says what to do.
409idempotency_in_progressA request with the same Idempotency-Key is still running.
409secret_already_issuedA retry of Set a webhook: its signing secret was shown once and is not kept. Set the webhook again with a new Idempotency-Key.
413payload_too_largeThe document is bigger than the limit.
422validation_errorThe body or query does not match the reference.
422idempotency_key_reusedThe Idempotency-Key was used with a different request.
429rate_limitedToo many requests: wait the Retry-After seconds.
503unavailableSomething on our side is not answering: try again soon.

Safe retries

Send an Idempotency-Key header (any unique string, a UUID is best) with a POST. If the network fails and you send the same request again with the same key within 24 hours, you get the first answer back (with Idempotent-Replayed: true) instead of a second booking. The same key with a different body gets 422 idempotency_key_reused. We keep the first answer for those 24 hours only, and never a webhook's signing secret: a retry of POST /v1/webhooks gets 409 secret_already_issued. PATCH, PUT and DELETE are safe to repeat as they are.

Common tasks

Today's calls

curlcurl "https://linesteward.com/v1/calls?since=2026-11-04T00:00:00Z&limit=50" \
  -H "Authorization: Bearer $API_KEY"
JavaScriptconst response = await fetch("https://linesteward.com/v1/calls?since=2026-11-04T00:00:00Z&limit=50", {
  headers: { Authorization: `Bearer ${process.env.API_KEY}` },
});
const { data, next_cursor } = await response.json();
for (const call of data) console.log(call.started_at, call.outcome, call.summary);
Pythonimport os
import requests

response = requests.get(
    "https://linesteward.com/v1/calls",
    params={"since": "2026-11-04T00:00:00Z", "limit": 50},
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    timeout=10,
)
response.raise_for_status()
for call in response.json()["data"]:
    print(call["started_at"], call["outcome"], call["summary"])

One call with its transcript

Transcripts hold your callers' personal data, so they come only with ?include=transcript.

curlcurl "https://linesteward.com/v1/calls/CALL_ID?include=transcript" \
  -H "Authorization: Bearer $API_KEY"
JavaScriptconst call = await (await fetch(`https://linesteward.com/v1/calls/${callId}?include=transcript`, {
  headers: { Authorization: `Bearer ${process.env.API_KEY}` },
})).json();
Pythoncall = requests.get(
    f"https://linesteward.com/v1/calls/{call_id}",
    params={"include": "transcript"},
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    timeout=10,
).json()

Make a booking

The API books with the same rules the receptionist uses on calls: the booking skill must be on, the time must be open, on the booking grid, after the notice and within the days ahead, the service must be one you list, and there must be room. If the time is not free you get 409 slot_unavailable with the nearest free times that day in free_times. No text message is sent to the customer. Businesses that book into a connected booking system, and trades job visits, can't be booked through the API yet.

curlcurl -X POST "https://linesteward.com/v1/bookings" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"business_id": "BUSINESS_ID", "service": "Women\u0027s cut and finish",
       "starts_at": "2026-11-04T10:30:00+00:00", "customer_name": "Sam Patel",
       "customer_phone": "+447700900123"}'
JavaScriptconst response = await fetch("https://linesteward.com/v1/bookings", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    business_id: businessId,
    service: "Women's cut and finish",
    starts_at: "2026-11-04T10:30:00+00:00",
    customer_name: "Sam Patel",
    customer_phone: "+447700900123",
  }),
});
if (response.status === 409) console.log((await response.json()).error.free_times);
Pythonimport uuid

response = requests.post(
    "https://linesteward.com/v1/bookings",
    json={
        "business_id": business_id,
        "service": "Women's cut and finish",
        "starts_at": "2026-11-04T10:30:00+00:00",
        "customer_name": "Sam Patel",
        "customer_phone": "+447700900123",
    },
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}", "Idempotency-Key": str(uuid.uuid4())},
    timeout=10,
)
booking = response.json()

Change opening hours

curlcurl -X PATCH "https://linesteward.com/v1/businesses/BUSINESS_ID" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hours": {"mon": [["09:00", "17:30"]], "sat": [["10:00", "14:00"]]}, "closures": ["2026-12-25"]}'
JavaScriptawait fetch(`https://linesteward.com/v1/businesses/${businessId}`, {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ hours: { mon: [["09:00", "17:30"]], sat: [["10:00", "14:00"]] }, closures: ["2026-12-25"] }),
});
Pythonrequests.patch(
    f"https://linesteward.com/v1/businesses/{business_id}",
    json={"hours": {"mon": [["09:00", "17:30"]], "sat": [["10:00", "14:00"]]}, "closures": ["2026-12-25"]},
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    timeout=10,
)

Get events at your address

curlcurl -X POST "https://linesteward.com/v1/webhooks" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"business_id": "BUSINESS_ID", "url": "https://example.com/hooks/receptionist",
       "events": ["call.completed", "booking.created"]}'
JavaScriptconst hook = await (await fetch("https://linesteward.com/v1/webhooks", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ business_id: businessId, url: "https://example.com/hooks/receptionist" }),
})).json();
// keep hook.signing_secret somewhere safe: it is shown once
Pythonhook = requests.post(
    "https://linesteward.com/v1/webhooks",
    json={"business_id": business_id, "url": "https://example.com/hooks/receptionist"},
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
    timeout=10,
).json()
# keep hook["signing_secret"] somewhere safe: it is shown once

Webhooks

Each business can send its events to one public HTTPS address (not localhost, an IP address or a private network: those get 400): the same one you can set in the app under Settings, "Send to other apps" (for Zapier, Make or n8n). Setting it through the API replaces it there too. We send each event as a POST with a JSON body, and try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours if your address does not answer with a 2xx status. After 50 failures in a row it is switched off; set it again to switch it back on.

EventWhen
call.completedA call ended: its id, business, caller's number, start, seconds, outcome, summary, language.
message.createdThe receptionist took a message: who, their number, the message, urgent or not.
booking.createdA booking was made, on a call, in the app or through the API.
booking.cancelledA booking was cancelled.
json{
  "event": "booking.created",
  "created_at": "2026-11-04T09:12:44.512+00:00",
  "data": {
    "id": "8d2f0a54-6f0e-4b3a-9a43-2c1f7e0b9d10",
    "ref": "K7MQ2P",
    "kind": "appointment",
    "status": "confirmed",
    "service": "Women's cut and finish",
    "starts_at": "2026-11-04T10:30:00+00:00",
    "ends_at": "2026-11-04T11:30:00+00:00",
    "people": 1,
    "customer_name": "Sam Patel",
    "customer_phone": "+447700900123",
    "notes": null,
    "source": "phone_ai",
    "call_id": "1b7c9e2a-3d4f-4a5b-8c6d-7e8f9a0b1c2d"
  }
}

Check the signature

Every request has X-Receptionist-Signature: t=<unix seconds>,v1=<hex>, where the hex is the HMAC-SHA256 of <t>.<the raw body> with your signing secret (shown once, when you set the webhook). Check it on the raw body, before you parse it, and refuse requests older than 5 minutes. X-Receptionist-Event names the event and X-Receptionist-Delivery is unique per delivery, so you can ignore one you have already handled.

Node.jsimport crypto from "node:crypto";

// header: the X-Receptionist-Signature header; rawBody: the request body exactly as received (a Buffer)
export function verifySignature(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  const given = Buffer.from(parts.v1 || "", "utf8");
  const wanted = Buffer.from(expected, "utf8");
  return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}

// Express: keep the raw body for this route
// app.post("/hooks/receptionist", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifySignature(process.env.WEBHOOK_SECRET, req.get("X-Receptionist-Signature"), req.body)) return res.sendStatus(400);
//   const event = JSON.parse(req.body);
//   res.sendStatus(200);
// });
Pythonimport hashlib
import hmac
import time


def verify_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    """header: the X-Receptionist-Signature header; raw_body: the body exactly as received."""
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t = int(parts["t"])
    except (ValueError, KeyError):
        return False
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Data protection

The API gives you your callers' personal data. Please handle it with care.
  • For your callers' data you are the controller and we (Linesteward, Adria Technologies Limited) are your processor, under our Data Processing Agreement. The API changes nothing about that: it is another way for you to reach your own data.
  • If a developer or another company uses a key for you, they act for you: they are your processor, so you need a contract with them (UK GDPR Article 28). You decide who gets a key, and you can revoke it at any time.
  • Copies you take out of Linesteward are yours to look after: keep them safe and no longer than you need. Our nightly deletion of old transcripts and messages does not reach your copies.
  • Ask for transcripts only when you need them. Tell your callers how you use their data in your own privacy notice.
  • Never use the API's data to market to callers by automated calls or texts. The API can't start calls or send texts.
  • We log each request made with a key (when, the method, the endpoint and the status, never the data) for 90 days, for security. When you send an Idempotency-Key, we keep that request's answer (which can hold a customer's name and phone, for a booking) for 24 hours so a retry gets the same answer, then delete it. See our privacy notice.

Reference

Every endpoint, with its scope, parameters and fields: the API reference. The same, as an OpenAPI 3.1 file for your tools: https://linesteward.com/v1/openapi.json.

Changelog

We add to version 1 without breaking it (new endpoints, new fields, new event types: ignore what you don't know). A change that would break your code gets a new version, and we tell owners first.

  • 2026-10-28: Version 1: API keys with scopes; businesses, knowledge and documents; calls, messages and bookings; webhooks with the call.completed event; idempotency keys; this guide and the reference.

Get your API key

Sign in, open Settings, then Developers, and make a key with the scopes you need.