API Reference · v2026-06-15
REST + outbound webhooks for the LeadStack CRM. Sub-account-scoped Bearer auth, idempotent writes, signed webhooks, request log observability.
1. Mint a key in Settings → API keys. Copy the lsk_live_... value — you only see it once.
2. Send a request. Authentication is HTTP Bearer:
curl https://YOUR_DEPLOYMENT/api/v1/contacts \
-H "Authorization: Bearer lsk_live_..." \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp","email":"hello@acme.com"}'3. Listen for events. Add a webhook in Settings → Webhooks, copy the signing secret, verify each delivery with HMAC-SHA256.
Bearer-token auth on every request. Keys are scoped to one sub-account and one mode (live or test).
lsk_<mode>_<8-char-prefix>_<32-char-secret>
e.g. lsk_live_AB12CD34_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxadmin — full CRUD across every resource. Server-to-server only.forms-ingest — write-only on POST /v1/forms/:id/submissions. Safe to embed in client-side JS. Only endpoint with open CORS.Live and test data are walled off entirely. A test-mode key cannot read or modify live data, and vice versa. Test-mode form submissions skip the Speed-to-Lead automation (no real emails or SMS fire).
Versions are date-coded. Pin a request with LeadStack-Version: 2026-06-15. If omitted, the request resolves to the version stamped on your key at mint time — so existing integrations don't break when we release a new version.
Current version: 2026-06-15.
All error responses share a stable shape. Discriminate on code, not message.
{
"error": {
"type": "invalid_request",
"code": "invalid_body",
"message": "`name` is required (string ≤ 200 chars).",
"request_id": "req_..."
}
}The request_id matches the X-Request-Id response header. Quote it in support tickets — it's the index we look up in the request log.
Per-key sliding windows, mode-namespaced (live + test separate budgets):
admin — 60 req/min, 1,000 req/hourforms-ingest — 300 req/minEvery response includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. A 429 response also sends Retry-After.
Send Idempotency-Key: <your-key> on POST / PATCH / DELETE. We cache the response for 24 hours; a retry with the same key returns the original response without re-executing the handler. A retry with the same key but a DIFFERENT body returns 409 idempotency_collision.
Keys are 1–255 characters of [A-Za-z0-9_-:.]. UUIDs are a good default.
The lead / customer / person record.
/v1/contacts/v1/contacts/v1/contacts/:id/v1/contacts/:id/v1/contacts/:id{
"id": "contact_xxx",
"object": "contact",
"livemode": true,
"name": "Acme Corp",
"email": "hello@acme.com",
"phone": "+15555550100",
"company": "Acme",
"address": "...",
"source": "website-form",
"tags": ["hot"],
"pipeline_stage": null,
"territory_id": "global",
"email_opted_out": false,
"sms_opted_out": false,
"attribution": null,
"location": null,
"created_at": "2026-05-31T10:00:00.000Z",
"updated_at": "2026-05-31T10:00:00.000Z"
}Cursor pagination via starting_after. Default limit 20, max 100.
GET /v1/contacts?limit=50&starting_after=contact_xxx/v1/deals/v1/deals/v1/deals/:id/v1/deals/:id/v1/deals/:idStages: new, contacted, qualified, proposal, won, lost. Priorities: high, medium, low. Filter list by ?stage=...&contact_id=....
/v1/tasks/v1/tasks/v1/tasks/:id/v1/tasks/:id/v1/tasks/:idFilter list by ?completed=true|false and ?contact_id=.... Setting completed: true stamps completed_at and emits task.completed.
/v1/events/v1/events/v1/events/:id/v1/events/:id/v1/events/:idCalendar events. start_at + end_at are ISO 8601. status: scheduled, awaiting_payment, completed, cancelled, no_show.
/v1/forms/:form_id/submissionsThe single endpoint with open CORS. Use a key with forms-ingest scope (write-only) for browser submissions; an admin key works too.
POST /v1/forms/form_xxx/submissions
{
"values": {
"field_id_name": "Acme Corp",
"field_id_email": "hello@acme.com",
"field_id_phone": "+15555550100"
}
}The submission creates a Contact, writes a form_submitted activity, fires the form's configured automation, and emits the form.submitted webhook. Test-mode submissions skip the automation fire so no real outbound traffic happens.
Subscribe to events from Settings → Webhooks. Each delivery is an HTTP POST to your URL with a signed JSON body.
{
"id": "evt_xxx",
"type": "contact.created",
"api_version": "2026-06-15",
"created": 1716123456,
"livemode": true,
"data": { "contact": { ... } },
"delivery": { "id": "del_xxx", "attempt": 1 }
}Each request carries LeadStack-Signature: t=<unix_ts>,v1=<hmac_hex>. Verify it before trusting the payload.
// Node.js verification
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=", 2)),
);
const ts = Number(parts.t);
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 5-min window
const expected = createHmac("sha256", secret)
.update(`${ts}.${rawBody}`, "utf8")
.digest("hex");
return timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
}Non-2xx responses (or network failures) retry 3 times at 1m, 5m, 30m. After 10 consecutive failed deliveries, the subscription auto-pauses. Resume it from Settings → Webhooks after fixing the upstream.