Skip to content

API Reference

The Hadar REST API is the same surface our dashboard uses. It is organised around resources — leads, conversations, properties, bookings, invoices, webhooks — and returns the same JSON envelope on every response. This page is the operator-facing narrative; for an interactive sandbox open /developers/api-docs, which renders our OpenAPI 3.1 schema in Swagger UI and lets you try every endpoint against your workspace with a temporary key.

Base URL

https://www.hadar-ai.com/api/v1

Every documented route lives under /api/v1/*. The unversioned alias /api/* exists for the dashboard only — do not rely on it from integrations.

Authentication

Send a workspace API key in the X-API-Key header. Mint keys from Settings > API Keys. Each key is scoped to a tenant, an environment (live or test), and a role; only the last four characters are visible after creation, so copy the full token immediately. Rotate keys every 90 days; revoking a key is instant and irreversible.

curl https://www.hadar-ai.com/api/v1/leads \
  -H "X-API-Key: ak_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Test keys are prefixed ak_test_ and target an isolated sandbox tenant. They never produce real WhatsApp sends, voice calls, or billing charges.

Rate limits

  • Starter: 60 requests / minute / key
  • Growth: 300 requests / minute / key
  • Enterprise: negotiated; default 1,000 / minute / key

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. When you exceed the budget you receive HTTP 429 with a Retry-After value in seconds. Auth and revenue routes are fail-closed — a Redis outage will not let traffic through unmetered.

Idempotency keys

All state-mutating routes accept an Idempotency-Key header (UUID v4 recommended). Hadar stores the request hash and the first response under that key for 24 hours per tenant; subsequent calls with the same key return the original response. Re-send with the same key on network failures rather than retrying without one — duplicate leads, double WhatsApp sends, and double billing charges all disappear when idempotency keys are used correctly.

curl -X POST https://www.hadar-ai.com/api/v1/leads \
  -H "X-API-Key: ak_live_..." \
  -H "Idempotency-Key: 11111111-2222-3333-4444-555555555555" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Aisha Khan",
    "phone": "+971501234567",
    "email": "aisha@example.com",
    "source": "bayut",
    "budget_aed": 1800000,
    "bedrooms": 2,
    "area": "Dubai Marina"
  }'

Response envelope

Every endpoint returns the same shape: { data, error }. Exactly one of the two is non-null. data contains the resource (or a paginated list); error is { code, message, field?, trace_id }.

// Success
{
  "data": {
    "id": "lead_01HMNZ...",
    "name": "Aisha Khan",
    "score": 78,
    "created_at": "2026-05-15T08:42:11.103Z"
  },
  "error": null
}

// Failure
{
  "data": null,
  "error": {
    "code": "validation_error",
    "message": "lead.email must be a valid email address",
    "field": "lead.email",
    "trace_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
  }
}

Always include the trace_id in any support ticket — it stitches the request across the edge proxy, application logs, and Sentry within one click.

Pagination

List endpoints are cursor-paginated. The default page size is 25 and the maximum is 100. Pass cursor and optionally limit as query parameters; the response carries a pagination block on the data envelope.

// GET /api/v1/leads?limit=50&cursor=eyJ0IjoxNzE...
{
  "data": {
    "items": [ /* up to 50 leads */ ],
    "pagination": {
      "next_cursor": "eyJ0IjoxNzE2MDQ4NDQ1LCJpZCI6ImxlYWRfMDFI...",
      "has_more": true,
      "limit": 50
    }
  },
  "error": null
}

Cursors are opaque, stable, and tenant-scoped. Do not parse them. When has_more is false, the next_cursor is null.

Key endpoints

POST /api/v1/leads — create a lead

The primary intake endpoint. Required: full_name, plus at least one of phone or email. Source UTMs are normalised; Bayut and Property Finder values are recognised by name.

curl -X POST https://www.hadar-ai.com/api/v1/leads \
  -H "X-API-Key: ak_live_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "full_name": "Omar Saeed",
    "phone": "+971559876543",
    "source": "property_finder",
    "budget_aed": 3500000,
    "bedrooms": 3,
    "area": "Palm Jumeirah",
    "handover_horizon": "off_plan_2027"
  }'

GET /api/v1/leads — list leads

Supports filters: source, score_min, created_after, assigned_to, status. Use cursor pagination for any result set larger than 100.

curl "https://www.hadar-ai.com/api/v1/leads?source=bayut&score_min=70&limit=50" \
  -H "X-API-Key: ak_live_..."

POST /api/v1/webhooks — register an outbound webhook

Create tenant-scoped webhooks for events that Hadar emits, such as lead, property, and appointment changes. The plaintext secret is returned once at creation time.

curl -X POST https://www.hadar-ai.com/api/v1/webhooks \
  -H "X-API-Key: ak_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hadar/webhook",
    "description": "CRM sync",
    "events": ["lead.created", "lead.updated"]
  }'

WhatsApp sending, template approval, and the 24-hour customer-service window are configured inside the authenticated dashboard. Provider-specific setup details live in WhatsApp Integration.

Webhooks & signature verification

Hadar signs every outbound webhook with HMAC-SHA256 over the raw request body using your per-endpoint secret. Verify before trusting the payload.

// Headers Hadar sends
X-Hadar-Signature: t=1715760000,v1=4f3b1c...8a2
X-Hadar-Event: lead.created
X-Hadar-Delivery: dlv_01HMNZ...

// Node.js verification (constant-time compare)
import { createHmac, timingSafeEqual } from "node:crypto"

function verify(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("="))
  ) as { t: string; v1: string }
  const signed = `${parts.t}.${rawBody}`
  const expected = createHmac("sha256", secret).update(signed).digest("hex")
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1)
  return a.length === b.length && timingSafeEqual(a, b)
}

Reject any request whose timestamp is more than five minutes old to defeat replay attacks. Retries use exponential backoff (1s, 5s, 30s, 5m, 30m, 2h, 12h) and stop after 24 hours; failed deliveries are visible under Settings > Webhooks > Deliveries.

Error codes reference

  • validation_error — 400. Field-level issue; see field for the path.
  • unauthorized — 401. Missing or invalid X-API-Key.
  • invalid_api_key — 401. Token signature recognised but revoked or expired.
  • forbidden — 403. Authenticated but not authorised for this resource.
  • not_found — 404. Resource does not exist in this tenant.
  • conflict — 409. Idempotency key reused with a different payload.
  • outside_24h_window — 422. WhatsApp free-form attempted outside the customer service window.
  • do_not_contact — 422. Lead has opted out of this channel.
  • rate_limited — 429. Honour Retry-After and back off.
  • internal_error — 500. Send the trace_id to support; we will have already opened the incident.
  • service_unavailable — 503. A downstream provider (Vapi, ElevenLabs, Meta, Creem) is degraded; check the status page.

Versioning

We version at the URL prefix (/v1). Breaking changes ship under a new prefix with at least 90 days of overlap before the prior version is retired. Non-breaking additions ship under the current prefix and are announced in the API documentation.

Where to go next