API documentation

Everything an integration needs: how to authenticate, the shape of every response, the leads API in full, website ingest keys, and signed outbound webhooks.

Copy the API documentation

Automatic copying was blocked by the browser. Select the text below and copy it manually - then paste it into any LLM.

Base URL & versioning

All core endpoints live under the /api/v1 prefix on your deployment's base URL. In every example below, YOUR_API_BASE_URL stands for that base URL - your workspace owner provides the real value. A second, smaller ingest surface accepts provider webhooks and website form submissions. Interactive OpenAPI (Swagger UI) is served by the API host at /docs, with the raw schema at /openapi.json.

Your first request
curl YOUR_API_BASE_URL/api/v1/auth/me \
  -H "Authorization: Bearer <access_token>"

Authentication

All authenticated endpoints take a Bearer JWT: Authorization: Bearer <access_token>. One unified login serves every audience:

POST /api/v1/auth/login - request
{ "email": "priya@sunriserealty.in", "password": "Str0ng!Passw0rd", "tenant_id": null }
Response 200 - tokens issued
{ "status": "ok",
  "access_token": "eyJhbGciOi...", "refresh_token": "eyJhbGciOi...",
  "token_type": "bearer", "principal_kind": "TENANT",
  "tenant_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "agency_id": null,
  "tenants": [] }

Access tokens live 30 minutes; refresh tokens live 30 days and rotate - POST /api/v1/auth/refresh returns a fresh pair, and reusing an old refresh token revokes the whole family. POST /api/v1/auth/logout revokes the refresh token. GET /api/v1/auth/me returns the caller's kind, roles, and permissions - call it on app load.

Principal kinds
principal_kindWhoScope
PLATFORMProduct owner + Vertex Loop office staffPlatform endpoints; no tenant_id
TENANTStaff of one customer workspaceBound to exactly one tenant, taken from the token
AGENCYAgency users under a tenant's grantOnly /api/v1/agency/*, per the grant

Two login behaviours to handle: if the same email is staff in more than one tenant, login returns status: "tenant_selection_required" with a tenants array and no tokens - show a workspace picker and call login again with the chosen tenant_id. And email verification is required before login: unverified credentials return 403 with a detail starting EMAIL_NOT_VERIFIED. Registration is POST /api/v1/auth/register-tenant (202, verification email sent); passwords need 12+ characters with mixed case, a digit, and a special character. Login is rate-limited (10/minute per IP+email; 5 failures lock the account for 15 minutes).

Conventions & errors

Lists use keyset (cursor) pagination: pass cursor (opaque - return it exactly as received) and limit (default 20, clamped 1..100). Responses are { "items": [...], "next_cursor": string | null }, newest first. All ids are UUID strings; timestamps are ISO-8601 UTC; money is sent and returned as JSON numbers. Deletes are soft deletes that answer { "status": "archived", ... }. Tenant endpoints never accept a tenant_id - another tenant's object returns 404.

Error shape (all endpoints)
{ "detail": "Lead not found" }

Validation failures - HTTP 422:
{ "detail": "Validation failed",
  "errors": [ { "field": "body.email", "msg": "value is not a valid email address" } ] }

Status codes: 400 bad request (detail explains), 401 missing/invalid token, key, or signature, 403 missing permission, wrong principal kind, suspended tenant, or unverified email, 404 not found, 409 conflict, 422 validation failed, 423 account locked, 429 rate limited, 500 internal error (internals are never exposed), 502 upstream provider failure, 503 NOT_CONFIGURED - a required server-side config is missing.

Leads

The field rule: a lead needs exactly two things - name and phone. They are the only mandatory fields on create, in the app and in this API. Everything else is optional.

Lead endpoints
MethodPathNotes
POST/api/v1/leadsCreate a lead (201). Name + phone are the only mandatory fields.
POST/api/v1/leads/bulkBulk create, 1-1000 per call. Bad rows are skipped with a per-row reason.
GET/api/v1/leadsList leads. Filters: status_filter, source, campaign, tag, assigned_to, list_id.
GET/api/v1/leads/{lead_id}One lead.
PATCH/api/v1/leads/{lead_id}Update - send only changed fields.
DELETE/api/v1/leads/{lead_id}Archive (soft delete).
POST/api/v1/leads/{lead_id}/activitiesRecord a note, call attempt, or status change (201).
GET/api/v1/leads/{lead_id}/activitiesThe unified timeline feed, newest first.
POST/api/v1/leads/{lead_id}/paymentsRecord a payment received against the lead (201).
GET/api/v1/leads/{lead_id}/paymentsThe lead's payments, newest first.
POST/api/v1/leads/{lead_id}/checkinRecord a location check-in (latitude, longitude, note).
POST/api/v1/lead-listsCreate a named lead list (201).
GET/api/v1/lead-listsList lead lists.
POST/api/v1/lead-lists/{list_id}/membersAdd a lead to a list (idempotent).
DELETE/api/v1/lead-lists/{list_id}/members/{lead_id}Remove a lead from a list.
Create a lead - POST /api/v1/leads (201)
{ "name": "Ravi Kumar", "phone": "+91 98123 45678",
  "email": "ravi@example.com", "city": "Chennai", "source": "manual",
  "tags": [], "status": "new", "custom_fields": {} }

Creating a lead dedupes it to a contact by normalized phone/email, assigns it (round-robin, or the assigned_to staff member you name), starts the 30-minute response-SLA clock, and fires the lead.created automation trigger. There is no /contacts API - contacts are the internal dedupe entity behind the lead.

curl - create a lead
curl -X POST YOUR_API_BASE_URL/api/v1/leads \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Ravi Kumar", "phone": "+91 98123 45678", "source": "website" }'
JavaScript (fetch) - list new leads
const res = await fetch(
  `YOUR_API_BASE_URL/api/v1/leads?status_filter=new&limit=50`,
  { headers: { Authorization: `Bearer ${accessToken}` } },
);
const { items, next_cursor } = await res.json();

Bulk imports take 1-1000 leads per call; a row missing name or phone is skipped with a per-row reason (missing_name / missing_phone) instead of failing the file. Activities are how work is recorded on a lead - a status_change activity moves the lead's status with an audit trail, and the activities feed is the unified timeline (notes, calls, field changes, agent work). Recording a payment fires the payment.completed event for workflows; it records a receipt - no gateway is charged.

Website ingest (API keys)

To push leads from your own website or forms, create a tenant API key (POST /api/v1/integrations/api-keys, permission integration:manage). The raw key is returned exactly once - it looks like vaalo_… and only its SHA-256 is stored. Then submit enquiries to the ingest surface with the key header:

Submit a website lead
curl -X POST YOUR_API_BASE_URL/ingest/website/<tenant_id> \
  -H "X-API-Key: vaalo_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Asha Rao", "phone": "+91 98123 45679", "source": "website" }'

Submissions flow through the same raw-event-first pipeline as every connected source (dedupe and idempotency included), at up to 300 requests/minute per key. An invalid or revoked key returns 401 Invalid API key; revoking is DELETE /api/v1/integrations/api-keys/{key_id} and takes effect immediately.

Webhooks out (HMAC-signed)

Register your own app's URL and Vaalo POSTs signed event notifications to it. Subscriptions are managed under /api/v1/webhook-subscriptions (create returns the signing secret exactly once; later reads show only a 4-character hint). URLs must be https and pass an SSRF guard. Subscribable events include lead.created, lead.status_changed, lead.converted, lead.updated, lead.assigned, lead.sla_breached, deal.created, deal.moved, proposal.sent, proposal.responded, billing.payment_captured, tenant.plan_changed, support.ticket_created, and support.ticket_status_changed.

Delivery envelope (the exact POST body)
{ "id": "whdel-uuid", "event": "lead.created",
  "occurred_at": "2026-10-23T10:00:01+00:00",
  "tenant_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "data": { "source": "manual", "status": "new" } }
Headers & signature
X-Vaalo-Event:     lead.created
X-Vaalo-Delivery:  whdel-uuid            (= the body's id)
X-Vaalo-Timestamp: 1761292801            (unix seconds)
X-Vaalo-Signature: sha256=<HMAC-SHA256(secret, timestamp + "." + raw_body)>

Verify by recomputing the HMAC over the raw body you received and comparing in constant time. Deliveries time out at 10 seconds; any 2xx is a success. Failures retry after +1 minute, +5 minutes, +30 minutes, and +2 hours, and the delivery is marked dead after the 5th failed attempt - revive it with POST /api/v1/webhook-deliveries/{delivery_id}/redrive, and test a subscription end-to-end with its /test endpoint before relying on live events.

Platform operators can also issue service identities - revocable machine credentials for platform-level integrations, managed from the owner console's security module, with every use written to the platform security log. Tenant integrations should use user JWTs or tenant API keys.

Building with an AI assistant?

Copy the complete documentation - everything on this page, plus the full payload reference and an example integration flow - as one Markdown document, and paste it into any LLM.

Copy the API documentation

Automatic copying was blocked by the browser. Select the text below and copy it manually - then paste it into any LLM.