Skip to content

Latest commit

 

History

History
308 lines (227 loc) · 9.84 KB

File metadata and controls

308 lines (227 loc) · 9.84 KB

Laravel CRM REST API (v2)

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

Install

The API ships with the package; you only need to wire Sanctum into the host application.

  1. Publish and run Sanctum migrations (creates the personal_access_tokens table):

    php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
    php artisan migrate
  2. Add HasApiTokens to your host User model:

    use Laravel\Sanctum\HasApiTokens;
    
    class User extends Authenticatable
    {
        use HasApiTokens;
        // ...
    }
  3. 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).

  4. 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.


Authentication

The API uses Sanctum personal access tokens. There are two ways to obtain one:

Option A — Issue 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):

{
  "token": "1|abcdef1234...",
  "user": {
    "id": 1,
    "name": "Jane Doe",
    "email": "user@example.com"
  }
}
  • Returns 422 on bad credentials, an unknown email, or when the user lacks crm_access. The response is intentionally indistinguishable across these cases to avoid leaking which emails belong to real users.
  • device_name is optional; defaults to the request's User-Agent or api-token.

Option B — Issue via artisan (ops use)

php artisan laravel-crm:api-token user@example.com --name="Mobile App"

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 and revoking the current token

GET    /crm/api/v2/auth/me      → 200 { id, name, email }
DELETE /crm/api/v2/auth/token   → 204

Headers

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.

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's current_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's SubstituteBindings runs before SetApiTeamContext. Use the list endpoints (filtered by X-Team-ID) to discover the correct UUIDs for the active team.

Endpoint matrix

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.

Auth

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.

Entities

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[]

Conventions across all entity endpoints

  • IDs are UUIDs. The JSON id is always the entity's external_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 (Z UTC 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=field ascending; ?sort=-field descending. 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: DELETE returns 204 and soft-deletes the row. Subsequent GETs return 404.

Nested line items (Quote / Order / Invoice)

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 id on every line in a PUT. Existing lines not matched in the payload are deleted.

Error format

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 has been soft-deleted).

{ "message": "..." }

429 Too Many Requests

Returned when the rate limit is exceeded. Standard Laravel headers include 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