Authentication
How REST API keys and mobile device tokens are created, sent, scoped, expired and revoked — with working curl examples.
Zodiac Tech CRM has two independent credential systems:
- API keys — long-lived, per-workspace, scope-limited. For servers and no-code tools hitting
/api/v1/*. - Device tokens — per-user, minted by mobile login, 90-day life. For the Android app on
/api/v1/mobile/*.
This page covers both end to end.
Who this is for
- Admin — creating, scoping and revoking API keys for the workspace.
- Developer — wiring a server integration or diagnosing 401/403 responses.
- Super-admin — auditing which keys exist and where the limits sit.
Prerequisites: access to the admin panel at /admin with permission to manage API keys (api_keys permissions), and an install reachable at your base URL.
Part 1 — REST API keys
Step 1: Create a key
- Log in to the tenant admin panel (
https://your-domain.com/admin). - Open Settings → API Keys and click Create.
- Fill in:
- Name — a label for the integration (max 100 characters), e.g.
Zapier – Facebook leads. - Scopes — tick exactly what the integration needs (see the scope table below).
- Rate limit per hour — default
1000, allowed range1–100000. - Expires at — optional. Leave empty for a non-expiring key.
- Name — a label for the integration (max 100 characters), e.g.
- Save. The raw key is shown once (
lh_+ 40 random characters). Copy it immediately — it is never displayed again.
Under the hood the key is stored as a SHA-256 hash; the visible prefix (first 8 characters) identifies it later in the list. You can revoke a key at any time from the same screen, and delete it if no longer needed.
Treat the key like a password Anyone holding the key can act as your workspace within its scopes. Store it in your integration's secret store, never in client-side code or a public repository.
Step 2: Choose scopes
Each route requires a specific scope (enforced per route via the api.key:<scope> middleware). Requesting more scopes than needed is the most common mistake.
| Scope | Grants |
|---|---|
read:leads / write:leads / delete:leads |
List & view / create & update / delete leads, lead tags |
read:pipelines / write:pipelines |
Pipelines and their stages |
read:tags / write:tags |
Tags |
read:users / write:users / delete:users |
Workspace users |
read:forms / write:forms / delete:forms |
Forms (write also covers submitting entries) |
read:automations / write:automations / delete:automations |
Automations |
read:integrations / write:integrations |
Integration connections (write covers create/update/delete) |
Step 3: Send the key
Every REST request carries the key in the standard Bearer position:
Authorization: Bearer lh_yourKeyHere
Accept: application/json
# List the 25 most recent leads
curl -s "https://crm.zodiactechsoft.in/api/v1/leads?per_page=25" \
-H "Authorization: Bearer lh_yourKeyHere" \
-H "Accept: application/json"
A successful response uses the standard envelope (data + pagination meta/links):
{
"data": [ { "id": 101, "first_name": "Aarav", "...": "..." } ],
"meta": { "current_page": 1, "last_page": 4, "per_page": 25, "total": 92, "from": 1, "to": 25 },
"links": { "first": "...", "last": "...", "prev": null, "next": "..." }
}
Step 4: Handle key failure
| Response | Meaning |
|---|---|
401 API key required. Provide Authorization: Bearer <key> header. |
Header missing/malformed |
401 Invalid or expired API key. |
Key revoked, expired, or hash mismatch |
403 Insufficient scope. Required: read:leads |
Key lacks the scope for that route |
403 This workspace is not active. Renew or reactivate the subscription to use the API. |
Tenant is inactive |
Subfolder installs and curl
When the CRM lives in a subfolder (for example the XAMPP dev install), the subfolder is part of the URL and must be URL-encoded or quoted:
# Local XAMPP install — note the encoded space in "Lead%20Hub"
curl -s "http://localhost/Lead%20Hub/api/v1/leads" \
-H "Authorization: Bearer lh_yourKeyHere" \
-H "Accept: application/json"
In a browser or tool that handles redirects (Postman, Insomnia), keep the URL exactly as above. If a tool strips the Authorization header on redirects, call the versioned /api/v1/... path directly to avoid the 308 hop.
Apache and the Authorization header The shipped root .htaccess re-exports the Authorization header into the request environment (E=HTTP_AUTHORIZATION), which is what makes Bearer auth work on Apache CGI/FastCGI setups. On a standard install (document root pointed at public/) the same rule lives in public/.htaccess.
Part 2 — Mobile device tokens
Device tokens are minted by the mobile API itself and belong to an individual user, not the workspace.
Login (password)
curl -s -X POST "https://crm.zodiactechsoft.in/api/v1/mobile/login" \
-H "Content-Type: application/json" \
-d '{
"identifier": "agent@example.com",
"password": "your-password",
"device_uuid": "a-unique-device-id"
}'
Notes:
identifieraccepts the user's e-mail or phone number. A legacyemailfield is still accepted for older app builds.device_uuidis required and ties the token to a physical device (re-registering the same UUID rebinds it).- The route is throttled to 10 requests/minute per IP.
The response contains the token and its expiry:
{
"data": {
"token": "64-character-random-token",
"expires_at": "2027-01-03T09:00:00+05:30",
"user": { "id": 7, "name": "Priya", "email": "agent@example.com", "phone": "+91...", "role": "member" },
"tenant": { "id": 12, "name": "Acme Realty", "slug": "acme", "timezone": "Asia/Kolkata" },
"device": { "id": 3, "...": "..." }
}
}
OTP login
If the workspace enables mobile OTP login:
POST /api/v1/mobile/otp/startwith the identifier (phone or email) sends a code. Limits: 3 per identifier and 10 per IP per 10 minutes.POST /api/v1/mobile/otp/verifywith the identifier, code anddevice_uuidreturns the same login payload as above (Firebase builds sendid_tokeninstead ofcode).
GET /api/v1/mobile/auth-config tells the app which methods the platform defaults support before login; per-workspace overrides resolve at login and OTP.
Using the token
curl -s "https://crm.zodiactechsoft.in/api/v1/mobile/home" \
-H "Authorization: Bearer <device-token>" \
-H "Accept: application/json"
DeviceAuthenticate resolves the token's user and workspace; every query is scoped to that workspace via the tenant global scope.
Token lifecycle
| Event | Effect |
|---|---|
| Login on the same device UUID | Previous token is replaced; a fresh 90-day token is issued |
| Login on a new device | New token + device record |
POST /api/v1/mobile/logout |
Token is revoked (revoked_at set) immediately |
| Token older than 90 days | Expired — the app must log in again |
| User or workspace deactivated | Requests return 401/403; see Error codes & handling |
Mobile error responses
{ "error": { "message": "Invalid email or password.", "code": "INVALID_CREDENTIALS" } }
Common codes: INVALID_CREDENTIALS (401), ACCOUNT_SUSPENDED (403), NO_WORKSPACE (403), WORKSPACE_INACTIVE (403), ACCOUNT_LOCKED (429 after 5 failed password attempts), RATE_LIMITED (429 — otp/start includes retry_after; login/OTP 429s come from the route throttle), otp_invalid (401).
Common mistakes
- Creating a key with all scopes "just in case" — a later breach now reaches every resource. Grant the minimum.
- Pasting the key into a browser address bar — it must be an
Authorizationheader, never a query string. - Reusing one device token across several agents — tokens are per user; each phone logs in for itself.
- Forgetting to store the raw API key at creation — it cannot be retrieved later; revoke and recreate instead.
Troubleshooting
| Symptom | Fix |
|---|---|
| 401 on every request | Check header spelling (Bearer prefix with a space) and that the key was not revoked |
403 Insufficient scope |
Edit the key and add the missing scope |
| Mobile 401 after weeks of use | Token expired at 90 days — log in again |
Mobile NO_WORKSPACE (403) |
The user has no active workspace membership; fix in Super Admin → Users |
| Login works but lists are empty | The user's workspace has no data visible to that role — not an auth problem |
Technical details
- REST auth:
App\Http\Middleware\ApiKeyAuthentication— resolvestenant_idfrom the key record, bindscurrent_tenant, enforces scopes and both hourly limits. - Key model:
App\Models\ApiKey(ALL_SCOPESconstant, SHA-256 lookup,lh_prefix generation, revoke/expire flags). - Admin UI:
App\Filament\Resources\ApiKeyResource(tenant Settings → Advanced; plan-gated by theapifeature and themax_api_keysquota). - Mobile auth:
App\Http\Controllers\Api\Mobile\AuthController(TOKEN_TTL_DAYS = 90, OTP attempt cap 5) andApp\Http\Middleware\DeviceAuthenticate.
Related guides
Was this page helpful?
Thanks — noted. ✦