Getting started
Quickstart
From nothing to a working integration in about ten minutes.
Before you start#
- A Line Me Up business on Basic or a plan above it, and its owner's sign-in — only the owner can make keys and endpoints.
-
A terminal with
curl, or Node 18, Python 3 or PHP 8 — every sample comes in all four; pick one and the whole site follows. -
For step 4, a way to give your laptop an HTTPS address for a few minutes: any tunnelling tool
will do. Webhooks are only ever sent to
https://addresses.
Try this on a quiet line, or before opening: the visit you add in step 3 is real and appears on the business's board.
1. Create a key#
- Sign in as the owner and open Settings → Integrations.
- Under API keys, choose Create key.
-
Name it Quickstart, and tick
readandvisits:write. Leave lines and desks on All. - Create it, and copy the key. It is shown once. Line Me Up keeps only a hash of it, so nobody — us included — can show it to you again.
# Your terminal. The key is shown once — paste it here, not into code.
export LINEMEUP_API_KEY="lmu_live_…" 2. Make your first call#
Ask who the key belongs to:
curl
curl "https://business.linemeup.in/api/v1/business" \
-H "Authorization: Bearer $LINEMEUP_API_KEY" Node
const res = await fetch("https://business.linemeup.in/api/v1/business", {
headers: {
Authorization: `Bearer ${process.env.LINEMEUP_API_KEY}`,
},
});
if (!res.ok) throw new Error(`Line Me Up answered ${res.status}`);
const answer = await res.json(); Python
import os
import requests
res = requests.get(
"https://business.linemeup.in/api/v1/business",
headers={
"Authorization": f"Bearer {os.environ['LINEMEUP_API_KEY']}",
},
timeout=10,
)
res.raise_for_status()
answer = res.json() PHP
<?php
$ch = curl_init("https://business.linemeup.in/api/v1/business");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LINEMEUP_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$answer = json_decode(curl_exec($ch), true); {
"id": "bus-0193f2a1-7c4e-7a10-9b2e-000000000001",
"name": "Sunrise Clinic",
"slug": "sunrise-clinic",
"category": "healthcare",
"timeZone": "Asia/Kolkata",
"today": "2026-10-30",
"clockFormat": "12h"
}
A 401 means the key was not sent whole, or has a space or a line break in it. Then list
the lines, and note the id of the one you will use:
curl
curl "https://business.linemeup.in/api/v1/lines" \
-H "Authorization: Bearer $LINEMEUP_API_KEY" Node
const res = await fetch("https://business.linemeup.in/api/v1/lines", {
headers: {
Authorization: `Bearer ${process.env.LINEMEUP_API_KEY}`,
},
});
if (!res.ok) throw new Error(`Line Me Up answered ${res.status}`);
const answer = await res.json(); Python
import os
import requests
res = requests.get(
"https://business.linemeup.in/api/v1/lines",
headers={
"Authorization": f"Bearer {os.environ['LINEMEUP_API_KEY']}",
},
timeout=10,
)
res.raise_for_status()
answer = res.json() PHP
<?php
$ch = curl_init("https://business.linemeup.in/api/v1/lines");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LINEMEUP_API_KEY"),
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$answer = json_decode(curl_exec($ch), true); {
"data": [
{
"id": "line-0193f2a1-7c4e-7a10-9b2e-000000000001",
"name": "General",
"code": "GEN",
"type": "walk_in",
"paused": false,
"waiting": 4,
"avgServiceMinutes": 10,
"maxCapacity": 60,
"description": "Walk-ins for a first visit",
"location": "Ground floor",
"callGraceMinutes": 5,
"appointmentGraceBeforeMinutes": 10,
"maxLateArrivalMinutes": 15,
"autoNoShowEnabled": true
},
{
"id": "line-0193f2a1-7c4e-7a10-9b2e-000000000002",
"name": "X-ray",
"code": "XR",
"type": "scheduled_walk_in",
"paused": false,
"waiting": 1,
"avgServiceMinutes": 15,
"maxCapacity": 30,
"description": null,
"location": "First floor, room 4",
"callGraceMinutes": 5,
"appointmentGraceBeforeMinutes": 10,
"maxLateArrivalMinutes": 10,
"autoNoShowEnabled": true
}
]
} 3. Add a visit#
Put somebody in the line, with a name and a phone number. Use your own line's id. The
Idempotency-Key makes it safe to run twice: the second time returns the same visit rather
than adding another — see Idempotency.
curl
curl -X POST "https://business.linemeup.in/api/v1/visits" \
-H "Authorization: Bearer $LINEMEUP_API_KEY" \
-H "Idempotency-Key: crm-visit-1042" \
-H "Content-Type: application/json" \
-d '{"lineId":"line-0193f2a1-7c4e-7a10-9b2e-000000000001","customer":{"name":"Asha Rao","phone":"+919000000001"},"note":"First visit","reference":"CRM-1042","consent":true}' Node
const res = await fetch("https://business.linemeup.in/api/v1/visits", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEMEUP_API_KEY}`,
"Idempotency-Key": "crm-visit-1042",
"Content-Type": "application/json",
},
body: JSON.stringify({
"lineId": "line-0193f2a1-7c4e-7a10-9b2e-000000000001",
"customer": {
"name": "Asha Rao",
"phone": "+919000000001"
},
"note": "First visit",
"reference": "CRM-1042",
"consent": true
}),
});
if (!res.ok) throw new Error(`Line Me Up answered ${res.status}`);
const answer = await res.json(); Python
import os
import requests
res = requests.post(
"https://business.linemeup.in/api/v1/visits",
headers={
"Authorization": f"Bearer {os.environ['LINEMEUP_API_KEY']}",
"Idempotency-Key": "crm-visit-1042",
},
json={
"lineId": "line-0193f2a1-7c4e-7a10-9b2e-000000000001",
"customer": {
"name": "Asha Rao",
"phone": "+919000000001",
},
"note": "First visit",
"reference": "CRM-1042",
"consent": True,
},
timeout=10,
)
res.raise_for_status()
answer = res.json() PHP
<?php
$ch = curl_init("https://business.linemeup.in/api/v1/visits");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("LINEMEUP_API_KEY"),
"Idempotency-Key: crm-visit-1042",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"lineId" => 'line-0193f2a1-7c4e-7a10-9b2e-000000000001',
"customer" => [
"name" => 'Asha Rao',
"phone" => '+919000000001',
],
"note" => 'First visit',
"reference" => 'CRM-1042',
"consent" => true,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$answer = json_decode(curl_exec($ch), true); {
"data": [
{
"id": "job-0193f2a1-7c4e-7a10-9b2e-000000000001",
"journeyId": "jny-0193f2a1-7c4e-7a10-9b2e-000000000001",
"token": "GEN-20261030-014",
"lineId": "line-0193f2a1-7c4e-7a10-9b2e-000000000001",
"status": "queued",
"bookingType": "walk_in",
"priority": false,
"note": "First visit",
"customer": {
"name": "Asha Rao",
"phone": "+919000000001"
},
"services": [
{
"id": "svc-0193f2a1-7c4e-7a10-9b2e-000000000002",
"name": "General consultation"
}
],
"deskId": null,
"position": 3,
"slotAt": null,
"createdAt": "2026-10-30T04:12:00.000Z",
"calledAt": null,
"startedAt": null,
"endedAt": null,
"updatedAt": "2026-10-30T04:12:00.000Z"
}
]
}
The answer is a list because a visit for services on two lines is two visits, one on each. Open
the board in the staff app: the token is in the line, at the place position says. To
take it out again, cancel it — Visits → Cancel.
4. Receive a webhook#
Now the other direction. Start a small receiver that checks each delivery's signature and prints the event:
mkdir line-me-up-hooks && cd line-me-up-hooks
npm init -y && npm install express
# save the two files below as verify-line-me-up.mjs and server.mjs, then:
export LINEMEUP_WEBHOOK_SECRET="whsec_…"
node server.mjs // verify-line-me-up.mjs — Node 18 or later, no dependencies.
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Whether a delivery really came from Line Me Up.
*
* rawBody: the request body exactly as it arrived (a Buffer or a string) —
* never JSON you parsed and turned back into text.
* header: the LineMeUp-Signature header.
* secret: your endpoint's signing secret, whsec_…
*/
export function verifyLineMeUp(
rawBody,
header,
secret,
{ toleranceSeconds = 300, nowSeconds = Math.floor(Date.now() / 1000) } = {},
) {
if (typeof header !== "string" || typeof secret !== "string") return false;
const parts = header.split(",").map((part) => part.trim());
const t = parts.find((part) => part.startsWith("t="))?.slice(2);
if (t === undefined || !/^\d+$/.test(t)) return false;
if (Math.abs(nowSeconds - Number(t)) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
return parts
.filter((part) => part.startsWith("v1="))
.some((part) => {
const given = Buffer.from(part.slice(3), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
} // server.mjs — npm install express
import express from "express";
import { verifyLineMeUp } from "./verify-line-me-up.mjs";
const app = express();
// express.raw, not express.json: the signature is over the bytes as sent.
app.post("/webhooks/line-me-up", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyLineMeUp(
req.body,
req.get("LineMeUp-Signature"),
process.env.LINEMEUP_WEBHOOK_SECRET,
);
if (!ok) return res.status(400).send("Bad signature");
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // answer within 10 seconds, then do the work
console.log(event.type, JSON.stringify(event.data, null, 2));
});
app.listen(3000); - Point your tunnel at port 3000 and copy its
https://address. -
In Settings → Integrations, under Webhooks, choose Add endpoint. Enter that
address followed by
/webhooks/line-me-up, and tickvisit.created. -
Copy the signing secret — also shown once — into
LINEMEUP_WEBHOOK_SECRETand restart the receiver. -
Press Send test event. Your terminal prints
webhook.test, and the endpoint's delivery log shows the attempt, with your receiver's200. -
Run step 3 again with a new
Idempotency-Key. Within seconds your terminal printsvisit.created:
{
"id": "evt-0193f2a1-7c4e-7a10-9b2e-000000000001",
"type": "visit.created",
"apiVersion": "v1",
"createdAt": "2026-10-30T04:12:00.000Z",
"businessId": "bus-0193f2a1-7c4e-7a10-9b2e-000000000001",
"test": false,
"data": {
"visit": {
"id": "job-0193f2a1-7c4e-7a10-9b2e-000000000001",
"journeyId": "jny-0193f2a1-7c4e-7a10-9b2e-000000000001",
"token": "GEN-20261030-014",
"lineId": "line-0193f2a1-7c4e-7a10-9b2e-000000000001",
"status": "queued",
"bookingType": "walk_in",
"priority": false,
"customer": {
"name": "Asha Rao",
"phone": "+919000000001"
},
"services": [
{
"id": "svc-0193f2a1-7c4e-7a10-9b2e-000000000002",
"name": "General consultation"
}
],
"deskId": null,
"position": 3,
"slotAt": null,
"createdAt": "2026-10-30T04:12:00.000Z",
"calledAt": null,
"startedAt": null,
"endedAt": null,
"updatedAt": "2026-10-30T04:12:00.000Z"
}
}
}
If nothing arrives, open the delivery log: each attempt shows the status your receiver answered,
or why we could not reach it. A 400 Bad signature nearly always means the secret was
pasted with a space, or the receiver parsed the JSON before checking it.
What next#
- Revoke the Quickstart key and make one for real, with only the scopes it needs — Scopes and restrictions.
- Move a visit along: call it, serve it, complete it — Visits.
- Read the event catalogue, and how retries work.
- Pick a guide close to what you are building.