API
Overview
Laravel CRM ships with a JSON REST API for partner developers and external integrations. The API is versioned, authenticated with Laravel Sanctum personal access tokens, and mounted at /crm/api/v2.
- Base URL:
https://your-app.test/crm/api/v2 - Content type:
application/json - Authentication: Sanctum bearer tokens
- Current version:
v2
The API exposes 8 resourceful entities with full CRUD (list, create, show, update, soft-delete), plus 3 auth endpoints for issuing, inspecting, and revoking tokens.
Entity references
Each entity has its own reference page covering endpoints, list parameters, JSON shape, and writable fields:
Requirements
- Laravel CRM installed and migrated.
- Laravel Sanctum installed in the host application.
- A CRM user with
crm_accessgranted, to issue tokens against.
Installation
The API ships with the package — you only need to wire Sanctum into the host application.
1. Publish and run Sanctum migrations
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
This creates the personal_access_tokens table.
2. Add HasApiTokens to your User model
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// ...
}
3. Verify the API routes are registered
php artisan route:list --path=crm/api
You should see 8 resourceful entities (5 verbs each), plus the 3 auth routes.
Authentication
The API uses Sanctum personal access tokens. Tokens can be issued through the API itself, or via an artisan command for ops use.
Issue a token via the API
POST /crm/api/v2/auth/token
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret",
"device_name": "Mobile App"
}
Response (201 Created):
{
"token": "1|abcdef1234...",
"user": {
"id": 1,
"name": "Jane Doe",
"email": "user@example.com"
}
}
- Returns
422on bad credentials, an unknown email, or when the user lackscrm_access. The response is intentionally indistinguishable across these cases to avoid leaking which emails belong to real users. device_nameis optional and defaults to the request'sUser-Agentorapi-token.- The plaintext token is shown once — store it securely.
Issue a token via artisan
php artisan laravel-crm:api-token user@example.com --name="Mobile App"
The plaintext token is printed once. The command exits non-zero if the user does not exist or lacks crm_access.
Making authenticated requests
Pass the token in the Authorization header:
GET /crm/api/v2/leads HTTP/1.1
Authorization: Bearer 1|abcdef1234...
Accept: application/json
Inspecting the current user
GET /crm/api/v2/auth/me
Response (200 OK):
{
"user": {
"id": 1,
"name": "Jane Doe",
"email": "user@example.com"
}
}
Revoking the current token
DELETE /crm/api/v2/auth/token
Returns 204 No Content and deletes the personal access token used to authenticate the request.
Headers
| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <token> |
Yes (except POST /auth/token) |
Sanctum personal access token. |
Accept: application/json |
Recommended | Forces JSON responses (the laravel-crm.api.json middleware sets this automatically when missing). |
Content-Type: application/json |
Yes (for POST / PUT) |
Request body is JSON. |
X-Team-ID: <team-id> |
Optional | Overrides the authenticated user's active team for the request. Must be a team the user belongs to; otherwise the API returns 403. Only relevant when laravel-crm.teams=true. |
Multi-tenancy notes
When the host app runs in teams mode (config('laravel-crm.teams', true)):
- Without
X-Team-ID, requests are scoped to the user'scurrent_team_id. - With
X-Team-ID, list / store / update / delete endpoints run in the context of that team. GET /{resource}/{uuid}resolves the route-bound model using the user's default current team, because Laravel'sSubstituteBindingsmiddleware runs before the team-context middleware. Use the list endpoints (filtered byX-Team-ID) to discover the correct UUIDs for the active team.
Endpoint summary
Auth
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/crm/api/v2/auth/token |
Public | Issue a personal access token. |
GET |
/crm/api/v2/auth/me |
Bearer | Return the authenticated user. |
DELETE |
/crm/api/v2/auth/token |
Bearer | Revoke the current token. |
Entities
All entity endpoints follow the same RESTful shape:
| Verb | Path | Action |
|---|---|---|
GET |
/{resource} |
List (paginated). |
POST |
/{resource} |
Create. |
GET |
/{resource}/{uuid} |
Show. |
PUT |
/{resource}/{uuid} |
Update. |
DELETE |
/{resource}/{uuid} |
Soft-delete. |
{uuid} is the entity's external_id (UUID), exposed as id in JSON responses.
| Resource | Path | Reference |
|---|---|---|
| Lead | /crm/api/v2/leads |
Leads |
| Product | /crm/api/v2/products |
Products |
| Organization | /crm/api/v2/organizations |
Organizations |
| Person | /crm/api/v2/people |
People |
| Deal | /crm/api/v2/deals |
Deals |
| Quote | /crm/api/v2/quotes |
Quotes |
| Order | /crm/api/v2/orders |
Orders |
| Invoice | /crm/api/v2/invoices |
Invoices |
Conventions
- IDs are UUIDs. The JSON
idis always the entity'sexternal_id. Integer primary keys are never exposed. - Foreign keys are also UUIDs for CRM entities — e.g.
person_id,organization_id,lead_id,pipeline_stage_id,lead_source_id,product_category_id, and everylabels[]entry must be a UUID (the related row'sexternal_id). - Some lookups are integer IDs. Host-app users (
user_owner_id) and a handful of small reference tables that aren't UUID-backed use integer primary keys:tax_rate_id,organization_type_id,industry_id,timezone_id. - Human-readable IDs are returned, not accepted. Entities also expose a sequential identifier (
lead_id,deal_id,quote_id,order_id,invoice_id, e.g.L1001,D1001) in the response. These are read-only and assigned automatically. - Money is dollars in JSON; cents in storage. All amount / price / total / subtotal / discount / tax / adjustments fields are sent and returned as decimal dollars (e.g.
1500.50). The package converts to integer cents on write. - Timestamps are ISO-8601 and must include a timezone offset or the
ZUTC suffix on input — e.g.2026-07-15T10:00:00+00:00or2026-07-15T10:00:00Z. Other formats are rejected with a422. - Pagination:
?per_page=N(1–100, default 25). Responses use Laravel's standard pagination envelope (data,meta,links). - Sorting:
?sort=fieldascending;?sort=-fielddescending. Unknown columns are silently ignored. Default sort is-created_at. - Soft deletes:
DELETEreturns204and soft-deletes the row. SubsequentGETs return404.
Errors
The API returns standard Laravel error envelopes.
422 Unprocessable Entity (validation)
{
"message": "The given data was invalid.",
"errors": {
"title": ["The title field is required."],
"person_id": ["The selected person_id is invalid."]
}
}
401 Unauthorized
{ "message": "Unauthenticated." }
403 Forbidden
{ "message": "This action is unauthorized." }
For the X-Team-ID non-member case:
{ "message": "You are not a member of the requested team." }
404 Not Found
Returned when a UUID does not resolve to a model (or it has been soft-deleted).
429 Too Many Requests
Returned when the rate limit is exceeded. Standard Laravel rate-limit headers are included: X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After.
Rate limits
The API enforces a single named rate limiter, laravel-crm-api:
| Caller | Limit |
|---|---|
| Authenticated (Sanctum) | 60 requests / minute / user |
| Unauthenticated | 30 requests / minute / IP |
Exceeding the limit returns 429 Too Many Requests with Retry-After in seconds.
Worked example
Issue a token, list leads, create a lead, then revoke the token.
# 1. Issue a token
curl -s -X POST https://example.test/crm/api/v2/auth/token \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secret","device_name":"curl"}' \
| jq .
# 2. List leads
TOKEN="1|abcdef..."
curl -s https://example.test/crm/api/v2/leads \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" \
| jq .
# 3. Create a lead
curl -s -X POST https://example.test/crm/api/v2/leads \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "New enterprise lead",
"amount": 12500.00,
"currency": "USD",
"expected_close": "2026-09-30T00:00:00Z"
}' \
| jq .
# 4. Revoke the token
curl -s -X DELETE https://example.test/crm/api/v2/auth/token \
-H "Authorization: Bearer $TOKEN" \
-i