Quickstart

Five minutes: create a key, push one lead, receive one webhook. Everything below is copy-paste; replace the three placeholders.

You need

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).

  1. 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 scopes leads:write, leads:read and webhooks:manage, and copy the key. It is shown once.

    bash
    export LOJIQ_KEY="lojiq_live_5b7a…e3"     # the key you just copied
    export LOJIQ_API="https://api.lojiq.ai/v1"
    

    Check it works — a 200 with the API name means the key is valid:

    bash
    curl -s "$LOJIQ_API/" -H "Authorization: Bearer $LOJIQ_KEY"
    # {"api":"lojiq-public-api","version":"v1","docs":"https://docs.lojiq.ai"}
    

    A 401 invalid_api_key means the key was mistyped or revoked. More on keys and scopes →

  2. Push your first lead#

    bash
    curl -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 201 and the stored lead. Run the exact same command again and you get 200 with the same id: Lojiq deduplicates by phone number inside your organization, so your integration can be retried freely. (Add an Idempotency-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"
    }
    
  3. Read it back#

    bash
    curl -s "$LOJIQ_API/leads?phone_number=%2B15551234567" -H "Authorization: Bearer $LOJIQ_KEY"
    

    Lists are paginated with limit (max 200) and offset; the response is { "data": [...], "limit": 50, "offset": 0 }.

  4. Subscribe to webhooks#

    bash
    curl -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/lojiq
    Content-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 2xx within 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).

  5. Verify the signature#

    Compute HMAC-SHA256(secret, "<t>.<raw body>") and compare it to v1 in constant time; reject if t is more than 5 minutes old.

    javascript
    import { 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();
    });
    
    python
    import 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#