Skip to content

API and webhooks

Last updated 15 October 2026

Send your calls, messages and bookings to your own software the moment they happen, read them back, and make bookings from other systems. Webhooks and the API come with the Growth and Pro plans (once a trial ends); Zapier works on every plan, trials included.

Getting started

Everything is set up on the Integrations page of your dashboard, by the account owner:

  • Webhooks: add the https address of your server (or of Make or n8n), choose events, and copy the signing secret. Use “Send test” to check it, and the delivery log to see what your server answered.
  • API keys: create a key, copy it (it's shown once) and send it with every request.
  • Zapier: press “Connect Zapier” and paste the key into Zapier when it asks.

The full reference is also available as an OpenAPI file.

Webhooks

Lightspire sends an HTTPS POST with a JSON body to your address within seconds of each event:

  • call.ended: Every answered call, once it's written up: the caller, outcome, summary and length.
  • message.taken: A caller left a message: who, their number and what it's about.
  • booking.created: A booking made on a call, in the dashboard or through the API.
  • booking.cancelled: A booking cancelled on a call or in the dashboard.
  • booking.rescheduled: A caller moved their booking to another time.
  • customer.created: Someone new called or was booked: their name, number and email.

Every body has the same envelope; data is the call, message, booking or customer, in the same shape the API returns:

{
  "id": "0b5e6f8a-3c1d-4e7b-9a52-6d1f2c3b4a59",
  "type": "booking.created",
  "created_at": "2026-10-15T01:02:03.000Z",
  "business_id": "9cb81032-581c-45d0-918d-845feaff45bf",
  "data": {
    "id": "6a1c9e44-0f6b-4b8f-8a39-0d6e1f2b3c4d",
    "status": "confirmed",
    "starts_at": "2026-10-16T23:00:00.000Z",
    "ends_at": "2026-10-16T23:30:00.000Z",
    "timezone": "Australia/Melbourne",
    "service_name": "Initial consult",
    "customer_name": "Sarah Nguyen",
    "customer_phone": "+61412345678",
    "customer_email": "sarah@example.com",
    "notes": null,
    "answers": [{ "question": "Is this for an injury?", "answer": "Yes, my knee" }],
    "source": "phone",
    "changed_by": "phone",
    "dashboard_url": "https://app.lightspire.ai/dashboard/bookings?booking=6a1c9e44-..."
  }
}

Booking events add changed_by (phone, manual or api); booking.rescheduled adds previous_booking_id, previous_starts_at and previous_ends_at. Times are UTC; timezone is the business's. Recordings are never sent: open dashboard_url to listen.

Delivery and retries

  • Answer with any 2xx within 10 seconds. We don't follow redirects, so use the final address.
  • Anything else is retried after 5 seconds, 5 minutes, 30 minutes, then 2, 5, 10, 14 and 20 hours, for up to 3 days. A 429 or 503 with Retry-After is respected.
  • A retry has the same webhook-id, so you can skip events you've already handled. Events can arrive out of order: use created_at.
  • After 5 failures in a row we pause for 10 minutes. After 3 days of failures, or if you answer 410 Gone, the endpoint is turned off and the owner is emailed.
  • Deliveries are listed in the dashboard for 30 days, with the status code and the first 500 characters of your answer, and a Retry button.

Checking signatures

Every request is signed the Standard Webhooks way, with three headers: webhook-id (the event id), webhook-timestamp (seconds since 1970) and webhook-signature: v1, and the base64 HMAC-SHA256 of {id}.{timestamp}.{body}, keyed with your secret (the part after whsec_, base64-decoded). Check it against the raw body, before parsing it, and refuse timestamps more than 5 minutes old.

Node.js, with the standard library:

// npm install standardwebhooks
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.LIGHTSPIRE_WEBHOOK_SECRET); // "whsec_..."

app.post("/lightspire", express.raw({ type: "application/json" }), (req, res) => {
  try {
    // Throws unless the signature matches and the timestamp is within 5 minutes
    const event = wh.verify(req.body.toString("utf8"), req.headers);
    // event.id is the same on every retry: skip it if you've already handled it
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});

Node.js, by hand:

import crypto from "node:crypto";

function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
  return headers["webhook-signature"].split(" ").some((part) => {
    const [version, sig] = part.split(",");
    const given = Buffer.from(sig ?? "", "base64");
    return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected);
  });
}

Python, with the standard library:

# pip install standardwebhooks
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["LIGHTSPIRE_WEBHOOK_SECRET"])  # "whsec_..."

@app.post("/lightspire")
def lightspire():
    try:
        event = wh.verify(request.get_data(as_text=True), dict(request.headers))
    except Exception:
        return "", 400
    return "", 200

Python, by hand:

import base64, hashlib, hmac, time

def verify(secret: str, headers: dict, raw_body: str) -> bool:
    msg_id, ts, signatures = headers["webhook-id"], headers["webhook-timestamp"], headers["webhook-signature"]
    if abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.{raw_body}".encode(), hashlib.sha256).digest()).decode()
    return any(hmac.compare_digest(expected, s.split(",", 1)[1]) for s in signatures.split(" ") if s.startswith("v1,"))

The API

Send your key in the Authorization header. Each key can make 60 requests a minute; every answer says how many are left (X-RateLimit-Remaining), and a 429 says when to try again (Retry-After).

curl https://app.lightspire.ai/api/v1/me -H "Authorization: Bearer ls_live_..."
  • GET /me: your business, its agents and each agent's intake questions (their ids, and which must be answered).
  • GET /calls and GET /calls/{id}: answered calls. Add ?include=transcript to one call for its transcript.
  • GET /messages: messages callers left.
  • GET /bookings (status, from, to) and POST /bookings.
  • GET /customers (phone, email) and POST /customers (finds the customer with that number, or adds them).
  • POST /hooks and DELETE /hooks/{id}: subscribe a URL to one event (REST hooks, for Zapier and Make).

Lists are newest first: limit (up to 100), then pass next_before as before for the next page.

Making a booking runs the same checks as a booking on a call: opening hours, notice, overlaps, your connected calendars, and your agent's must-ask intake questions. Send the answers in answers by question id (from GET /me); a must-ask question left out is a 400 (send "didn't say" if the customer would rather not). If the agent has a service area, send the suburb the job is in as suburb (with its postcode if you have it, like "Ryde 2112"): without it the booking is a 400 ("suburb required"), and so is a suburb outside the area or one we can't be sure of. Bookings made in the dashboard aren't checked against the area: they're yours to make. Date and time are in the business's time zone:

curl https://app.lightspire.ai/api/v1/bookings -H "Authorization: Bearer ls_live_..." -H "Content-Type: application/json" \
  -d '{"date":"2026-10-20","time":"09:30","name":"Sarah Nguyen","phone":"0412 345 678","answers":{"injury":"Sore knee"}}'

Errors look like {"error":{"code":"CONFLICT","message":"That time isn't available.","requestId":"..."}}. Codes: VALIDATION (400), UNAUTHENTICATED (401), FORBIDDEN (403), NOT_FOUND (404), CONFLICT (409), RATE_LIMITED (429), UNAVAILABLE (503, try again after Retry-After) and INTERNAL (500).

Zapier

The Lightspire app in Zapier has instant triggers for a new call, a new message, a new booking and a cancelled booking, and actions to create a booking and to find or create a customer. Press “Connect Zapier” on the Integrations page for the key Zapier asks for. Revoking that key disconnects Zapier. A Zapier key works only with the Zapier app (it subscribes only Zapier's own addresses); for Make, n8n or your own code, use an API key. Zapier stops when a free trial ends, until a plan is chosen.

Your callers' data

Webhooks and the API carry your callers' names, numbers, emails and call summaries to the places you choose. Keep your keys and secrets private, revoke any you no longer use, and see our privacy policy.