API Reference

MyDesk Core API Reference



Complete reference for all endpoints in routes/api.php.

Live docs: http://localhost:8000/docs/api-reference

---

Base URL



http://localhost:8000/api


Replace with your deployment URL in production (e.g. https://api.mydesk.com/api).

---

Authentication



Every request requires a Core Token (app-to-app secret).

Core Token (required on all routes)



| Header / field | Example |
|----------------|---------|
| X-Core-Token | your_core_token |
| Core-Token | your_core_token |
| Body/query core_token | your_core_token |

Set in .env as CORE_TOKEN.

API Token (protected routes only)



Protected routes also require a Vendor or Admin API token:

| Header | Example |
|--------|---------|
| Authorization | Bearer {api_token} |
| X-API-Token | {api_token} |

| Caller | Token source |
|--------|----------------|
| Vendor | vendors.api_token (issued on login / email verify) |
| Admin | Plaintext token from POST /admin/login (stored hashed in admins.remember_token) |

> Important: After vendor login with organizations, data.session.core_access_token is the token for Core API (/api/*). Top-level data.api_token is the workspace token from the organization database โ€” use it for the tenant app, not core admin routes.

Auth levels



| Level | Middleware | Endpoints |
|-------|------------|-----------|
| Public | core.token | Login, register, invitations, org-stats, etc. |
| Protected | core.token + auth.api | Organizations, vendors, plans, invoices, roles, admins |

---

Response formats



Most endpoints use the MessageService envelope:

{
"status": 200,
"success": true,
"message": "Human-readable message",
"data": {}
}


Paginated lists add:

{
"payload": {
"pagination": {
"page": 1,
"last_page": 5,
"items_per_page": "10",
"total": 50,
"from": 1,
"to": 10,
"next_page_url": "...",
"prev_page_url": null,
"links": []
}
}
}


Legacy Admin / Role endpoints may return "status": "200" (string) and omit success.

Validation error (422):

{
"status": 422,
"success": false,
"message": "Validation failed",
"errors": {
"email": ["The email field is required."]
}
}


---

Quick endpoint index



| # | Method | Path | Auth |
|---|--------|------|------|
| 1 | POST | /admin/login | Public |
| 2 | POST | /admin/verify-token | Public |
| 3 | POST | /login | Public |
| 4 | POST | /otp-verify | Public |
| 5 | POST | /verify_token | Public |
| 6 | POST | /core-login | Public |
| 7 | POST | /register | Public |
| 8 | POST | /vendor/send-reset-password | Public |
| 9 | POST | /reset-password | Public |
| 10 | GET | /verify-email | Public |
| 11 | POST | /resend-verification | Public (vendor email) |
| 12 | POST | /invite-vendor | Public |
| 13 | POST | /vendor-invitations/accept | Public |
| 14 | POST | /notify | Public |
| 15 | POST | /org-stats | Public |
| 16 | GET | /roles | Protected |
| 17 | POST | /role | Protected |
| 18 | GET | /role/{id} | Protected |
| 19 | PUT | /role/{id} | Protected |
| 20 | DELETE | /role/{id} | Protected |
| 21 | POST | /admin/{id}/assign-role | Protected |
| 22 | GET | /admins | Protected |
| 23 | POST | /admin | Protected |
| 24 | GET | /admin/{id} | Protected |
| 25 | PUT | /admin/{id} | Protected |
| 26 | DELETE | /admin/{id} | Protected |
| 27 | POST | /admin/send-reset-link | Protected |
| 28 | POST | /admin/password/reset/{token} | Public |
| 29 | GET | /organizations | Protected |
| 30 | POST | /organization | Protected |
| 31 | POST | /organization/update/{orgcode} | Protected |
| 32 | DELETE | /organization/{orgcode} | Protected |
| 33 | POST | /organization/action/{orgcode} | Protected |
| 34 | POST | /send-organization-invitations | Protected |
| 35 | POST | /organization/team-members | Protected |
| 36 | GET | /organization/{orgcode} | Protected |
| 37 | POST | /check-organization-access | Protected |
| 38 | POST | /organization/{orgcode}/upload-logo | Protected |
| 39 | POST | /organization/{orgcode}/upload-file | Protected |
| 40 | GET | /organization/{orgcode}/files | Protected |
| 41 | DELETE | /organization/{orgcode}/file | Protected |
| 42 | GET | /organizations-list | Protected |
| 43 | GET | /vendors | Protected |
| 44 | GET | /vendor/{id} | Protected |
| 45 | PUT | /vendor/{id} | Protected |
| 46 | DELETE | /vendor/{id} | Protected |
| 47 | POST | /vendor/activate/{id} | Protected |
| 48 | POST | /vendor/deactivate/{id} | Protected |
| 49 | POST | /vendor/delete/{id} | Protected |
| 50 | POST | /vendor/delete_permanently/{id} | Protected |
| 51 | GET | /plans | Protected |
| 52 | GET | /plan/{id} | Protected |
| 53 | POST | /plan | Protected (admin) |
| 54 | PUT | /plan/{id} | Protected (admin) |
| 55 | DELETE | /plan/{id} | Protected (admin) |
| 56 | POST | /vendor/{vendorId}/assign-plan | Protected (admin) |
| 57 | GET | /vendor/{vendorId}/plan | Protected |
| 58 | GET | /invoices | Protected |
| 59 | GET | /invoice/{id} | Protected |
| 60 | POST | /invoice | Protected (admin) |
| 61 | POST | /invoices/generate | Protected (admin) |
| 62 | PUT | /invoice/{id} | Protected (admin) |
| 63 | POST | /invoice/{id}/mark-paid | Protected (admin) |
| 64 | POST | /invoice/{id}/cancel | Protected (admin) |
| 65 | POST | /invoice/{id}/void | Protected (admin) |
| 66 | DELETE | /invoice/{id} | Protected (admin) |
| 67 | GET | /vendor/{vendorId}/invoices | Protected |

---

Authentication Endpoints



1. Admin Login



POST /admin/login ยท Public

Request body:

{
"email": "admin@mydesk.com",
"password": "Admin@123456"
}


Success (200):

{
"status": "200",
"message": "Login successful",
"data": {
"token": "plaintext_admin_token_use_as_bearer"
}
}


cURL:

curl -X POST http://localhost:8000/api/admin/login \
-H "Content-Type: application/json" \
-H "X-Core-Token: your_core_token" \
-d '{"email":"admin@mydesk.com","password":"Admin@123456"}'


---

2. Admin Verify Token



POST /admin/verify-token ยท Public

Request body:

{
"token": "plaintext_admin_token"
}


Success (200):

{
"status": "200",
"message": "Token is valid",
"data": {
"name": "Admin",
"email": "admin@mydesk.com",
"role_id": 1,
"status": 1
}
}


---

3. Vendor Login



POST /login ยท Public

Request body (password):

{
"email": "vendor@example.com",
"action": "password",
"password": "SecurePass1!"
}


Request body (OTP request):

{
"email": "vendor@example.com",
"action": "otp"
}


Success โ€” password login with organizations (200):

{
"status": 200,
"success": true,
"message": "Login successful",
"data": {
"user": {
"id": 1,
"email": "vendor@example.com",
"name": "Jane Doe",
"is_active": true
},
"session": {
"core_access_token": "core_api_token_for_protected_routes",
"access_token": "workspace_token_from_org_db",
"token_type": "Bearer",
"organization_code": "1588571752",
"database": "mydesk_1588571752"
},
"api_token": "workspace_token_from_org_db",
"organizations": [
{
"id": 1,
"code": "1588571752",
"name": "Acme Corp",
"status": "ready",
"api_token": "workspace_token_from_org_db",
"database": "mydesk_1588571752"
}
],
"meta": {
"total_organizations": 1,
"ready_organizations": 1
}
}
}


Success โ€” password login, no organization yet (200):

{
"status": 200,
"success": true,
"message": "Login successful! Let's start to setup your first organization.",
"data": {
"user": { "id": 1, "email": "vendor@example.com", "name": null, "is_active": true },
"session": {
"access_token": "core_api_token",
"token_type": "Bearer",
"scope": "core"
},
"api_token": "core_api_token",
"organizations": []
},
"requires_organization": true,
"action": "create_organization"
}


Success โ€” OTP requested (200):

{
"status": 200,
"success": true,
"message": "OTP has been sent to your registered email address.",
"action": "verify_otp",
"expires_in": "10 minutes"
}


Errors: 404 email not found ยท 401 inactive / suspended / wrong password

---

4. OTP Verify



POST /otp-verify ยท Public

Request body:

{
"email": "vendor@example.com",
"otp": "123456"
}


Success (200): Same shape as password login (completeAuthenticatedLogin).

Errors: 401 invalid or expired OTP

---

5. Vendor Token Verify



POST /verify_token ยท Public

Request body:

{
"api_token": "vendor_core_api_token"
}


Success (200):

{
"status": 200,
"success": true,
"message": "Token is valid",
"data": {
"full_name": "Jane Doe",
"avatar": null,
"email": "vendor@example.com"
}
}


---

6. Core Login



POST /core-login ยท Public

Alternative login that issues core + workspace session tokens (no OTP).

Request body:

{
"email": "vendor@example.com",
"password": "SecurePass1!"
}


Success (200):

{
"status": 200,
"success": true,
"message": "Success",
"data": {
"user": { "id": 1, "email": "vendor@example.com", "name": "Jane Doe" },
"session": {
"core_access_token": "core_api_token",
"access_token": "workspace_token",
"token_type": "Bearer",
"organization_code": "1588571752",
"database": "mydesk_1588571752"
},
"api_token": "workspace_token",
"organizations": [],
"meta": {}
}
}


---

Vendor Registration & Account



7. Register Vendor



POST /register ยท Public

Request body:

{
"email": "newvendor@example.com",
"password": "SecurePass1!",
"password_confirmation": "SecurePass1!"
}


Password rules: min 8, mixed case, numbers, symbols, uncompromised.

Success (200):

{
"status": 200,
"success": true,
"message": "Registration successful! Please check your email for verification.",
"data": {
"email": "newvendor@example.com"
}
}


New vendors are auto-assigned the Free plan.

---



POST /vendor/send-reset-password ยท Public

Request body:

{
"email": "vendor@example.com"
}


Success (200):

{
"status": 200,
"success": true,
"message": "Password reset link sent successfully to your email..."
}


---

9. Reset Password



POST /reset-password ยท Public

Request body:

{
"token": "reset_token_from_email",
"password": "NewSecurePass1!",
"password_confirmation": "NewSecurePass1!"
}


Success (200):

{
"status": 200,
"success": true,
"message": "Password reset successfully!"
}


---

10. Verify Email



GET /verify-email?token={verification_token} ยท Public

Success (200):

{
"status": 200,
"success": true,
"message": "Congratulations! Your account has been successfully verified.",
"data": {
"api_token": "new_core_api_token",
"user": {
"id": 1,
"full_name": null,
"email": "vendor@example.com",
"is_active": true
}
},
"requires_organization": true,
"next_step": "create_organization"
}


---

11. Resend Email Verification



POST /resend-verification ยท Public

> Note: This route is registered twice in api.php. The public vendor resend (registered first) takes precedence. The protected organization resend on the same path is unreachable.

Request body:

{
"email": "vendor@example.com"
}


Success (200):

{
"status": 200,
"success": true,
"message": "Verification email has been sent successfully! Please check your inbox.",
"data": {
"email": "vendor@example.com"
}
}


---

Vendor Management (Protected)



12. List Vendors



GET /vendors ยท Protected

Query parameters:

| Param | Type | Description |
|-------|------|-------------|
| items_per_page | int | Default 10 |
| filter_status | string | 1, active, true = active only |
| filter_organization_id | int | Vendors linked to org |
| search | string | Search full_name or email |
| sort | string | id, name, full_name, email, created_at |
| order | string | asc or desc |

Example: GET /api/vendors?items_per_page=20&filter_status=active&search=jane

Success (200):

{
"status": 200,
"success": true,
"message": "Records listed successfully!",
"data": [
{
"id": 1,
"email": "vendor@example.com",
"full_name": "Jane Doe",
"is_active": true,
"plan_id": 1,
"billing_day": 1,
"created_at": "2026-01-15T10:00:00.000000Z"
}
],
"payload": { "pagination": { "page": 1, "total": 1 } }
}


---

13. Get Vendor



GET /vendor/{id} ยท Protected

Success (200):

{
"status": 200,
"success": true,
"message": "Record retrieved successfully!",
"data": {
"full_name": "Jane Doe",
"email": "vendor@example.com",
"is_active": true,
"deleted_at": null,
"avatar": null
}
}


---

14. Update Vendor



PUT /vendor/{id} ยท Protected

Request body:

{
"email": "updated@example.com",
"full_name": "Jane Smith",
"password": "NewSecurePass1!",
"is_active": true,
"avatar": "https://cdn.example.com/avatar.jpg"
}


| Field | Rules |
|-------|-------|
| email | required, unique |
| full_name | required |
| password | optional, min 8 |
| is_active | required boolean |
| avatar | optional string |

---

15. Delete Vendor



DELETE /vendor/{id} ยท Protected

Soft-deletes the vendor.

---

16. Vendor Operations



POST /vendor/activate/{id} ยท Protected
POST /vendor/deactivate/{id} ยท Protected
POST /vendor/delete/{id} ยท Protected (soft delete)
POST /vendor/delete_permanently/{id} ยท Protected (force delete)

No request body required.

Success (200):

{
"status": 200,
"success": true,
"message": "Vendor activated successfully!"
}


---

Organization Management (Protected)



17. Create Organization



POST /organization ยท Protected

Provisions a new tenant database immediately.

Request body:

{
"organization_name": "Acme Corp",
"full_name": "Jane Doe",
"layout_preference": "grid",
"plan": "free",
"database_plan": "separate_db_same_host",
"dbserver": "self",
"vendor_id": 1
}


| Field | Rules |
|-------|-------|
| organization_name | optional string |
| full_name | required string |
| layout_preference | grid, list, or table |
| plan / vendor_plan | team, standard, free, enterprise, business, shared, free_shared (maps to database tenancy) |
| database_plan | shared_db, separate_db_same_host, separate_db_separate_host |
| vendor_id | optional; defaults to authenticated vendor |

Success (200):

{
"status": 200,
"success": true,
"message": "Success",
"data": {
"orgcode": "1588571752",
"name": "Acme Corp"
}
}


cURL:

curl -X POST http://localhost:8000/api/organization \
-H "Content-Type: application/json" \
-H "X-Core-Token: your_core_token" \
-H "Authorization: Bearer vendor_core_token" \
-d '{"organization_name":"Acme Corp","full_name":"Jane Doe","plan":"free"}'


---

18. List Organizations



GET /organizations ยท Protected

Query: items_per_page, organization_id, filter_status, search, sort, order

Admin response โ€” paginated with vendor_name, deleted_at, etc.

Vendor response โ€” unpaginated list scoped to owned/member orgs:

{
"status": 200,
"success": true,
"message": "Organizations listed successfully",
"data": [
{
"name": "Acme Corp",
"orgcode": "1588571752",
"email": "org@acme.com",
"logo": "https://...",
"is_active": true,
"status_label": "Active",
"self": true
}
]
}


---

19. Get Organization



GET /organization/{orgcode} ยท Protected

Success (200):

{
"status": 200,
"success": true,
"message": "Organization retrieved successfully!",
"data": {
"id": 1,
"name": "Acme Corp",
"orgcode": "1588571752",
"email": "org@acme.com",
"phone": null,
"address": null,
"website": null,
"logo": "https://...",
"status": 1
}
}


---

20. Update Organization



POST /organization/update/{orgcode} ยท Protected

Request body:

{
"organization_name": "Acme Corporation",
"full_name": "Jane Doe",
"is_active": true,
"layout_preference": "list"
}


---

21. Delete Organization



DELETE /organization/{orgcode} ยท Protected

Permanently deletes organization and tenant database.

---

22. Organization Quick Actions



POST /organization/action/{orgcode} ยท Protected

Request body:

{
"action": "soft_delete"
}


Allowed: soft_delete, activate, deactivate

---

23. Send Organization Invitations



POST /send-organization-invitations ยท Protected

Request body:

{
"emails": "user1@example.com, user2@example.com",
"organization_id": 1
}


Success (200):

{
"status": 200,
"success": true,
"message": "Invitations sent successfully.",
"sent_count": 2
}


Email-only invitations (does not create a vendor row or bind to organization_vendor). Prefer Add Organization Team Member when the teammate should become a vendor linked to the org.

---

Add Organization Team Member



POST /organization/team-members ยท Protected

Create (or fetch) a vendor by email, bind them to the organization via organization_vendor, and send a vendor invitation email. The calling vendor must already have access to the target organization.

Request body:

{
"email": "teammate@example.com",
"orgcode": "1588571752",
"full_name": "Jane Doe"
}


| Field | Required | Notes |
|-------|----------|-------|
| email | Yes | Team member email; creates vendors row if missing |
| orgcode | One of orgcode / organization_id | Target organization shortcode |
| organization_id | One of orgcode / organization_id | Target organization id |
| full_name | No | Display name for new vendor |
| approve_token | No | Opaque token from org app; generated if omitted |

Success (200):

{
"status": 200,
"success": true,
"message": "Team member added and bound to the organization successfully!",
"data": {
"vendor_id": 2,
"vendor_email": "teammate@example.com",
"vendor_full_name": "Jane Doe",
"organization_id": 1,
"orgcode": "1588571752",
"already_linked": false,
"invitation_id": 5,
"invitation_url": "http://localhost:3000/1588571752/uuid-token",
"mail_sent": true
}
}


cURL:

curl -X POST http://localhost:8000/api/organization/team-members \
-H "Content-Type: application/json" \
-H "X-Core-Token: your_core_token" \
-H "Authorization: Bearer vendor_api_token" \
-d '{"email":"teammate@example.com","orgcode":"1588571752","full_name":"Jane Doe"}'


Notes:
  • Invitee accepts via POST /vendor-invitations/accept (see Vendor Invitations).

  • Invitation email links to {FRONTEND_URL}/accept-invitation/{orgcode}/{token} on the marketing site (mydesk-website).

  • Re-inviting the same email refreshes the invitation token and re-sends the email.

  • Invitation email is sent via VendorInvitationMail using FRONTEND_URL for the accept link (SPA Origin is ignored).

  • Related public endpoint: POST /invite-vendor (core token only; no caller membership check).


  • ---

    25. Check Organization Access



    POST /check-organization-access ยท Protected

    Request body:

    {
    "orgcode": "1588571752"
    }


    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Access granted",
    "data": {
    "vendor_id": 1,
    "organization_id": 1,
    "token": "workspace_access_token"
    }
    }


    ---

    26. My Organizations List



    GET /organizations-list ยท Protected

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Organizations listed successfully",
    "data": [
    { "id": 1, "name": "Acme Corp", "vendor_id": 1 }
    ]
    }


    ---

    27. Organization Stats



    POST /org-stats ยท Public

    Request body:

    {
    "orgcode": "1588571752"
    }


    Success (200):

    {
    "organization": {
    "name": "Acme Corp",
    "orgcode": "1588571752"
    },
    "db": {
    "size_mb": 12.5,
    "tables": 40
    },
    "projects": { "all": 10, "active": 8, "inactive": 2 },
    "tasklists": { "all": 20, "active": 15, "inactive": 5 },
    "users": { "all": 5, "active": 4, "inactive": 1 }
    }


    ---

    Organization File Management (Protected)





    POST /organization/{orgcode}/upload-logo ยท Protected
    Content-Type: multipart/form-data

    | Field | Rules |
    |-------|-------|
    | logo | required file, max 100 KB |

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Organization logo uploaded successfully!",
    "data": {
    "logo_path": "organizations/1588571752/logos/logo.png",
    "logo_url": "https://cdn.example.com/organizations/1588571752/logos/logo.png"
    }
    }


    ---

    29. Upload File



    POST /organization/{orgcode}/upload-file ยท Protected
    Content-Type: multipart/form-data

    | Field | Rules |
    |-------|-------|
    | file | required file, max 100 KB |
    | folder | optional: documents, images, temp |

    ---

    30. List Files



    GET /organization/{orgcode}/files?folder=documents ยท Protected

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Organization files listed successfully!",
    "data": {
    "files": [
    {
    "name": "report.pdf",
    "path": "organizations/1588571752/documents/report.pdf",
    "url": "https://...",
    "size": 2048,
    "last_modified": 1704067200
    }
    ],
    "folder": "documents",
    "total_files": 1
    }
    }


    ---

    31. Delete File



    DELETE /organization/{orgcode}/file ยท Protected

    Request body:

    {
    "file_path": "organizations/1588571752/documents/report.pdf"
    }


    ---

    Admin Management (Protected)



    31. List Admins



    GET /admins ยท Protected

    Query: items_per_page, filter_role, filter_status, search, sort, order

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Records listed successfully!",
    "data": [
    { "id": 1, "name": "Admin", "email": "admin@mydesk.com", "role_name": "Admin" }
    ],
    "payload": { "pagination": { "page": 1, "total": 1 } }
    }


    ---

    32. Create Admin



    POST /admin ยท Protected

    Request body:

    {
    "name": "New Admin",
    "email": "newadmin@example.com",
    "password": "password123",
    "role_id": 1,
    "status": true,
    "avatar": null
    }


    ---

    33. Get Admin



    GET /admin/{id} ยท Protected

    ---

    34. Update Admin



    PUT /admin/{id} ยท Protected

    Request body: name, email, avatar, password (min 6), role_id, status โ€” all optional

    ---

    35. Delete Admin



    DELETE /admin/{id}?action=soft_delete ยท Protected

    Query action: soft_delete, activate, deactivate, permanent_delete

    ---



    POST /admin/send-reset-link ยท Protected

    Request body:

    {
    "email": "admin@mydesk.com"
    }


    ---

    37. Reset Admin Password



    POST /admin/password/reset/{token} ยท Public

    Request body:

    {
    "email": "admin@mydesk.com",
    "password": "newpassword123",
    "password_confirmation": "newpassword123"
    }


    ---

    Role Management (Protected)



    38. List Roles



    GET /roles?items_per_page=10 ยท Protected

    ---

    39. Create Role



    POST /role ยท Protected

    Request body:

    {
    "name": "Manager",
    "description": "Can manage projects and members"
    }


    ---

    40. Get Role



    GET /role/{id} ยท Protected

    ---

    41. Update Role



    PUT /role/{id} ยท Protected

    ---

    42. Delete Role



    DELETE /role/{id} ยท Protected

    ---

    43. Assign Role to Admin



    POST /admin/{id}/assign-role ยท Protected

    Request body:

    {
    "role_id": 1
    }


    ---

    Vendor Invitations (Public)



    For the authenticated frontend flow (logged-in vendor adds a teammate by email), use Add Organization Team Member (POST /organization/team-members). The endpoints below are the legacy/public invite path and acceptance.

    44. Create Vendor Invitation



    POST /invite-vendor ยท Public

    Creates or fetches a vendor, binds them to the organization, and emails an invitation. Does not verify that the caller belongs to the organization โ€” prefer the protected team-members endpoint for product UI.

    Request body:

    {
    "email": "collaborator@example.com",
    "orgcode": "1588571752",
    "full_name": "John Collaborator",
    "approve_token": "approval_token_from_org_app"
    }


    Alternative name fields: first_name + last_name instead of full_name.

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Invitation sent successfully!",
    "data": {
    "vendor_id": 2,
    "organization_id": 1,
    "invitation_url": "http://app/1588571752/uuid-token"
    }
    }


    ---

    45. Accept Vendor Invitation



    POST /vendor-invitations/accept ยท Public

    Request body:

    {
    "invitation_token": "uuid-from-invitation-url",
    "orgcode": "1588571752",
    "password": "ChooseASecurePass1!",
    "password_confirmation": "ChooseASecurePass1!"
    }


    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Invitation accepted successfully!",
    "data": {
    "vendor_id": 2,
    "org_id": 1,
    "orgcode": "1588571752",
    "email": "teammate@example.com",
    "accepted_at": "2026-07-15T10:00:00.000000Z",
    "api_token": "new_vendor_core_token"
    }
    }


    Notes:
  • The invitee sets their own password on the accept page; it is stored on vendors.password and synced to the org user.password.

  • Then they sign in with that email + password.


  • ---

    Notifications (Public)



    46. Send Notification



    POST /notify ยท Public

    Broadcasts via Laravel Reverb to organization users.

    Request body:

    {
    "user_ids": [1, 2, 3],
    "orgcode": "1588571752",
    "title": "New task assigned",
    "url": "https://app.mydesk.com/tasks/42",
    "message": "You have been assigned to task #42"
    }


    Success (200):

    {
    "status": "success",
    "data": {
    "title": "New task assigned",
    "url": "https://app.mydesk.com/tasks/42",
    "message": "You have been assigned to task #42",
    "orgcode": "1588571752"
    },
    "user_ids": [1, 2, 3]
    }


    ---

    Plan Management (Protected)



    Default seeded plans:

    | Plan | Price | Team | Storage |
    |------|-------|------|---------|
    | Free | $0/mo | Max 5 | 500 MB |
    | Personal | $5/user | Unlimited | 5 GB |
    | Business | $3.49/user | Unlimited | 30 GB |

    47. List Plans



    GET /plans ยท Protected

    Query: items_per_page, search, active_only (admin only)

    Vendors see active plans only. Admins see all unless active_only=true.

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Plans listed successfully!",
    "data": [
    {
    "id": 1,
    "name": "Free",
    "slug": "free",
    "description": "Get started with limited team size...",
    "price_monthly": 0,
    "price_per_user": 0,
    "max_team_members": 5,
    "max_storage_mb": 500,
    "max_storage_gb": 0.49,
    "features": {
    "unlimited_projects": true,
    "unlimited_tasklists": true,
    "unlimited_tasks": true,
    "unlimited_statuses": true,
    "unlimited_roles": true
    },
    "is_active": true,
    "sort_order": 1
    }
    ],
    "payload": { "pagination": { "page": 1, "total": 3 } }
    }


    ---

    48. Get Plan



    GET /plan/{id} ยท Protected

    ---

    49. Create Plan (Admin only)



    POST /plan ยท Protected ยท Admin

    Request body:

    {
    "name": "Enterprise",
    "slug": "enterprise",
    "description": "Custom enterprise plan",
    "price_monthly": 99.00,
    "price_per_user": 2.99,
    "max_team_members": null,
    "max_storage_mb": 102400,
    "is_active": true,
    "sort_order": 4
    }


    null on limit fields = unlimited.

    ---

    50. Update Plan (Admin only)



    PUT /plan/{id} ยท Protected ยท Admin

    ---

    51. Delete Plan (Admin only)



    DELETE /plan/{id} ยท Protected ยท Admin

    Fails with 400 if vendors are still assigned.

    ---

    52. Assign Plan to Vendor (Admin only)



    POST /vendor/{vendorId}/assign-plan ยท Protected ยท Admin

    Request body:

    {
    "plan_id": 2,
    "billing_day": 15
    }


    billing_day: 1โ€“28 (day of month for recurring invoices).

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Plan assigned to vendor successfully!",
    "data": {
    "vendor_id": 1,
    "plan": { "id": 2, "name": "Personal", "slug": "personal" },
    "plan_started_at": "2026-07-15T10:00:00.000000Z",
    "billing_day": 15
    }
    }


    ---

    53. Get Vendor Plan



    GET /vendor/{vendorId}/plan ยท Protected

    Vendor can only access own plan. Admin can access any vendor.

    ---

    Invoice Management (Protected)



    Invoice statuses: draft, pending, paid, overdue, cancelled, void
    Generation types: automatic, manual

    54. List Invoices



    GET /invoices ยท Protected

    Query:

    | Param | Description |
    |-------|-------------|
    | items_per_page | Default 10 |
    | vendor_id | Admin filter |
    | status | pending, paid, etc. |
    | generation_type | automatic or manual |
    | search | Invoice number or plan name |
    | period_start / period_end | Date filters |

    Vendor sees own invoices only.

    ---

    55. Get Invoice



    GET /invoice/{id} ยท Protected

    Success (200):

    {
    "status": 200,
    "success": true,
    "message": "Invoice retrieved successfully!",
    "data": {
    "id": 1,
    "invoice_number": "INV-202607-00001",
    "vendor_id": 1,
    "vendor": {
    "id": 1,
    "email": "vendor@example.com",
    "full_name": "Jane Doe"
    },
    "plan_id": 2,
    "plan_name": "Personal",
    "period_start": "2026-07-01",
    "period_end": "2026-07-31",
    "team_members_count": 8,
    "price_per_user": 5.00,
    "base_amount": 0.00,
    "seats_amount": 40.00,
    "subtotal": 40.00,
    "tax_rate": 0,
    "tax_amount": 0.00,
    "total": 40.00,
    "currency": "USD",
    "status": "pending",
    "generation_type": "automatic",
    "due_date": "2026-08-14",
    "paid_at": null,
    "items": [
    {
    "id": 1,
    "description": "Personal plan โ€” team members",
    "quantity": 8,
    "unit_price": 5.00,
    "amount": 40.00,
    "item_type": "seats"
    }
    ]
    }
    }


    ---

    56. Create Invoice Manually (Admin only)



    POST /invoice ยท Protected ยท Admin

    Request body:

    {
    "vendor_id": 1,
    "period_start": "2026-07-01",
    "period_end": "2026-07-31",
    "team_members_count": 8,
    "tax_rate": 0,
    "notes": "July billing",
    "status": "pending",
    "due_date": "2026-08-14"
    }


    ---

    57. Generate Invoices (Admin only)



    POST /invoices/generate ยท Protected ยท Admin

    Single vendor:

    {
    "vendor_id": 1,
    "generation_type": "manual",
    "period_start": "2026-07-01",
    "period_end": "2026-07-31"
    }


    All vendors (bulk monthly):

    {
    "all_vendors": true,
    "period_start": "2026-07-01"
    }


    Bulk success (200):

    {
    "status": 200,
    "success": true,
    "message": "Bulk invoice generation completed!",
    "data": {
    "generated": [
    {
    "vendor_id": 1,
    "invoice_id": 1,
    "invoice_number": "INV-202607-00001",
    "total": 40.00
    }
    ],
    "skipped": [],
    "failed": []
    }
    }


    CLI equivalent:

    php artisan invoices:generate --month=2026-07 --mark-overdue


    ---

    58. Update Invoice (Admin only)



    PUT /invoice/{id} ยท Protected ยท Admin

    Request body:

    {
    "notes": "Updated seat count",
    "team_members_count": 10,
    "tax_rate": 8.5,
    "due_date": "2026-08-20",
    "status": "pending"
    }


    Only draft and pending invoices are editable.

    ---

    59. Mark Invoice Paid (Admin only)



    POST /invoice/{id}/mark-paid ยท Protected ยท Admin

    Request body:

    {
    "paid_at": "2026-07-20T14:30:00Z"
    }


    ---

    60. Cancel Invoice (Admin only)



    POST /invoice/{id}/cancel ยท Protected ยท Admin

    Request body (optional):

    {
    "notes": "Customer requested cancellation"
    }


    ---

    61. Void Invoice (Admin only)



    POST /invoice/{id}/void ยท Protected ยท Admin

    ---

    62. Delete Invoice (Admin only)



    DELETE /invoice/{id} ยท Protected ยท Admin

    Soft-delete. Paid invoices cannot be deleted โ€” void instead.

    ---

    63. List Vendor Invoices



    GET /vendor/{vendorId}/invoices ยท Protected

    Same as GET /invoices scoped to one vendor. Vendor can only access own ID.

    ---

    Error Responses



    | HTTP | When | Example |
    |------|------|---------|
    | 401 | Missing/invalid core or API token | { "success": false, "status": 401, "message": "Invalid or missing core token" } |
    | 403 | Forbidden (e.g. vendor calling admin-only) | { "status": 403, "success": false, "message": "Access forbidden" } |
    | 404 | Resource not found | { "status": 404, "success": false, "message": "Plan not found" } |
    | 422 | Validation failed | See validation format above |
    | 500 | Server error | { "status": 500, "success": false, "message": "Something went wrong.", "error": "..." } |

    ---

    Standard request headers



    All requests


    -H "X-Core-Token: your_core_token"
    -H "Content-Type: application/json"

    Protected routes (vendor)


    -H "Authorization: Bearer vendor_core_api_token"

    Protected routes (admin)


    -H "Authorization: Bearer admin_token_from_login"


    ---

    Testing workflow



    1. Register


    curl -X POST http://localhost:8000/api/register \
    -H "X-Core-Token: $CORE_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","password":"SecurePass1!","password_confirmation":"SecurePass1!"}'

    2. Verify email (from email link token)


    curl "http://localhost:8000/api/verify-email?token=VERIFICATION_TOKEN" \
    -H "X-Core-Token: $CORE_TOKEN"

    3. Login


    curl -X POST http://localhost:8000/api/login \
    -H "X-Core-Token: $CORE_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"email":"test@example.com","action":"password","password":"SecurePass1!"}'

    4. Create organization (use core_access_token from login)


    curl -X POST http://localhost:8000/api/organization \
    -H "X-Core-Token: $CORE_TOKEN" \
    -H "Authorization: Bearer $CORE_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"organization_name":"My Workspace","full_name":"Test User","plan":"free"}'

    5. List plans


    curl http://localhost:8000/api/plans \
    -H "X-Core-Token: $CORE_TOKEN" \
    -H "Authorization: Bearer $CORE_TOKEN"

    6. Admin login


    curl -X POST http://localhost:8000/api/admin/login \
    -H "X-Core-Token: $CORE_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"email":"admin@mydesk.com","password":"Admin@123456"}'


    ---

    Known quirks



    1. Two response styles โ€” Admin/Role use string "status": "200"; newer endpoints use integer status + success.
    2. Duplicate route โ€” POST /resend-verification registered for both vendor (public) and organization (protected); public wins.
    3. Token confusion โ€” After login with orgs, use session.core_access_token for Core API, not top-level api_token (workspace token).
    4. Organization plan field โ€” Maps to database tenancy (database_plan), separate from billing Plans (/plans).

    ---



  • Organization Database Configuration

  • Multi-Tenancy Architecture

  • Database Seeders Guide