API overview
The two APIs Zodiac Tech CRM exposes — the REST API for servers and the Mobile API for the Android app — how they authenticate, version, and rate-limit.
Zodiac Tech CRM exposes two separate HTTP APIs:
| API | Base path | Who it is for | Authentication |
|---|---|---|---|
| REST API | /api/v1 |
Servers, no-code tools (Zapier, Pabbly, n8n), custom scripts | Per-workspace API keys (Authorization: Bearer lh_...) |
| Mobile API | /api/v1/mobile |
The ZodiacTech CRM Android app (com.zodiactech.crm) |
Per-user device tokens (Authorization: Bearer <token>) |
They share the same Laravel application but have different auth, different response envelopes and different rate limits. Pick the one that matches your client:
- A server syncing leads in and out → REST API (REST API (API keys)).
- The Android app installed on a sales agent's phone → Mobile API (Mobile API (device tokens)).
- External platforms calling you (Meta, WhatsApp, Razorpay, Twilio) → webhooks (Webhook endpoints).
Who this is for
- Developer — building an integration against the REST API or debugging the mobile app's traffic.
- Admin — creating and scoping API keys for your workspace.
- Super-admin — monitoring API health and platform-level rate limits.
Prerequisites: a working install (see Go-live checklist) reachable over HTTP(S).
Base URLs
The exact base URL depends on how the install is deployed. When the domain's document root points at the public/ folder (the recommended setup on cPanel), there is no subfolder in the URLs:
Production: https://crm.zodiactechsoft.in/api/v1 (REST)
https://crm.zodiactechsoft.in/api/v1/mobile/ (Mobile)
If the app runs inside a subfolder — for example the XAMPP development install at http://localhost/Lead%20Hub/, where the root .htaccess bridge forwards requests into public/ — the subfolder is part of the path:
Local dev: http://localhost/Lead%20Hub/api/v1
http://localhost/Lead%20Hub/api/v1/mobile/
Which layout am I running? If https://your-domain.com/ opens the CRM directly, the document root is pointed at public/ and no subfolder appears in API URLs. If https://your-domain.com/somefolder/ is the CRM, include somefolder in every API URL.
Versions
The API version is read from config('leadhub.api_version'), set by the LEADHUB_API_VERSION environment variable (default v1). The canonical surface is /api/v1/*.
Two compatibility behaviours exist for convenience:
- Unversioned lead aliases —
GET/POST /api/leads(plus/api/leads/{id}) work exactly like/api/v1/leads. These exist because some no-code platforms (Pabbly Connect, older Zapier connectors, n8n HTTP nodes) do not follow 308 redirects onPOST. - Automatic 308 redirect — any other path under
/api/that does not match an explicit route is redirected to its/api/v1/equivalent, preserving the HTTP method and body. Unknown paths return a JSON 404 ({"status":"error","message":"Endpoint not found.","code":"NOT_FOUND"}).
Interactive documentation
The full OpenAPI 3.1.0 specification renders as Swagger UI — no login required:
/api/docs(canonical)/api/documentationand/api/swagger(aliases)/api/v1/docs(versioned alias)
The spec documents API-key scopes, the pagination envelope, and the outbound webhook events (plus a virtual x-webhooks endpoint).
Authentication at a glance
| Client | Header | Credential created in | Lifetime |
|---|---|---|---|
| REST API | Authorization: Bearer lh_... |
Admin panel → Settings → API Keys | Set per key (expires_at optional) |
| Mobile API | Authorization: Bearer <64-char token> |
Minted at POST /api/v1/mobile/login |
90 days, revoked on logout |
Detailed flows: Authentication. Error handling for both: Error codes & handling.
Rate limits
Rate limiting is enforced by middleware, per API key and per workspace:
| Limit | Source | Default |
|---|---|---|
| Per-key hourly limit | api_keys.rate_limit_per_hour (set on the key) |
1000 requests/hour |
| Workspace hourly ceiling | tenants.settings.api.rate_limit_per_hour, else a multiple of the per-key limit |
5× the per-key limit |
The multiplier comes from config('leadhub.api.tenant_rate_limit_multiplier') (LH_API_TENANT_RL_MULTIPLIER). Both are fixed hourly windows — the counter resets on the hour boundary.
Successful responses carry the per-key headers:
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset
When the per-key limit is exceeded the API returns 429 with those same headers plus Retry-After and Rate limit exceeded. Limit: N requests/hour. A workspace-level 429 instead carries the tenant-suffixed headers — X-RateLimit-Limit-Tenant / X-RateLimit-Remaining-Tenant / X-RateLimit-Reset-Tenant — plus Retry-After and Tenant rate limit exceeded. Limit: N requests/hour.
Other endpoints have their own, separate limiter:
| Endpoint group | Limit |
|---|---|
Inbound lead intake (/api/inbound/leads, /api/v1/inbound) — Bearer tenant API key + ?tenant={slug} |
120/min per IP |
Token webhook receiver (/api/webhook/...) |
120/min per IP |
Public form submit (/api/forms/{tenant}/{slug}/submit) |
60/min per IP |
Messaging webhooks (/api/messaging/...) |
120/min per IP |
| Commercial payment webhooks (`/api/webhooks/razorpay | cashfree`) |
Mobile login / OTP (/api/v1/mobile/login, otp/*) |
10/min per IP (plus per-identifier limits) |
Health check
GET /api/v1/health is public and never requires a key. It round-trips the database, cache and storage — each reported as a boolean in checks — and reports the configured queue connection name in queue:
{
"status": "ok",
"checks": { "db": true, "cache": true, "storage": true },
"queue": "database",
"version": "2.0.12",
"timestamp": "2026-10-06T09:00:00+05:30"
}
status is ok or degraded; a degraded install returns HTTP 503. Failures are also written to the Laravel log (healthcheck.db.failed, and equivalents). The admin panel exposes the same probe at /up. Point your uptime monitor at this URL — see Monitoring & alerts.
Response shapes
- REST API success:
{"data": ...}, wrapped withmeta(pagination:current_page,last_page,per_page,total,from,to) andlinks(first/last/prev/next/self) for lists. Errors:{"status":"error","message":"...","code":"..."}. - Mobile API success:
{"data": ...}(plusmetafor paginated lists). Errors:{"error":{"message":"...","code":"..."}}.
Common mistakes
- Using an API key against
/api/v1/mobile/*— device tokens and API keys are not interchangeable (and vice versa). - Forgetting the subfolder when the install is not document-rooted at
public/— the request 404s at the web-server level before Laravel sees it. - Letting a key's
expires_atpass silently — revoked/expired keys return 401Invalid or expired API key.
Troubleshooting
| Symptom | Likely cause |
|---|---|
401 API key required |
Missing or malformed Authorization header |
403 Insufficient scope |
The key was created without the scope the endpoint needs |
403 This workspace is not active |
The tenant's subscription is suspended or restricted |
| 429 on every call | Per-key limit too low, or a runaway loop shared across the workspace ceiling |
404 Endpoint not found. JSON |
Path is wrong (and not merely unversioned) |
Technical details
- Routes:
routes/api.php(REST, mobile, public webhook endpoints). - REST middleware:
App\Http\Middleware\ApiKeyAuthentication. - Mobile middleware:
App\Http\Middleware\DeviceAuthenticate. - Error payloads:
App\Http\Responses\ApiErrorResponse. - Pagination resource:
App\Http\Resources\Api\ApiResource. - Health:
App\Http\Controllers\Api\HealthController. - OpenAPI viewer:
App\Http\Controllers\Api\ApiDocsController.
Related guides
Was this page helpful?
Thanks — noted. ✦