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://api.hadar-ai.com/v1Every 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 as a Bearer token. 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://api.hadar-ai.com/v1/leads \
-H "Authorization: Bearer hdr_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Test keys are prefixed hdr_test_ and target an isolated sandbox tenant. They never produce real WhatsApp sends, voice calls, or Stripe 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 Stripe charges all disappear when idempotency keys are used correctly.
curl -X POST https://api.hadar-ai.com/v1/leads \
-H "Authorization: Bearer hdr_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: 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://api.hadar-ai.com/v1/leads \
-H "Authorization: Bearer hdr_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"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://api.hadar-ai.com/v1/leads?source=bayut&score_min=70&limit=50" \
-H "Authorization: Bearer hdr_live_..."POST /api/v1/whatsapp/messages — send a WhatsApp message
Within the 24-hour customer service window you can send free-form text; outside it you must send an approved template. The endpoint validates which side of the window you are on and rejects with code: "outside_24h_window" when the wrong shape is used.
curl -X POST https://api.hadar-ai.com/v1/whatsapp/messages \
-H "Authorization: Bearer hdr_live_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"lead_id": "lead_01HMNZ...",
"template": "viewing_confirmation_en",
"variables": {
"agent_name": "Sara",
"property_name": "Marina Vista 2BR",
"viewing_time": "Saturday 11:00 GST"
}
}'Provider-specific details (UltraMsg vs Meta Cloud), template approval, and the 24-hour window mechanics 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; seefieldfor the path.unauthorized— 401. Missing or malformed Bearer token.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. HonourRetry-Afterand back off.internal_error— 500. Send thetrace_idto support; we will have already opened the incident.service_unavailable— 503. A downstream provider (Vapi, ElevenLabs, Meta, Stripe) 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 CHANGELOG.
Where to go next
- Interactive Swagger UI — try every endpoint live.
- Security — IP allow-lists, webhook secret rotation, and audit log access.
- Service Level Agreement — what to expect when a downstream provider degrades.
- Privacy Notice — data flows, sub-processors, and PDPL posture.