A JSON REST API for partner developers integrating with venturedrake/laravel-crm. All requests
are authenticated with Laravel Sanctum personal access tokens.
- Base URL:
<your-app>/crm/api/v2 - Content type:
application/json - Auth: Sanctum bearer tokens
The API ships with the package; you only need to wire Sanctum into the host application.
-
Publish and run Sanctum migrations (creates the
personal_access_tokenstable):php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate -
Add
HasApiTokensto your hostUsermodel:use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens; // ... }
-
Confirm the API routes load. After installing the package and running migrations, run:
php artisan route:list --path=crm/api
You should see 8 resourceful entities (
leads,products,organizations,people,deals,quotes,orders,invoices) × 5 verbs each, plus 3 auth routes (POST auth/token,GET auth/me,DELETE auth/token). -
Issue a token via the ops command (no controller round-trip required):
php artisan laravel-crm:api-token user@example.com --name="Mobile App"The plaintext token is printed once — copy it and store securely. The command exits non-zero if the user does not exist or lacks
crm_access.
The API uses Sanctum personal access tokens. There are two ways to obtain one:
POST /crm/api/v2/auth/token
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret",
"device_name": "Mobile App"
}Response (201):
{
"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; defaults to the request'sUser-Agentorapi-token.
php artisan laravel-crm:api-token user@example.com --name="Mobile App"Pass the token in the Authorization header:
GET /crm/api/v2/leads HTTP/1.1
Authorization: Bearer 1|abcdef1234...
Accept: application/jsonGET /crm/api/v2/auth/me → 200 { id, name, email }
DELETE /crm/api/v2/auth/token → 204| Header | Required | Purpose |
|---|---|---|
Authorization: Bearer <token> |
Yes (except POST /auth/token) |
Sanctum personal access token. |
Accept: application/json |
Recommended | The laravel-crm.api.json middleware forces JSON responses; this header is set automatically by Laravel 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 this request. Must be a team the user belongs to (per User::allTeams()); otherwise the API returns 403. Only relevant when laravel-crm.teams=true. |
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, the request runs in the context of that team for list/store/update/delete endpoints.GET /{resource}/{uuid}resolves the route-bound model using the user's default current team because Laravel'sSubstituteBindingsruns beforeSetApiTeamContext. Use the list endpoints (filtered byX-Team-ID) to discover the correct UUIDs for the active team.
All entity endpoints follow the same shape:
| Verb | Path | Action |
|---|---|---|
GET |
/{resource} |
List (paginated). |
POST |
/{resource} |
Create. |
GET |
/{resource}/{uuid} |
Show. |
PUT |
/{resource}/{uuid} |
Update. |
DELETE |
/{resource}/{uuid} |
Soft-delete. |
The {uuid} in URIs is the entity's external_id (UUID), exposed as id in JSON responses.
| Method | Path | Notes |
|---|---|---|
POST |
/crm/api/v2/auth/token |
Issue a token. Public (no auth required). |
GET |
/crm/api/v2/auth/me |
Return the authenticated user. |
DELETE |
/crm/api/v2/auth/token |
Revoke the current token. |
| Resource | Path | Notable fields |
|---|---|---|
| Lead | /crm/api/v2/leads |
title, description, amount, currency, expected_close, person_id, organization_id, lead_source_id, pipeline_stage_id, labels[], user_owner_id |
| Product | /crm/api/v2/products |
name, code, description, unit_price, currency, tax_rate, tax_rate_id, product_category_id, active, user_owner_id |
| Organization | /crm/api/v2/organizations |
name, website, email, phone, annual_revenue, total_money_raised, number_of_employees, industry_id, organization_type_id, timezone_id, labels[], user_owner_id |
| Person | /crm/api/v2/people |
first_name, last_name, gender, birthday, description, organization_id, labels[], user_owner_id |
| Deal | /crm/api/v2/deals |
title, description, amount, currency, expected_close, lead_id, person_id, organization_id, pipeline_stage_id, labels[], user_owner_id |
| Quote | /crm/api/v2/quotes |
title, description, issue_at, expire_at, currency, sub_total, discount, tax, adjustment, total, person_id, organization_id, labels[], line_items[] |
| Order | /crm/api/v2/orders |
description, currency, sub_total, discount, tax, adjustment, total, person_id, organization_id, labels[], line_items[] |
| Invoice | /crm/api/v2/invoices |
reference, issue_date, due_date, currency, sub_total, discount, tax, adjustment, total, amount_due, amount_paid, person_id, organization_id, labels[], line_items[] |
- IDs are UUIDs. The JSON
idis always the entity'sexternal_id. Integer primary keys are never exposed. Lookup tables (lead source, pipeline stage, industry, etc.) accept integer IDs. - Money is dollars in JSON; cents in storage. All amount/price/total fields are sent and
returned as decimal dollars (e.g.
1500.50). The package's model mutators multiply by 100 on write. - Timestamps are ISO-8601 with timezone offset, e.g.
2026-07-15T10:00:00+00:00(ZUTC suffix is also accepted on input). - 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. - Filtering:
?user_owner_id=<int>(and?active=on products) is supported on list endpoints. Other filters are documented per-resource as needed. - Soft deletes:
DELETEreturns204and soft-deletes the row. SubsequentGETs return404.
The line_items array is accepted on POST and PUT. Each item has the following shape:
{
"id": "8f1a...optional-uuid-for-existing-line",
"product_id": "44d4...product-uuid",
"quantity": 2,
"unit_price": 100.00,
"amount": 200.00,
"comments": "Optional notes"
}- Create: omit
id. A new line is inserted. - Update in place: include the existing line's
id(UUID). The line is updated. - Replace lines: omit
idon every line in aPUT. Existing lines not matched in the payload are deleted.
The API returns standard Laravel error envelopes:
{
"message": "The given data was invalid.",
"errors": {
"title": ["The title field is required."],
"person_id": ["The selected person_id is invalid."]
}
}{ "message": "Unauthenticated." }{ "message": "This action is unauthorized." }For the X-Team-ID non-member case:
{ "message": "You are not a member of the requested team." }Returned when a UUID does not resolve to a model (or has been soft-deleted).
{ "message": "..." }Returned when the rate limit is exceeded. Standard Laravel headers include
X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After.
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.
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