Quickstart
Five minutes: create a key, push one lead, receive one webhook. Everything below is copy-paste; replace the three placeholders.
An Owner or Admin login to a Lojiq organization, curl, and a public HTTPS URL that can receive a POST (for step 4 — webhook.site is fine for a first test).
-
Create an API key#
In the Lojiq app open Developer → API keys and click Create API key. Name it after the system that will use it (
hubspot-sync,website-forms), tick the scopesleads:write,leads:readandwebhooks:manage, and copy the key. It is shown once.bashexport LOJIQ_KEY="lojiq_live_5b7a…e3" # the key you just copied export LOJIQ_API="https://api.lojiq.ai/v1"Check it works — a
200with the API name means the key is valid:bashcurl -s "$LOJIQ_API/" -H "Authorization: Bearer $LOJIQ_KEY" # {"api":"lojiq-public-api","version":"v1","docs":"https://docs.lojiq.ai"}A
401 invalid_api_keymeans the key was mistyped or revoked. More on keys and scopes → -
Push your first lead#
bashcurl -s -X POST "$LOJIQ_API/leads" \ -H "Authorization: Bearer $LOJIQ_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone_number": "+15551234567", "first_name": "Avery", "last_name": "Rivera", "email_address": "avery@example.com", "consent_source": "web_form", "consented_at": "2026-10-02T16:58:00Z" }'You get
201and the stored lead. Run the exact same command again and you get200with the sameid: Lojiq deduplicates by phone number inside your organization, so your integration can be retried freely. (Add anIdempotency-Key: <uuid>header and a retry returns the first answer without running again.) Leads guide →201 Created{ "id": "4b1e0d1a-3c4e-4d0a-9f2b-7e1a2b3c4d5e", "first_name": "Avery", "last_name": "Rivera", "full_name": "Avery Rivera", "phone_number": "+15551234567", "email_address": "avery@example.com", "status": "new", "campaign_id": null, "consent_source": "web_form", "consented_at": "2026-10-02T16:58:00Z", "source_tag": "api:standalone", "created_at": "2026-10-02T17:03:11Z" } -
Read it back#
bashcurl -s "$LOJIQ_API/leads?phone_number=%2B15551234567" -H "Authorization: Bearer $LOJIQ_KEY"Lists are paginated with
limit(max 200) andoffset; the response is{ "data": [...], "limit": 50, "offset": 0 }. -
Subscribe to webhooks#
bashcurl -s -X POST "$LOJIQ_API/webhooks" \ -H "Authorization: Bearer $LOJIQ_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "first-test", "target_url": "https://YOUR-ENDPOINT.example.com/lojiq", "event_types": ["lead.created", "lead.updated", "call.transcript_ready", "appointment.booked"] }'201 Created — the secret is shown once{ "id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d", "name": "first-test", "target_url": "https://YOUR-ENDPOINT.example.com/lojiq", "event_types": ["lead.created", "lead.updated", "call.transcript_ready", "appointment.booked"], "enabled": true, "created_at": "2026-10-02T17:03:11Z", "secret": "whsec_9f2c…1e2f" }Store the
secret. Now create a lead with a new phone number (step 2 with a different number) and within seconds your endpoint receives:POST https://YOUR-ENDPOINT.example.com/lojiqContent-Type: application/json X-Lojiq-Event: lead.created X-Lojiq-Event-Id: c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f X-Lojiq-Delivery-Attempt: 1 X-Lojiq-Signature-256: t=1759424591,v1=3f1c9e… User-Agent: lojiq-webhooks/1.0 {"id":"c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f","type":"lead.created","organization_id":"7f6e…","occurred_at":"2026-10-02T17:03:11Z","data":{"lead_id":"…","deduped":false,"source":"public_api"}}Answer
2xxwithin 10 seconds. Anything else is retried after 30 s, 1 m, 5 m, 30 m, 2 h, 6 h and 12 h (eight attempts in all). -
Verify the signature#
Compute
HMAC-SHA256(secret, "<t>.<raw body>")and compare it tov1in constant time; reject iftis more than 5 minutes old.javascriptimport { createHmac, timingSafeEqual } from 'node:crypto'; export function verifyLojiq(rawBody, signatureHeader, secret, toleranceSec = 300) { const parts = Object.fromEntries(signatureHeader.split(',').map(p => p.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); return expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex')); } // Express: keep the RAW body — JSON re-serialisation changes bytes and breaks the HMAC. app.post('/lojiq', express.raw({ type: 'application/json' }), (req, res) => { if (!verifyLojiq(req.body.toString('utf8'), req.get('X-Lojiq-Signature-256'), process.env.LOJIQ_WEBHOOK_SECRET)) { return res.status(401).end(); } const event = JSON.parse(req.body); // dedupe on event.id, then do your work asynchronously res.status(204).end(); });pythonimport hmac, hashlib, time def verify_lojiq(raw_body: bytes, signature_header: str, secret: str, tolerance_sec: int = 300) -> bool: parts = dict(p.split("=", 1) for p in signature_header.split(",")) try: t = int(parts["t"]) except (KeyError, ValueError): return False if abs(time.time() - t) > tolerance_sec: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) # Flask @app.post("/lojiq") def lojiq_webhook(): if not verify_lojiq(request.get_data(), request.headers.get("X-Lojiq-Signature-256", ""), LOJIQ_WEBHOOK_SECRET): return "", 401 event = request.get_json(force=True) # dedupe on event["id"], then do your work asynchronously return "", 204
Next steps#
Bulk import from CSV
Preflight with inspect-csv, then import thousands of rows with a column mapping.
All 14 events
Which events fire today, their payloads, retries and the idempotency rules.
Start a call
Create a voice agent and place a phone or browser call with POST /agents/{id}/calls.
SDKs & Postman
A Postman collection and thin TypeScript / Python clients generated from the spec.