REST API (API keys)
Complete reference for every /api/v1 endpoint — method, path, required scope, key parameters and response summary, grouped by resource.
This is the complete REST API surface. Every endpoint lives under /api/v1 (plus a friendly unversioned alias for lead CRUD), authenticates with an API key, and enforces one scope per route.
Base URL https://crm.zodiactechsoft.in/api/v1 in production. On a subfolder install the subfolder is part of the path (http://localhost/Lead%20Hub/api/v1). Append the path from the tables below.
All requests need:
Authorization: Bearer lh_yourKeyHere
Accept: application/json
List responses use the pagination envelope (data + meta + links); single-record responses are {"data": {...}}; errors follow Error codes & handling.
Who this is for
- Developer — integrating a system, no-code tool or script against the CRM.
- Admin — checking which scope a key needs before creating it.
Prerequisites: an API key with the relevant scopes (Authentication).
Health
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/health |
none (public) | DB/cache/storage/queue check, version, timestamp. 200 ok or 503 degraded |
Leads
Scopes: read:leads, write:leads, delete:leads.
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/leads |
read:leads |
List leads (paginated, filterable) |
POST |
/leads |
write:leads |
Create a lead — 201 on success; 402 if the plan's lead quota is exhausted |
GET |
/leads/{id} |
read:leads |
Single lead with tags, assigned user, pipeline stage |
PUT / PATCH |
/leads/{id} |
write:leads |
Update a lead |
DELETE |
/leads/{id} |
delete:leads |
Delete a lead (soft delete) |
POST |
/leads/{id}/tags |
write:leads |
Attach tags (creates missing tags) |
DELETE |
/leads/{id}/tags/{tag} |
write:leads |
Detach one tag by id or name |
List filters (query string): source, status, pipeline_id, stage_id, assigned_user_id, tag (name), search (matches first/last name, email, phone), created_after, created_before, starred=1, sort (one of created_at, updated_at, lead_score, first_name, last_name, email; default created_at), direction (asc/desc, default desc), per_page (default 15, max 100).
Create body (real fields):
{
"first_name": "Aarav",
"last_name": "Shah",
"email": "aarav@example.com",
"phone": "+91 98xxxxxx21",
"source": "api",
"status": "new",
"pipeline_id": 1,
"pipeline_stage_id": 4,
"assigned_user_id": 7,
"notes": "Interested in the 3BHK project",
"custom_fields": { "budget": "80L" },
"tags": ["website", "hot"]
}
first_name is required; email max 255, phone max 50, source/status max 80/50. pipeline_id, pipeline_stage_id and assigned_user_id are validated to belong to this workspace (cross-tenant ids fail 422).
Tag attach body: {"tags": ["hot", "mumbai"]} → {"data": [...], "message": "2 tag(s) attached."}. Tag detach accepts an id or a name in the URL segment.
Unversioned aliases
The same lead CRUD is also exposed without the version prefix for tools that mishandle 308 redirects: GET/POST /api/leads, GET/PUT/PATCH/DELETE /api/leads/{id}, POST /api/leads/{id}/tags, DELETE /api/leads/{id}/tags/{tag}. Same scopes, same behaviour.
Pipelines & stages
Scopes: read:pipelines, write:pipelines (stage routes are covered by the pipelines scope).
| Method | Path | Purpose |
|---|---|---|
GET |
/pipelines |
List pipelines |
POST |
/pipelines |
Create — body: name (required, max 100), description (optional), is_default (bool) |
GET |
/pipelines/{id} |
Pipeline detail |
PUT / PATCH |
/pipelines/{id} |
Update |
DELETE |
/pipelines/{id} |
Delete |
GET |
/pipelines/{id}/stages |
List stages of a pipeline |
POST |
/pipelines/{id}/stages |
Create stage — body: name (required, max 100), color, sort_order, is_won, is_lost |
GET |
/pipelines/{id}/stages/{stage} |
Stage detail |
PUT / PATCH |
/pipelines/{id}/stages/{stage} |
Update |
DELETE |
/pipelines/{id}/stages/{stage} |
Delete — blocked with 422 Cannot delete stage with leads. Move leads first. if leads sit in the stage |
Tags
Scopes: read:tags, write:tags.
| Method | Path | Purpose |
|---|---|---|
GET |
/tags |
List tags |
POST |
/tags |
Create — body: name (required, max 80), color |
GET |
/tags/{id} |
Tag detail |
PUT / PATCH |
/tags/{id} |
Update |
DELETE |
/tags/{id} |
Delete |
Users
Scopes: read:users, write:users, delete:users.
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/users |
read:users |
List workspace users |
POST |
/users |
write:users |
Create — body: name, email (unique), password (min 8), timezone, role (admin or member) |
GET |
/users/{id} |
read:users |
User detail |
PUT / PATCH |
/users/{id} |
write:users |
Update |
DELETE |
/users/{id} |
delete:users |
Delete |
Forms
Scopes: read:forms, write:forms, delete:forms.
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/forms |
read:forms |
List forms |
POST |
/forms |
write:forms |
Create — body: name (required), title, description, submit_label, thank_you_message, redirect_url, pipeline_id, pipeline_stage_id, active, multi_step |
GET |
/forms/{id} |
read:forms |
Form detail |
PUT / PATCH |
/forms/{id} |
write:forms |
Update |
DELETE |
/forms/{id} |
delete:forms |
Delete |
POST |
/forms/{id}/submissions |
write:forms |
Submit an entry — body: {"fields": {"name": "...", "email": "..."}}; creates a lead through the same pipeline as the public form |
Automations
Scopes: read:automations, write:automations, delete:automations.
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/automations |
read:automations |
List automations |
POST |
/automations |
write:automations |
Create — body: name, description, enabled, trigger_type (must be a supported trigger), trigger_config (object) |
GET |
/automations/{id} |
read:automations |
Detail |
PUT / PATCH |
/automations/{id} |
write:automations |
Update |
DELETE |
/automations/{id} |
delete:automations |
Delete |
Integrations
Scopes: read:integrations, write:integrations (write covers create, update and delete).
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET |
/integrations/types |
read:integrations |
List supported integration types |
GET |
/integrations |
read:integrations |
List connections |
POST |
/integrations |
write:integrations |
Create — body: type (must be a supported type, else 422 Unsupported integration type.), name, enabled, config (stored encrypted), field_mapping, filter_config |
GET |
/integrations/{id} |
read:integrations |
Detail |
PUT / PATCH |
/integrations/{id} |
write:integrations |
Update |
DELETE |
/integrations/{id} |
write:integrations |
Delete |
Documentation endpoints (no auth)
| Path | Purpose |
|---|---|
/api/docs |
Swagger UI (canonical) |
/api/documentation, /api/swagger |
Aliases of the same UI |
/api/v1/docs |
Versioned alias |
Public (token/anon) endpoints — outside /api/v1
Most authenticate by URL token or provider signature; inbound lead intake instead uses a Bearer tenant API key. They are documented in full on Webhook endpoints.
| Method | Path | Rate limit | Purpose |
|---|---|---|---|
POST |
/api/inbound/leads and /api/v1/inbound |
120/min/IP | Lead intake for Zapier/n8n-style callers — Authorization: Bearer {tenant api_key} + ?tenant={slug} |
POST |
/api/webhook/{tenant}/{source}/{token} |
120/min/IP | Token-based generic inbound webhook receiver |
POST |
/api/forms/{tenant}/{slug}/submit |
60/min/IP | Public form submission |
POST / GET |
/api/messaging/{channel}/webhook/{tenant}/{token} |
120/min/IP | WhatsApp/SMS/Telegram/Viber inbound + verification handshake |
POST |
/api/voice/{tenant}/{twiml|status|recording}/{token} |
120/min/IP | Twilio voice callbacks |
POST |
/api/webhooks/razorpay and /api/webhooks/cashfree |
240/min/IP | Commercial billing provider webhooks |
Inbound lead intake: authenticate with the workspace's tenant API key as a Bearer token and name the same workspace in ?tenant={slug} — a missing key returns 401, an unknown or inactive tenant slug 404, and a key that does not match that tenant 401. email is required; accepted fields: first_name, last_name, phone, company, source, notes, tags[], custom_fields. A new lead returns 201 {"status":"created","lead_id":...}; an email that already exists returns 200 {"status":"duplicate","lead_id":...}.
Version fallback behaviour
Any other /api/<path> request is 308-redirected to /api/v1/<path> (method and body preserved). Paths whose first segment is already a root surface (v1, docs, documentation, swagger, leads, inbound, webhook, webhooks, forms, messaging, voice) are excluded and return JSON 404 {"status":"error","message":"Endpoint not found.","code":"NOT_FOUND"}.
Common mistakes
- Requesting
/api/v1/leads/{id}with a lead id from another workspace — returns 404 by design. - Sending
statusas an array — it is a string filter here. - Using
PUTwith partial data —PUTvalidates likePATCH(sometimesrules), but send the full object when in doubt. - Forgetting
Accept: application/json— Laravel then renders HTML error pages that break JSON parsers (message parsing still works, but use the header).
Troubleshooting
| Symptom | Check |
|---|---|
| 403 with a scope name | Key scopes vs the table above |
| 422 "does not belong to your workspace" | pipeline_id / stage_id / assigned_user_id from another tenant |
| 402 on create | Plan lead quota — inbound capture paths still work; only manual/API creation is blocked |
| Stage delete fails | Move leads out of the stage first |
Technical details
- Routes:
routes/api.php; controllers:App\Http\Controllers\Api\V1\*. - Resources:
App\Http\Resources\Api\{LeadResource, TagResource, ApiResource}. - Scope middleware:
api.key:<scope>→App\Http\Middleware\ApiKeyAuthentication. - Exact validation rules live in each controller (
LeadController,PipelineController,StageController,TagController,UserController,FormController,AutomationController,IntegrationController).
Related guides
Was this page helpful?
Thanks — noted. ✦