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.
- 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.
- 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. - 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.
| Scope | Lets the key |
|---|---|
calls:read | Read calls: times, outcome, summary, the caller's number and, when asked for, the transcript. |
messages:read | Read the messages the receptionist took. |
bookings:read | Read bookings. |
bookings:write | Make and cancel bookings. |
businesses:read | Read the account's businesses: details, opening hours, greeting. |
businesses:write | Change a business's details, hours and greeting, and add a business (within the plan). |
knowledge:write | Replace the facts the receptionist answers from, and upload documents. |
webhooks:write | Read, 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."}}
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A value was not accepted, or a cursor or time is not valid. |
| 401 | unauthorized | No key, or the key is wrong or revoked. |
| 403 | insufficient_scope | The key lacks the scope this endpoint needs. |
| 403 / 409 | plan_limit | Your plan has no room for another business. |
| 404 | not_found | Not found, or not in your account (we never say which). |
| 409 | slot_unavailable | That booking time is not free; free_times lists the nearest free times. |
| 409 | skill_off booking_system_connected not_supported too_late_to_cancel line_not_ready | The business is not set up for this; the message says what to do. |
| 409 | idempotency_in_progress | A request with the same Idempotency-Key is still running. |
| 409 | secret_already_issued | A retry of Set a webhook: its signing secret was shown once and is not kept. Set the webhook again with a new Idempotency-Key. |
| 413 | payload_too_large | The document is bigger than the limit. |
| 422 | validation_error | The body or query does not match the reference. |
| 422 | idempotency_key_reused | The Idempotency-Key was used with a different request. |
| 429 | rate_limited | Too many requests: wait the Retry-After seconds. |
| 503 | unavailable | Something 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 oncePythonhook = 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 onceWebhooks
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.
| Event | When |
|---|---|
call.completed | A call ended: its id, business, caller's number, start, seconds, outcome, summary, language. |
message.created | The receptionist took a message: who, their number, the message, urgent or not. |
booking.created | A booking was made, on a call, in the app or through the API. |
booking.cancelled | A 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
- 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.