Technical documentation
Routes, guards, payloads, and files as the Tenant API stands today.
Dashboard
Tenant API is the per-tenant CRM runtime. REST is registered with Fastify prefix /api/v1 (server.js → registerRoutes). JWT tenant routes send Authorization: Bearer {{access_token}} unless a route is explicitly public.
Shipment pipeline
flowchart LR Lead --> Quote --> Order --> Dispatch Dispatch --> Invoice Invoice --> Payment
Envelope
- Success:
{ ok: true, status, invokedEndpoint, invokedMethod, responseTime, timestamp, ...result } - Error:
{ ok: false, status, invokedMethod, responseTime, timestamp, error } - Do not leak SQL or stacks in production (
src/core/index.jserror handler). - Lists paginate. Role-scoped totals must match visible rows (
src/common/list-scope.js).
Guards
| Guard | Behavior |
|---|---|
authGuard |
Bearer JWT → request.user. Then IP allowlist (assertIpAllowlist). |
roleGuard(roles) |
Requires at least one of request.user.roles. |
permissionGuard(key) |
Requires key in request.user.permissions (example leads.read). |
departmentGuard(dept) |
Requires request.user.department === dept. |
Platform rules
- One DB + one runtime per tenant. Owner remains source of truth for platform billing. Tenant stores a mirrored copy of those commercial transactions only.
- Static uploads:
GET /uploads/*with path traversal protection. Multipart writes usegetTenantApiPublicDir(). - Set
TENANT_API_PUBLIC_ORIGINto the browser-facing API origin (no trailing slash) so stored signed-PDF URLs are not derived from internalHost. - Quote → order is
POST /api/v1/tenant/orderswithquoteId. There is noPOST /tenant/quotes/:id/convert-to-order. - Accounts
send-invoicesetspaymentStatustoinvoice_sent. Order/dispatchsend-invoiceis email only. - Invoices module (
/invoices) ≠ Accounts workbench. Payments module (/payments) ≠ order nested/tenant/orders/:orderId/payments≠ AccountspaymentStatuson the order row.
Health
| Method | Path | Access | Notes |
|---|---|---|---|
| GET | /health (full: /api/v1/health) |
Public | Registered inside registerRoutes so the live path is /api/v1/health |
Body includes ok, version (0.1.0), environment, timestamp, uptime, message.
curl --request GET "{{base_url}}/api/v1/health"Uploads
GET /uploads/*is registered on the app without the/api/v1prefix (server.js).GET /uploads/customer-signatures/:filename/downloadis a dedicated download route.- Quote/order/dispatch attachment URLs typically live under this tree after multipart write.
Implementation map
| Piece | Path |
|---|---|
| Server | tenant-api/src/server.js |
| Module register | tenant-api/src/modules/index.js |
| Guards / envelope | tenant-api/src/core/index.js |
| List scope | src/common/list-scope.js |
| List filters / month window | src/common/list-filters.js |
| Pagination schema | src/common/pagination-schema.js |
| Public uploads helper | src/common/public-paths.js |
| Multipart attachments | src/common/multipart-attachments.js |
Auth
Login issues JWT access and refresh tokens. Access tokens carry userId, tenantId, roles[], department, and permissions[]. Passwords arrive encoded from the CRM UI; the API decodes, then bcrypts. Never store plaintext.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| POST | /tenant/auth/login |
Public |
| GET | /tenant/auth/me |
authGuard |
| POST | /tenant/auth/refresh |
Public (body refreshToken) |
| POST | /tenant/auth/logout |
Public / session |
| POST | /tenant/auth/request-reset |
Public |
| POST | /tenant/auth/request-reset/resend |
Public |
| POST | /tenant/auth/request-reset/verify-otp |
Public |
| POST | /tenant/auth/reset |
Public |
| POST | /tenant/auth/verify-email/request |
Public |
| POST | /tenant/auth/verify-email/resend |
Public |
| POST | /tenant/auth/verify-email |
Public |
| POST | /tenant/auth/verify-password |
authGuard |
| PATCH | /tenant/auth/password |
authGuard |
Bodies
| Endpoint | Required fields |
|---|---|
| login | email, password |
| refresh | refreshToken (min 10 chars) |
| request-reset / resend | email |
| verify-otp | email, otp (length 6) |
| reset | email, password (min 8), otp (length 6) |
| verify-email request/resend | email |
| verify-email | email, otp (length 6) |
| verify-password | password |
| PATCH password | password (min 8), confirmPassword (min 8) |
flowchart LR Login --> AccessToken AccessToken --> Me Refresh --> AccessToken RequestReset --> VerifyOtp VerifyOtp --> Reset
curl --request POST "{{base_url}}/api/v1/tenant/auth/login" \
--header "Content-Type: application/json" \
--data-raw '{ "email": "admin@example.com", "password": "encoded-password" }'Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/auth/routes.js |
| Controller | tenant-api/src/modules/auth/controller.js |
| Service | tenant-api/src/modules/auth/service.js |
| Schemas | tenant-api/src/modules/auth/schemas.js |
| Crypto helpers | src/common/crypto-helpers.js |
| IP allowlist | src/common/ip-allowlist.js |
SSE
GET /api/v1/tenant/sse is an authenticated event stream. Heartbeats every 25 seconds (: heartbeat). First event is connected with { ok, tenantId, userId }. Helpers: sendSSEToTenant, sendSSEToUser, sendSSEToUsers. Created lifecycle events (lead.created, quote.created, order.created, dispatch.created) go to a scoped recipient set, not blindly the whole tenant.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/sse |
authGuard |
Known events
| Event | Typical payload / notes |
|---|---|
connected |
{ ok, tenantId, userId } on connect |
lead.created / lead.updated / lead.deleted / lead.assigned / lead.converted |
Created is scoped; assigned is sendSSEToUser |
lead.vehicle.* / lead.other.* / lead.note.* |
Tenant broadcast from lead nested controllers |
quote.created / quote.updated / quote.deleted / quote.assigned / quote.converted / quote.rejected / quote.rework_required |
Created/converted scoped; rejected on order reject |
order.created / order.updated / order.deleted / order.assigned / order.approved / order.rejected / order.reactivated / order.requires_approval |
Created scoped |
order.carrier_assigned / order.carrier_updated / order.carrier_removed |
Assign vs reassignment |
order.vehicle.* / order.other.* / order.note.* / order.signature.* / order.payment.* / order.calendar_event.* |
Nested order resources |
dispatch.created / dispatch.updated / dispatch.deleted / dispatch.assigned / dispatch.delivered / dispatch.cancelled |
Created scoped |
dispatch.vehicle.* / dispatch.other.* / dispatch.note.* / dispatch.bol.* |
Nested dispatch |
notify.info / notify.read / notify.all_read / notify.deleted |
In-app notifications |
user:created / user:updated / user:activated / user:suspended / user:deleted / user:bulk-operation / user:avatar_updated |
Users (colon names) |
user:invite:created / user:invite:resent / user:invite:cancelled / user:invite:accepted |
Invites |
team.created / team.updated / team.deleted / team.members_updated |
Teams |
carrier.created / carrier.updated / carrier.deleted / nested driver/insurance/trailer/otherContact |
Carriers |
calendar.created / calendar.updated / calendar.deleted |
Standalone calendar |
accounts.invoice_sent / accounts.payment_updated |
Accounts workbench |
settings.updated / settings.integrations_updated |
Settings |
email-template.created / .updated / .deleted |
Email templates |
edoc.signed |
Public eDoc submit |
analytics.team_dashboard.updated |
{ team, date } after sales/dispatch mutations |
activity-log.created |
To privileged viewers |
curl --request GET "{{base_url}}/api/v1/tenant/sse" \
--header "Authorization: Bearer {{access_token}}" \
--header "Accept: text/event-stream"Implementation map
| Piece | Path |
|---|---|
| Plugin | tenant-api/src/modules/sse/index.js |
| Helpers | src/common/sse-helpers.js |
| Scoped emit / notify | src/common/notification-helpers.js |
| Team dashboard SSE | src/common/team-dashboard-sse.js |
Users
Staff CRUD, invites, bulk activate/suspend/delete, activate/suspend, avatar. Lists never include the requester. Higher-role users do not appear in lower-role lists (ROLE_HIERARCHY in users/utils.js). Commission fields apply to sales and dispatch roles.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/users/stats |
authGuard + users.read |
| GET | /tenant/users |
authGuard + users.read |
| PATCH | /tenant/users/me/avatar |
authGuard (self) |
| POST | /tenant/users |
authGuard + roleGuard(admin,sub-admin) + users.create |
| GET | /tenant/users/:id |
authGuard + users.read |
| PATCH | /tenant/users/:id |
authGuard + users.update |
| PATCH | /tenant/users/password/reset-by-email |
authGuard + roleGuard(admin,sub-admin) |
| POST | /tenant/users/:id/activate |
authGuard + roleGuard(admin,sub-admin) + users.update |
| POST | /tenant/users/:id/suspend |
authGuard + roleGuard(admin,sub-admin) + users.update |
| DELETE | /tenant/users/:id |
authGuard + roleGuard(admin) + users.delete |
| POST | /tenant/users/bulk |
authGuard + roleGuard(admin,sub-admin) + users.update |
| POST | /tenant/users/invites |
authGuard + roleGuard(admin,sub-admin) + users.create |
| GET | /tenant/users/invites |
authGuard + users.read |
| POST | /tenant/users/invites/:inviteId/resend |
authGuard + roleGuard(admin,sub-admin) + users.create |
| POST | /tenant/users/invites/:inviteId/cancel |
authGuard + roleGuard(admin,sub-admin) + users.update |
| POST | /tenant/users/invites/accept |
Public |
List / query fields
search,status(active|suspended),departmentId,roleIddateFrom,dateTolimit,offsetorderBy:createdAt|updatedAt|lastLoginAt|firstName|lastName|email- Invite list:
status(pending|accepted|cancelled, defaultpending),search,limit,offset
Required create fields
- Create user:
email,password(min 8),firstName,lastName. Optional:departmentId,roleIds[],commissionType(percentage|fixed) /commissionValue(aliasescommisionType/commisionValue) - Invite:
email,firstName,lastName. OptionaldepartmentId,roleIds[] - Accept invite:
token(uuid),password(min 8) - Bulk:
userIds[](min 1),action(activate|suspend|delete) - Admin reset-by-email:
email,password(min 8)
Enums / lifecycle
| Field | Values |
|---|---|
| User status | active, suspended |
| Invite status | pending, accepted, cancelled |
| Commission type | percentage (0–100), fixed (0–99999) |
| Commission-eligible roles | sales, sales-head, dispatch, dispatch-head |
SSE
user:created, user:updated, user:activated, user:suspended, user:deleted, user:bulk-operation, user:avatar_updated, user:invite:created, user:invite:resent, user:invite:cancelled, user:invite:accepted.
curl --request GET "{{base_url}}/api/v1/tenant/users?limit=50&offset=0" \
--header "Authorization: Bearer {{access_token}}"Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/users/routes.js |
| Controller | tenant-api/src/modules/users/controller.js |
| Service | tenant-api/src/modules/users/service.js |
| Schemas | tenant-api/src/modules/users/schemas.js |
| Hierarchy / commission | tenant-api/src/modules/users/utils.js |
Departments
Org units used by users, teams, email templates, and some list filters. Create/delete require Admin or Sub Admin plus permission keys. Patch uses departments.update without the extra roleGuard.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/departments |
authGuard + departments.read |
| POST | /tenant/departments |
authGuard + roleGuard(admin,sub-admin) + departments.create |
| GET | /tenant/departments/:id |
authGuard + departments.read |
| PATCH | /tenant/departments/:id |
authGuard + departments.update |
| DELETE | /tenant/departments/:id |
authGuard + roleGuard(admin,sub-admin) + departments.delete |
List / query fields
limit(default 50)offset(default 0)orderBy/orderDir(ASC|DESC, defaultDESC)searchdateFrom/dateTo(ISO datetime)month+yeartogether (month 1–12). Date range wins when eitherdateFromordateTois sent- List windows default to the current calendar month when no explicit range is sent (
resolveListCreatedAtRange)
Required create fields
name(1–80 chars). Optionaldescription.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/departments/routes.js |
| Schemas | tenant-api/src/modules/departments/schemas.js |
Teams
Teams own members, heads (roleInTeam: head | member), and sourceOrigins. Team origins win over tenant Settings origins when both match a public capture request (capture-origin.js + lead capture controller).
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/teams |
authGuard + teams.read |
| POST | /tenant/teams |
authGuard + roleGuard(admin,sub-admin) + teams.create |
| GET | /tenant/teams/:id |
authGuard + teams.read |
| PATCH | /tenant/teams/:id |
authGuard + roleGuard(admin,sub-admin) + teams.update |
| DELETE | /tenant/teams/:id |
authGuard + roleGuard(admin,sub-admin) + teams.delete |
| PUT | /tenant/teams/:id/members |
authGuard; controller: admin/sub-admin or team head (sales-head/dispatch-head) |
List / query fields
limit(default 50)offset(default 0)orderBy/orderDir(ASC|DESC, defaultDESC)searchdateFrom/dateTo(ISO datetime)month+yeartogether (month 1–12). Date range wins when eitherdateFromordateTois sent- List windows default to the current calendar month when no explicit range is sent (
resolveListCreatedAtRange)
Required create fields
name(1–120). Optional:description,departmentId,sourceOrigins[](strings),isActive(default true)- Replace members:
{ members: [{ userId, roleInTeam? }] }withroleInTeamhead|member(defaultmember)
Org-team list-scope
Lifecycle lists merge org-team visibility (mergeOrgTeamLifecycleWhere): Admin/Sub Admin unchanged; team members see team-origin records plus assigned plus created-by-self; non-team privileged heads hide team-owned records unless assigned/created.
SSE
team.created, team.updated, team.deleted, team.members_updated.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/teams/routes.js |
| Controller | tenant-api/src/modules/teams/controller.js |
| Schemas | tenant-api/src/modules/teams/schemas.js |
| Capture origins | src/common/capture-origin.js |
| List scope teams | src/common/list-scope.js |
Roles
Seeded roles include admin, sub-admin, sales, sales-head, dispatch, dispatch-head, marketing, marketing-head, accounts, accounts-head (plus admin-depart in hierarchy utils). Create attaches permissionIds.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/roles |
authGuard + roles.read |
| POST | /tenant/roles |
authGuard + roleGuard(admin,sub-admin) + roles.create |
| GET | /tenant/roles/:id |
authGuard + roles.read |
| PATCH | /tenant/roles/:id |
authGuard + roles.update |
| DELETE | /tenant/roles/:id |
authGuard + roleGuard(admin,sub-admin) + roles.delete |
List / query fields
limit(default 50)offset(default 0)orderBy/orderDir(ASC|DESC, defaultDESC)searchdateFrom/dateTo(ISO datetime)month+yeartogether (month 1–12). Date range wins when eitherdateFromordateTois sent- List windows default to the current calendar month when no explicit range is sent (
resolveListCreatedAtRange)
Required create fields
name(1–80). Optionaldescription,permissionIds[](uuids, default[]).
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/roles/routes.js |
| Schemas | tenant-api/src/modules/roles/schemas.js |
Permissions
Keys look like leads.read or orders.update. Routes call permissionGuard('resource.action'). Create stores key, name, optional description.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/permissions |
authGuard + permissions.read |
| POST | /tenant/permissions |
authGuard + roleGuard(admin,sub-admin) + permissions.create |
| GET | /tenant/permissions/:id |
authGuard + permissions.read |
| PATCH | /tenant/permissions/:id |
authGuard + permissions.update |
| DELETE | /tenant/permissions/:id |
authGuard + roleGuard(admin,sub-admin) + permissions.delete |
List / query fields
limit(default 50)offset(default 0)orderBy/orderDir(ASC|DESC, defaultDESC)searchdateFrom/dateTo(ISO datetime)month+yeartogether (month 1–12). Date range wins when eitherdateFromordateTois sent- List windows default to the current calendar month when no explicit range is sent (
resolveListCreatedAtRange)
Required create fields
key(1–120),name(1–150). Optionaldescription.
Permission keys used by routes (non-exhaustive)
users.read|create|update|deletedepartments.*,teams.*,roles.*,permissions.*leads.read|create|update|delete,quotes.*,orders.*,dispatch.*sales.assign,dispatch.assign,carriers.assignorders.send-invoice,dispatch.send-invoiceaccounts.read,accounts.send_invoice,accounts.update_paymentinvoices.*,payments.*,carriers.*,customers.read|create|updateanalytics.view,templates.view,search.view,email-templates.read
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/permissions/routes.js |
| Schemas | tenant-api/src/modules/permissions/schemas.js |
Admin
Tenant-admin views of subscription, features, and owner-pushed templates. Owner syncs into this tenant on login and on an interval, plus a webhook. Tenant is not the authority for platform billing.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /admin/subscription |
authGuard + roleGuard(admin,sub-admin) |
| GET | /admin/features |
authGuard + roleGuard(admin,sub-admin); query enabled optional |
| GET | /admin/template-version |
authGuard + roleGuard(admin,sub-admin) |
| POST | /admin/apply-template |
authGuard + roleGuard(admin,sub-admin) |
| GET | /admin/sync |
authGuard + roleGuard(admin,sub-admin); query force optional boolean |
| POST | /admin/sync/webhook |
Shared token (TENANT_SYNC_WEBHOOK_TOKEN), no user JWT |
Bodies
- Apply template: optional
key(min 3),overwrite(default true) - Webhook body: optional
tenantId,reason,triggeredBy,triggeredAt,timestamp
Implementation map
| Piece | Path |
|---|---|
| Admin routes | tenant-api/src/modules/admin/routes.js |
| Admin schemas | tenant-api/src/modules/admin/schemas.js |
| Sync routes | tenant-api/src/modules/admin-sync/routes.js |
| Sync schemas | tenant-api/src/modules/admin-sync/schemas.js |
Leads
Authenticated lead CRUD, packages (vehicles or other), notes, history, convert-to-quote. Public capture is unauthenticated and origin-gated. Convert loads the linked Customer and writes city, state, postal, address, and country onto the new quote. Creating a quote with leadId does the same hydration.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| POST | /public/leads/capture |
Public + x-tenant-id + Origin allowlist (tenant sourceOrigins / team sourceOrigins) |
| GET | /tenant/leads/stats |
authGuard + leads.read |
| GET | /tenant/leads |
authGuard + leads.read |
| POST | /tenant/leads |
authGuard + leads.create + sales.assign if assignedTo set |
| POST | /tenant/leads/:id/duplicate |
authGuard + leads.create |
| GET | /tenant/leads/:id |
authGuard + leads.read |
| PATCH | /tenant/leads/:id |
authGuard + leads.update + sales.assign if assignedTo set |
| DELETE | /tenant/leads/:id |
authGuard + leads.delete |
| POST | /tenant/leads/:id/convert-to-quote |
authGuard + leads.create |
| GET | /tenant/leads/:leadId/vehicles |
authGuard + leads.read |
| POST | /tenant/leads/:leadId/vehicles |
authGuard + package manage or leads.create |
| GET | /tenant/leads/:leadId/vehicles/:vehicleId |
authGuard + leads.read |
| PATCH | /tenant/leads/:leadId/vehicles/:vehicleId |
authGuard + package manage or leads.update |
| PUT | /tenant/leads/:leadId/vehicles/:vehicleId |
Same as PATCH (frontend PUT) |
| DELETE | /tenant/leads/:leadId/vehicles/:vehicleId |
authGuard + package manage or leads.delete |
| GET | /tenant/leads/:leadId/others |
authGuard + leads.read |
| POST | /tenant/leads/:leadId/others |
authGuard + package manage or leads.create |
| GET | /tenant/leads/:leadId/others/:otherId |
authGuard + leads.read |
| PATCH | /tenant/leads/:leadId/others/:otherId |
authGuard + package manage or leads.update |
| PUT | /tenant/leads/:leadId/others/:otherId |
Same as PATCH |
| DELETE | /tenant/leads/:leadId/others/:otherId |
authGuard + package manage or leads.delete |
| GET | /tenant/leads/:leadId/internal-notes |
authGuard + leads.read |
| POST | /tenant/leads/:leadId/internal-notes |
authGuard + leads.create |
| GET | /tenant/leads/:leadId/internal-notes/:noteId |
authGuard + leads.read |
| DELETE | /tenant/leads/:leadId/internal-notes/:noteId |
authGuard + leads.delete |
| GET | /tenant/leads/:leadId/change-history |
authGuard + leads.read |
List / query fields
search,stage,isActive,priority(low|normal|high),assignedTo,customer,transportTypeoriginCity/originState/originZip,destinationCity/destinationState/destinationZipexpectedShipDateFrom/expectedShipDateTo,source,trackingNumberdateFrom/dateTo,month+yearlimit,offsetorderBy:createdAt|updatedAt|expectedShipDate|expectedDeliveryDate|amount|priority|leadNumber- Change history:
limit,offset,orderBy(timeStamp|actionType)
Required create fields
- Either vehicle (
make,model,year) orother(type,name) — not both - Optional staff fields include
contactName,contactEmail,contactPhone, origin/destination,transportType,assignedTo(triggerssales.assign),priority(defaultnormal) - Public capture additionally requires Origin match; assignment fields in the body are ignored
Nested resources
- Vehicles:
modelYear(1900–2100),make,model. Optional:type,inop,carrierPay,brokerFee,vin,plateNumber,color,weight,mods,notes,addOn,lotNumber,status(active|inactive) - Others:
type,name. Optionalweight,dimensions,description,status - Internal notes:
note(min 1)
Enums / lifecycle
| Set | Values |
|---|---|
stage |
new, contacted, quoted, order, dispatch, delivered, closed, lost |
state (LEAD_STATES) |
Hot Leads, Not Interested (NI), VM, Disconnected Number (DC), Dealer Potential, Quoted, Booked |
priority |
low, normal, high |
transportType |
Open, Enclosed, Driveaway, Other, DD, SDL, HS, SD, RGN, RGNE, Flat bed, Dry van, Power only, Box Truck, Sprinter Van |
List-scope
- Admin / Sub Admin / sales-head: all sales pipeline (
canViewAllSalesPipeline) - sales: rows where
assignedTois self - Others: empty list (
mode: none) - Plus org-team visibility merge
SSE
lead.created (scoped), lead.updated, lead.deleted, lead.assigned (to assignee), lead.converted + quote.created on convert, lead.vehicle.*, lead.other.*, lead.note.*.
flowchart LR Capture --> Lead StaffCreate --> Lead Lead --> ConvertToQuote ConvertToQuote --> Quote
curl --request POST "{{base_url}}/api/v1/public/leads/capture" \
--header "Content-Type: application/json" \
--header "Origin: https://quotes.example.com" \
--header "x-tenant-id: {{tenant_id}}" \
--data-raw '{ "make": "Honda", "model": "Civic", "year": 2018, "contactEmail": "jane@example.com" }'curl --request GET "{{base_url}}/api/v1/tenant/leads?limit=50&offset=0" \
--header "Authorization: Bearer {{access_token}}"Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/leads/routes.js |
| Schemas | tenant-api/src/modules/leads/schemas.js |
| Stages | tenant-api/src/modules/leads/constants.js |
| Lifecycle states | src/common/lifecycle-state.js |
| Package access | src/common/lead-quote-access.js |
| Capture origin | src/common/capture-origin.js |
Quotes
Quote CRUD, duplicate, customer email, vehicles/others, notes, history, Aqua pricing helper. POST /tenant/quotes with leadId hydrates customer fields from the lead’s customer. Multipart create/PATCH merges uploaded files with attachment URLs.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/quotes/stats |
authGuard + quotes.read |
| GET | /tenant/quotes |
authGuard + quotes.read |
| POST | /tenant/quotes |
authGuard + quotes.create + sales.assign if assigning |
| POST | /tenant/quotes/:id/duplicate |
authGuard + quotes.create |
| POST | /tenant/quotes/customer-email |
authGuard + quotes.update |
| GET | /tenant/quotes/:id |
authGuard + quotes.read |
| PATCH | /tenant/quotes/:id |
authGuard + quotes.update + sales.assign if assigning; JSON or multipart |
| DELETE | /tenant/quotes/:id |
authGuard + quotes.delete |
| GET | /tenant/quotes/:quoteId/vehicles |
authGuard + quotes.read |
| POST | /tenant/quotes/:quoteId/vehicles |
authGuard + block dispatch package + package manage or quotes.create |
| GET | /tenant/quotes/:quoteId/vehicles/:vehicleId |
authGuard + quotes.read |
| PATCH | /tenant/quotes/:quoteId/vehicles/:vehicleId |
authGuard + block dispatch package + package manage or quotes.update |
| PUT | /tenant/quotes/:quoteId/vehicles/:vehicleId |
Same as PATCH |
| DELETE | /tenant/quotes/:quoteId/vehicles/:vehicleId |
authGuard + block dispatch package + package manage or quotes.delete |
| GET | /tenant/quotes/:quoteId/others |
authGuard + quotes.read |
| POST | /tenant/quotes/:quoteId/others |
authGuard + block dispatch package + package manage or quotes.create |
| GET | /tenant/quotes/:quoteId/others/:otherId |
authGuard + quotes.read |
| PATCH | /tenant/quotes/:quoteId/others/:otherId |
authGuard + block dispatch package + package manage or quotes.update |
| PUT | /tenant/quotes/:quoteId/others/:otherId |
Same as PATCH |
| DELETE | /tenant/quotes/:quoteId/others/:otherId |
authGuard + block dispatch package + package manage or quotes.delete |
| GET | /tenant/quotes/:quoteId/internal-notes |
authGuard + quotes.read |
| POST | /tenant/quotes/:quoteId/internal-notes |
authGuard + quotes.create |
| GET | /tenant/quotes/:quoteId/internal-notes/:noteId |
authGuard + quotes.read |
| DELETE | /tenant/quotes/:quoteId/internal-notes/:noteId |
authGuard + quotes.delete |
| GET | /tenant/quotes/:quoteId/change-history |
authGuard + quotes.read |
| GET | /tenant/quotes/:id/aqua-pricing |
authGuard + quotes.read |
List / query fields
- Common list window:
search,dateFrom/dateTo,month+year,limit,offset status/quoteStatus,state,leadId,assignedUser,assignedUserId,assignedTo,salesAssignedUserId,assignedTeamcustomer,transportType,trackingNumber,quoteNumber,leadSource,approvalStatusoriginCity/originState/originPostalCode/originZip, destination equivalents,sourceshippingDateFrom/shippingDateTo,isActiveorderBy:createdAt|updatedAt|quoteCreated|shippingDate|amount|quoteNumber
Required create fields
- With
leadId: package/customer can hydrate from the lead - Without
leadId: vehicle (make,model,year) orother(type,name), plustransportType,customerPhone, origin city/state/country, destination city/state/country,shippingDate - Customer email:
POST /tenant/quotes/customer-emailbodyquoteId,subject(1–998), optionalmessage
Enums / lifecycle
| Set | Values |
|---|---|
quoteStatus |
New, Sales Campaign, On Hold, Hot, Warm, Cold, VM, Email, Text, Red, Blue, Green, Yellow, Do not text, Booking Link Sent, Followup Campaign Set, Repeat Customer Campaign, Archived |
approvalStatus |
draft, pending_approval, approved, rejected, cancelled |
PATCH action |
submit, approve, reject, cancel |
state union |
LEAD_STATES + ORDER_PROGRESS_STATES (Booked, Post to load board, Driver Assign) + DISPATCH_STATES (Picked Up, Delivered, Cancelled) |
Package manage
canManageQuotePackages: admin/sub-admin/sales-head, or sales whose assignedUser matches. Dispatch roles blocked from package mutations.
List-scope
Same sales pipeline as leads: all for admin/sub-admin/sales-head; sales sees assignedUser self or quotes whose lead is assigned to self.
SSE
quote.created (scoped), quote.updated, quote.deleted, quote.assigned, quote.converted (on order create from quote), quote.rejected, quote.rework_required.
curl --request POST "{{base_url}}/api/v1/tenant/orders" \
--header "Authorization: Bearer {{access_token}}" \
--header "Content-Type: application/json" \
--data-raw '{ "quoteId": "{{quote_id}}", "customerEmail": "jane@example.com" }'Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/quotes/routes.js |
| Schemas | tenant-api/src/modules/quotes/schemas.js |
| Constants | tenant-api/src/modules/quotes/constants.js |
| Package blocker | src/common/package-access.js |
Orders
Lifecycle after quote approval: list, create (including quote convert), patch, payment status, delivered, cancel, activate, approve/reject, assign carrier, nested packages, BOL (view-only), calendar events, signatures, nested payments, emails. orders.update is seeded for sales / sales-head so operational edits (including attachments) do not require Admin.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/orders/stats |
authGuard + orders.read |
| GET | /tenant/orders |
authGuard + orders.read |
| POST | /tenant/orders |
authGuard + orders.create (or quote-convert exception) + dispatch.assign if assignedTo and not convert |
| POST | /tenant/orders/:id/duplicate |
authGuard + orders.create |
| GET | /tenant/orders/:id |
authGuard + orders.read |
| PATCH | /tenant/orders/:id |
authGuard + sales/sales-head or orders.update; sales-assign + dispatch-assign conditionals |
| PATCH | /tenant/orders/:id/payment-status |
authGuard + roleGuard(admin,sub-admin,sales-head); body paymentStatus pending|paid |
| PATCH | /tenant/orders/:id/delivered |
authGuard + orders.update |
| PATCH | /tenant/orders/:id/cancel |
authGuard + admin/sub-admin/dispatch-head or orders.update |
| PATCH | /tenant/orders/:id/activate |
authGuard + admin/sub-admin/sales-head/dispatch-head |
| DELETE | /tenant/orders/:id |
authGuard + orders.delete |
| GET | /tenant/orders/:orderId/vehicles |
authGuard + orders.read |
| POST | /tenant/orders/:orderId/vehicles |
authGuard + block dispatch package + canManageOrderVehicles or orders.create |
| GET | /tenant/orders/:orderId/vehicles/:vehicleId |
authGuard + orders.read |
| PATCH | /tenant/orders/:orderId/vehicles/:vehicleId |
authGuard + block dispatch package + manage or orders.update |
| DELETE | /tenant/orders/:orderId/vehicles/:vehicleId |
authGuard + block dispatch package + manage or orders.delete |
| GET | /tenant/orders/:orderId/others |
authGuard + orders.read |
| POST | /tenant/orders/:orderId/others |
Same package manage as vehicles |
| GET | /tenant/orders/:orderId/others/:otherId |
authGuard + orders.read |
| PATCH | /tenant/orders/:orderId/others/:otherId |
Package manage |
| DELETE | /tenant/orders/:orderId/others/:otherId |
Package manage |
| GET | /tenant/orders/:orderId/bol |
authGuard + canViewOrderBol |
| GET | /tenant/orders/:orderId/bol/:bolId |
authGuard + canViewOrderBol |
| GET | /tenant/orders/:orderId/calendar-events |
authGuard + canViewOrderCalendarEvents |
| POST | /tenant/orders/:orderId/calendar-events |
authGuard + canManageOrderCalendarEvents |
| GET | /tenant/orders/:orderId/calendar-events/:eventId |
view |
| PATCH | /tenant/orders/:orderId/calendar-events/:eventId |
manage |
| DELETE | /tenant/orders/:orderId/calendar-events/:eventId |
manage |
| GET/POST/PATCH/DELETE | /tenant/orders/:orderId/notes and .../internal-notes |
view/manage/delete helpers in order-access.js; admin/sub-admin always |
| GET | /tenant/orders/:orderId/history |
authGuard + orders.read |
| GET | /tenant/orders/:orderId/signatures |
authGuard + orders.read |
| POST | /tenant/orders/:orderId/signatures |
authGuard + orders.create |
| GET | /tenant/orders/:orderId/signatures/:signatureId |
authGuard + orders.read |
| PATCH | /tenant/orders/:orderId/signatures/:signatureId |
authGuard + orders.update |
| DELETE | /tenant/orders/:orderId/signatures/:signatureId |
authGuard + orders.delete |
| GET | /tenant/orders/:orderId/payments |
authGuard + orders.read |
| POST | /tenant/orders/:orderId/payments |
authGuard + orders.create |
| GET | /tenant/orders/:orderId/payments/:paymentId |
authGuard + orders.read |
| PATCH | /tenant/orders/:orderId/payments/:paymentId |
authGuard + orders.update |
| DELETE | /tenant/orders/:orderId/payments/:paymentId |
authGuard + orders.delete |
| POST | /tenant/orders/:id/approve |
authGuard + orders.update |
| POST | /tenant/orders/:id/reject |
authGuard + orders.update; body reason required |
| POST | /tenant/orders/:id/assign-carrier |
authGuard + carriers.assign; body carrierId |
| DELETE | /tenant/orders/:orderId/carrier/:carrierId |
authGuard + carriers.assign |
| POST | /tenant/orders/:id/send-invoice |
authGuard + orders.send-invoice (email only) |
| POST | /tenant/orders/:id/customer-email |
authGuard + dispatch/dispatch-head or orders.update |
| POST | /tenant/orders/customer-email |
Same |
| POST | /tenant/order/customer-email |
Singular alias, same access |
List / query fields
- Common window +
status(ORDER_STATUSES),orderStatus,secondaryStatus,state leadId,quoteId,assignedTo,assignedUser,assignedUserId,salesAssignedUserId,assignedDepartmentId,assignedRoles(comma),assignedTeamcustomer,transportType,trackingNumber,orderNumber,customerReference,leadSource,approvalStatus,paymentStatus- origin/destination city/state/postal/zip,
source, pickup/delivery date ranges,isActive orderBy:createdAt|updatedAt|orderCreated|pickupDate|deliveryDate|amount|status|orderNumber
Required create / convert fields
- Direct create: optional
quoteId/leadId; most fields optional with defaults (orderStatusdefaultdraft, money defaults 0) - Convert:
quoteIdrequired +customerEmailrequired (convertQuoteToOrderSchema) - Nested payment:
paymentType,amount(positive) - Nested calendar:
title,startAt - Assign carrier:
carrierId - Reject:
reason(min 1) - Send invoice / customer email: optional
emailTemplateId; customer email needssubject
Enums / lifecycle
| Set | Values |
|---|---|
ORDER_STATUSES |
draft, awaiting_approval, awaiting_dispatch, awaiting_client_signature, scheduled, in_transit, delivered, cancelled |
APPROVAL_STATUSES |
draft, pending_approval, approved, rejected, cancelled |
| PATCH payment-status (this module) | pending, paid — not the Accounts enum |
| Carrier pay terms | COD - Cash, COD - Check, Net 15, Net 30, Net 45, Net 60, Prepaid, Other |
| Broker fee terms | Charge on Dispatch, Charge on Delivery, Charge on Pickup, Prepaid, Other |
flowchart LR Quote --> PostOrder PostOrder --> PendingApproval PostOrder --> AwaitingDispatch PendingApproval --> Approve Approve --> AssignCarrier AssignCarrier --> Dispatch
List-scope
- Admin / Sub Admin / dispatch-head: all orders
- dispatch:
assignedToself - sales-head: orders owned by sales/sales-head via quote.assignedUser / lead.assignedTo (not dispatcher)
- sales: same ownership for self only
SSE
order.created (scoped), order.updated, order.deleted, order.assigned, order.approved, order.rejected, order.reactivated, order.requires_approval, order.carrier_assigned / order.carrier_updated / order.carrier_removed, nested vehicle/other/note/signature/payment/calendar_event, plus quote.converted / quote.rejected / dispatch.deleted on related actions.
curl --request GET "{{base_url}}/api/v1/tenant/orders?limit=50&offset=0" \
--header "Authorization: Bearer {{access_token}}"Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/orders/routes.js |
| Schemas | tenant-api/src/modules/orders/schemas.js |
| Constants | tenant-api/src/modules/orders/constants.js |
| Access helpers | src/common/order-access.js |
| List scope | src/common/list-scope.js |
Dispatch
On-road records: state, secondary status, delivered, cancel, vehicles/others, notes, history, BOL (create lives here; orders BOL is view-only), invoice email and customer email. Create requires orderId.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/dispatch/stats |
authGuard + dispatch.read |
| GET | /tenant/dispatch |
authGuard + dispatch.read |
| POST | /tenant/dispatch |
authGuard + dispatch.create + dispatch.assign if assignedTo |
| POST | /tenant/dispatch/:id/duplicate |
authGuard + dispatch.create |
| GET | /tenant/dispatch/:id |
authGuard + dispatch.read |
| PATCH | /tenant/dispatch/:id |
authGuard + dispatch.update + dispatch.assign if assignedTo; JSON or multipart |
| PATCH | /tenant/dispatch/:id/secondary-status |
authGuard + dispatch.update; body secondaryStatus |
| PATCH | /tenant/dispatch/:id/state |
authGuard + dispatch.update; body state, optional reason |
| PATCH | /tenant/dispatch/:id/delivered |
authGuard + dispatch.update |
| PATCH | /tenant/dispatch/:id/cancel |
authGuard + dispatch.update |
| DELETE | /tenant/dispatch/:id |
authGuard + dispatch.delete |
| GET | /tenant/dispatch/:dispatchId/vehicles |
authGuard + dispatch.read |
| POST | /tenant/dispatch/:dispatchId/vehicles |
authGuard + block dispatch package + dispatch.create |
| GET | /tenant/dispatch/:dispatchId/vehicles/:vehicleId |
authGuard + dispatch.read |
| PATCH | /tenant/dispatch/:dispatchId/vehicles/:vehicleId |
authGuard + block dispatch package + dispatch.update |
| DELETE | /tenant/dispatch/:dispatchId/vehicles/:vehicleId |
authGuard + block dispatch package + dispatch.delete |
| GET | /tenant/dispatch/:dispatchId/others |
authGuard + dispatch.read |
| POST | /tenant/dispatch/:dispatchId/others |
authGuard + block dispatch package + dispatch.create |
| GET | /tenant/dispatch/:dispatchId/others/:otherId |
authGuard + dispatch.read |
| PATCH | /tenant/dispatch/:dispatchId/others/:otherId |
authGuard + block dispatch package + dispatch.update |
| DELETE | /tenant/dispatch/:dispatchId/others/:otherId |
authGuard + block dispatch package + dispatch.delete |
| GET | /tenant/dispatch/:dispatchId/internal-notes |
authGuard + notes view helper |
| POST | /tenant/dispatch/:dispatchId/internal-notes |
notes manage helper |
| GET | /tenant/dispatch/:dispatchId/internal-notes/:noteId |
notes view |
| PATCH | /tenant/dispatch/:dispatchId/internal-notes/:noteId |
notes manage |
| DELETE | /tenant/dispatch/:dispatchId/internal-notes/:noteId |
notes delete helper |
| GET | /tenant/dispatch/:dispatchId/change-history |
authGuard + dispatch.read |
| GET | /tenant/dispatch/:dispatchId/bol |
authGuard + BOL view (via linked order) |
| POST | /tenant/dispatch/:dispatchId/bol |
authGuard + canManageDispatchBol |
| GET | /tenant/dispatch/:dispatchId/bol/:bolId |
BOL view |
| PATCH | /tenant/dispatch/:dispatchId/bol/:bolId |
BOL manage |
| DELETE | /tenant/dispatch/:dispatchId/bol/:bolId |
BOL manage |
| POST | /tenant/dispatch/:id/send-invoice |
authGuard + dispatch.send-invoice (email only) |
| POST | /tenant/dispatch/:id/customer-email |
dispatch/dispatch-head or dispatch.update |
| POST | /tenant/dispatch/customer-email |
Same |
List / query fields
search,status,secondaryStatus,state,orderId,carrierId,carrierName,assignedTo,assignedDepartmentId,assignedRolescustomer,transportType,trackingNumber,dispatchNumber,origin,destination, city/state/postal/zip,source,leadSource,primaryRep,referralSource,paymentStatus- pickup/delivery/ship date ranges,
isActive,dateFrom/dateTo,month+year orderBy:createdAt|updatedAt|status|pickupDate|deliveryDate|dispatchedAt|pickedUpAt|deliveredAt|dispatchNumber
Required create fields
orderId(uuid). OptionalcarrierId,statusdefaultconfirmed, assignment, dates, money,attachments.
Enums / lifecycle
| Set | Values |
|---|---|
DISPATCH_STATUSES |
unposted, posted, confirmed, dispatched, picked_up, in_transit, delivered, cancelled |
| Maps to order status | unposted/posted → awaiting_dispatch; confirmed → awaiting_client_signature; dispatched → scheduled; picked_up/in_transit → in_transit; delivered/cancelled same |
DISPATCH_STATES |
Picked Up, Delivered, Cancelled |
| Secondary examples | Awaiting Client Signature, Awaiting Dispatch, Dispatched |
List-scope
Admin / Sub Admin / dispatch-head: all. dispatch: assignedTo self. Others: none.
SSE
dispatch.created (scoped), dispatch.updated, dispatch.deleted, dispatch.assigned, dispatch.delivered, dispatch.cancelled, nested vehicle/other/note/bol; may also emit order.approved, order.carrier_removed, order.note.*.
curl --request GET "{{base_url}}/api/v1/tenant/dispatch?limit=50&offset=0" \
--header "Authorization: Bearer {{access_token}}"Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/dispatch/routes.js |
| Schemas | tenant-api/src/modules/dispatch/schemas.js |
| Constants | tenant-api/src/modules/dispatch/constants.js |
| Lifecycle map | src/common/lifecycle-state.js |
Carriers
Carrier CRUD plus nested drivers, insurance, trailers, other contacts, and attachments. Trailer delete allows Admin/Sub Admin or carriers.delete.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/carriers/stats |
authGuard + carriers.read |
| GET | /tenant/carriers |
authGuard + carriers.read |
| POST | /tenant/carriers |
authGuard + carriers.create |
| GET | /tenant/carriers/:id |
authGuard + carriers.read |
| PATCH | /tenant/carriers/:id |
authGuard + carriers.update |
| POST | /tenant/carriers/:carrierId/attachments |
authGuard + carriers.update |
| DELETE | /tenant/carriers/:id |
authGuard + carriers.delete |
| GET | /tenant/carriers/:carrierId/drivers |
authGuard + carriers.read |
| POST | /tenant/carriers/:carrierId/drivers |
authGuard + carriers.create |
| GET | /tenant/carriers/:carrierId/drivers/:driverId |
authGuard + carriers.read |
| PATCH | /tenant/carriers/:carrierId/drivers/:driverId |
authGuard + carriers.update |
| DELETE | /tenant/carriers/:carrierId/drivers/:driverId |
authGuard + carriers.delete |
| GET | /tenant/carriers/:carrierId/insurance |
authGuard + carriers.read |
| POST | /tenant/carriers/:carrierId/insurance |
authGuard + carriers.create |
| GET | /tenant/carriers/:carrierId/insurance/:insuranceId |
authGuard + carriers.read |
| PATCH | /tenant/carriers/:carrierId/insurance/:insuranceId |
authGuard + carriers.update |
| DELETE | /tenant/carriers/:carrierId/insurance/:insuranceId |
authGuard + carriers.delete |
| GET | /tenant/carriers/:carrierId/trailers |
authGuard + carriers.read |
| POST | /tenant/carriers/:carrierId/trailers |
authGuard + carriers.create |
| GET | /tenant/carriers/:carrierId/trailers/:trailerId |
authGuard + carriers.read |
| PATCH | /tenant/carriers/:carrierId/trailers/:trailerId |
authGuard + carriers.update |
| DELETE | /tenant/carriers/:carrierId/trailers/:trailerId |
Admin/Sub Admin or carriers.delete |
| GET | /tenant/carriers/:carrierId/other-contacts |
authGuard + carriers.read |
| POST | /tenant/carriers/:carrierId/other-contacts |
authGuard + carriers.create |
| GET | /tenant/carriers/:carrierId/other-contacts/:contactId |
authGuard + carriers.read |
| PATCH | /tenant/carriers/:carrierId/other-contacts/:contactId |
authGuard + carriers.update |
| DELETE | /tenant/carriers/:carrierId/other-contacts/:contactId |
authGuard + carriers.delete |
List / query fields
- Carriers:
search,status(new|active|inactive|suspended|archived),limit,offset,orderBy(name|status|mcNumber|createdAt|updatedAt) - Drivers:
search,status(active|inactive), pagination,orderBydriverName|status|createdAt|updatedAt - Insurance:
search,status(active|expired|inactive),dateFrom/dateTo,orderBydefaultexpirationDate - Trailers / other-contacts:
search,status, pagination; contacts alsocontactType
Required create fields
- Carrier:
name(1–150). Optional MC, phones, emails, capabilities booleans,attachments[] - Driver:
driverName - Insurance:
policyNumber,insuranceCompany,expirationDate - Trailer:
trailerType - Other contact:
contactName
SSE
carrier.created / updated / deleted; carrier.driver.*; carrier.insurance.*; carrier.trailer.*; carrier.otherContact.*.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/carriers/routes.js |
| Schemas | tenant-api/src/modules/carriers/schemas.js |
Customers
Customer book and sales assignment. Assign is sales.assign. Create allows admin/sub-admin/sales-head/sales or customers.create. Update allows admin/sub-admin/sales-head or customers.update.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/customers/stats |
authGuard + customers.read |
| GET | /tenant/customers |
authGuard + customers.read |
| POST | /tenant/customers |
authGuard + role exception or customers.create |
| GET | /tenant/customers/:id |
authGuard + customers.read |
| PATCH | /tenant/customers/:id |
authGuard + role exception or customers.update |
| PATCH | /tenant/customers/:id/assign-sales |
authGuard + sales.assign |
List / query fields
- Common list window +
isActive,state,city,postalCode/postalZip,companyName orderBy:email|name|companyName|createdAt|updatedAt|leadsCount|quotesCount|ordersCount
Required create fields
name(1–180). Eitheremailorphonerequired. Optional address fields:companyName,address,address2,city,state,postalCode,country- Assign sales:
assignedSalesUserId(optional uuid / none token → null)
Enums
status: active | inactive.
SSE
Assigning sales can emit lead.assigned / quote.assigned to the new assignee when linked records move.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/customers/routes.js |
| Schemas | tenant-api/src/modules/customers/schemas.js |
| Sales assignee helper | src/common/sales-assignee.js |
Invoices
Brokerage invoice records billed against an order (orderId + amount). This is not the Accounts workbench and not Owner platform invoices.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /invoices |
authGuard + invoices.read |
| POST | /invoices |
authGuard + invoices.create |
| GET | /invoices/:id |
authGuard + invoices.read |
| PATCH | /invoices/:id |
authGuard + invoices.update |
| DELETE | /invoices/:id |
authGuard + invoices.delete |
List / query fields
search,status,orderId,dateFrom,dateTo,limit,offset,orderBy(createdAt|updatedAt|dueDate|amount|status)
Required create fields
orderId(uuid),amount(number). Optionalstatus(defaultpending),dueDate.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/invoices/routes.js |
| Schemas | tenant-api/src/modules/invoices/schemas.js |
Payments
Brokerage payment records against an invoice row (invoiceId). This is not nested order payments (/tenant/orders/:orderId/payments) and not Accounts paymentStatus on the order.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /payments |
authGuard + payments.read |
| POST | /payments |
authGuard + payments.create |
| GET | /payments/:id |
authGuard + payments.read |
| PATCH | /payments/:id |
authGuard + payments.update |
| DELETE | /payments/:id |
authGuard + payments.delete |
List / query fields
search,status,invoiceId,method,dateFrom,dateTo,limit,offset,orderBy(createdAt|updatedAt|amount|status)
Required create fields
invoiceId(uuid),amount. Optionalmethod,status(defaultpending).
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/payments/routes.js |
| Schemas | tenant-api/src/modules/payments/schemas.js |
Accounts
Accounts workbench over orders in awaiting_client_signature (ACCOUNT_ORDER_STATUS). Permissions: accounts.read, accounts.send_invoice, accounts.update_payment. :id is an order id.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/accounts/stats |
authGuard + accounts.read |
| GET | /tenant/accounts |
authGuard + accounts.read |
| GET | /tenant/accounts/:id |
authGuard + accounts.read |
| POST | /tenant/accounts/:id/send-invoice |
authGuard + accounts.send_invoice |
| PATCH | /tenant/accounts/:id/payment-status |
authGuard + accounts.update_payment |
List / query fields
- Common list window +
paymentStatus,paymentMethod,orderId,dispatchId,assignedTo,invoiceSentBy,transportType minAmount/maxAmount,isActiveorderBy:createdAt|updatedAt|totalTariff|paymentStatus|invoiceSentAt|orderNumber|dispatchNumber
Bodies
- Send invoice: optional
emailTemplateId - Payment status: at least one of
paymentStatus,commissionPaidOn,paymentDate partial/paidrequirepaidAmount> 0commissionPaidOnnon-null requirespaymentDatein the same requestpaymentDatemust be on/after ordercreatedAtand no later than ~2 months from today
Enums
| Set | Values |
|---|---|
PAYMENT_STATUSES |
pending, invoice_sent, partial, paid, refunded, cancelled, overdue |
PAYMENT_METHODS |
credit_card, debit_card, ach, wire_transfer, check, cash, zelle, paypal, other |
COMMISSION_PAID_ON_OPTIONS |
Slot booking, On dispatch, on Pickup, after delivery fee, Partial payment |
SSE
accounts.invoice_sent, accounts.payment_updated.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/accounts/routes.js |
| Schemas | tenant-api/src/modules/accounts/schemas.js |
| Constants | tenant-api/src/modules/accounts/constants.js |
Bulk operations
Multi-select actions from CRM tables. Ids arrays are 1–100 uuids. Each route uses the same permission the single-record action would need, plus assign permissions when an assignee is in the body.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| POST | /tenant/bulk/leads/reassign |
authGuard + leads.update + sales.assign if assignedTo set |
| POST | /tenant/bulk/leads/convert-to-quote |
authGuard + leads.create |
| POST | /tenant/bulk/quotes/reassign |
authGuard + quotes.update + sales.assign if assignedUser set |
| POST | /tenant/bulk/quotes/convert-to-order/preview |
authGuard + quotes.read |
| POST | /tenant/bulk/quotes/convert-to-order |
authGuard + orders.create |
| POST | /tenant/bulk/orders/reassign |
authGuard + orders.update + dispatch.assign if assignedTo set |
| POST | /tenant/bulk/orders/assign-carrier/preview |
authGuard + orders.read |
| POST | /tenant/bulk/orders/assign-carrier |
authGuard + carriers.assign |
| POST | /tenant/bulk/dispatch/reassign |
authGuard + dispatch.update + dispatch.assign if assignedTo set |
Bodies
| Action | Body |
|---|---|
| Lead reassign | ids[], assignedTo (uuid|null), optional assignedTeam |
| Lead convert | ids[], optional additionalQuoteData |
| Quote reassign | ids[], assignedUser (uuid|null), optional assignedTeam |
| Quote convert preview | ids[] |
| Quote convert | items[{ quoteId, totalTariff, carrierPay, brokerFee, customerEmail?, ... }], optional shared |
| Order reassign | ids[], assignedTo, optional assignedTeam |
| Assign carrier preview | ids[] |
| Assign carrier | items[{ orderId, carrierId }] |
| Dispatch reassign | ids[], assignedTo, optional assignedTeam |
SSE
Bulk convert emits quote.created / order.created via emitBulkOperationEvent. Individual reassign still uses module assignment events.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/bulk-operations/routes.js |
| Schemas | tenant-api/src/modules/bulk-operations/schemas.js |
Marketing
Campaigns and list imports. Authenticated only (authGuard); no permissionGuard on these routes. Import completion can create leads and emit lead.created.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /marketing/campaigns |
authGuard |
| POST | /marketing/campaigns |
authGuard |
| PATCH | /marketing/campaigns/:id |
authGuard |
| DELETE | /marketing/campaigns/:id |
authGuard |
| GET | /marketing/imports |
authGuard |
| POST | /marketing/imports |
authGuard |
| PATCH | /marketing/imports/:importId |
authGuard |
Required create fields
- Campaign:
name,channel. Optionalstatus(defaultdraft),budget,startAt,endAt - Start import:
source - Update import: optional
status, row counts,leads[{ title, source?, amount?, notes? }]
SSE
Import-created leads emit lead.created to the tenant from the marketing controller.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/marketing/routes.js |
| Schemas | tenant-api/src/modules/marketing/schemas.js |
Calendar
Standalone events at /calendar/events. Order-nested calendar lives under /tenant/orders/:orderId/calendar-events (see Orders). Standalone routes are authGuard only.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /calendar/events |
authGuard |
| POST | /calendar/events |
authGuard |
| PATCH | /calendar/events/:id |
authGuard |
| DELETE | /calendar/events/:id |
authGuard |
List / query fields
userId,from,to(ISO datetime). No pagination schema on this list.
Required create fields
title,startAt. Optionaldescription,endAt,location,userId.
SSE
calendar.created ({ id, ... }), calendar.updated, calendar.deleted. Order nested events use order.calendar_event.*.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/calendar/routes.js |
| Schemas | tenant-api/src/modules/calendar/schemas.js |
| Reminder job | tenant-api/src/jobs/calendar/send-reminders.js |
Notifications
In-app notifications plus Web Push subscribe/unsubscribe and preferences. VAPID public key is on Settings (GET /tenant/settings/vapid-public-key). Default list limit is 10, not 50.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/notifications/stats |
authGuard |
| PATCH | /tenant/notifications/read-all |
authGuard |
| GET | /tenant/notifications |
authGuard |
| GET | /tenant/notifications/:id |
authGuard |
| POST | /tenant/notifications |
authGuard |
| PATCH | /tenant/notifications/:id/read |
authGuard |
| DELETE | /tenant/notifications/:id |
authGuard |
| POST | /tenant/notifications/push/subscribe |
authGuard |
| POST | /tenant/notifications/push/unsubscribe |
authGuard |
| GET | /tenant/notifications/push/subscriptions |
authGuard |
| GET | /tenant/notifications/push/preferences |
authGuard |
| PATCH | /tenant/notifications/push/preferences |
authGuard |
List / query fields
status(all|read|unread, defaultall)userIdlimit(default 10),offsetsearch
Required create / push fields
- Create:
title. OptionaluserId(null = tenant-wide),category(defaultgeneral),body,path,data - Mark read:
{ read: boolean }default true - Push subscribe:
endpoint(url),keys.p256dh,keys.auth
SSE
notify.info, notify.read, notify.all_read, notify.deleted. Notification-service also pushes notify.info per recipient.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/notifications/routes.js |
| Schemas | tenant-api/src/modules/notifications/schemas.js |
| Notification service | src/common/notification-service.js |
Email templates
List is open to any authenticated user. Get-by-id requires email-templates.read. Create/update/delete require Admin, Sub Admin, or a department head role (sales-head, dispatch-head, marketing-head, accounts-head). Inactive templates are filtered in the list service unless status=all.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/email-templates |
authGuard |
| GET | /tenant/email-templates/:id |
authGuard + email-templates.read |
| POST | /tenant/email-templates |
authGuard + roleGuard(admin, sub-admin, *-head) |
| PATCH | /tenant/email-templates/:id |
same roleGuard |
| DELETE | /tenant/email-templates/:id |
same roleGuard |
List / query fields
status(all|active|inactive, defaultactive)searchlimit(1–100, default 50),offsetorderBytitle|updatedAt|createdAt(defaultupdatedAt)
Required create fields
title(1–255). Optionalpayloadobject (default{}),status(active|inactive, defaultactive).
SSE
email-template.created, email-template.updated, email-template.deleted.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/email-templates/routes.js |
| Schemas | tenant-api/src/modules/email-templates/schemas.js |
| Constants | tenant-api/src/modules/email-templates/constants.js |
Files
Structured file metadata at /api/v1/files. This is not the static /uploads/* tree used by quote/order/dispatch attachments.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /files |
authGuard |
| POST | /files |
authGuard |
| PATCH | /files/:id |
authGuard |
| DELETE | /files/:id |
authGuard |
List / query fields
tagoptional.
Required create fields
originalName,mimeType,size. ProvidestorageKeyorbufferBase64. Optionaltags[]. PATCH only updatestags.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/files/routes.js |
| Schemas | tenant-api/src/modules/files/schemas.js |
Search
Global search, recent searches, and lifecycle-number lookup. All three require search.view. Results are permission- and list-scope-filtered (leads/quotes/orders/dispatch follow the same assignee rules as list endpoints).
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/search |
authGuard + search.view |
| GET | /tenant/search/recent |
authGuard + search.view |
| GET | /tenant/search/lifecycle-number |
authGuard + search.view |
Query fields
- Global:
term(default''),limit(1–25, default 10) - Recent:
limit(1–20, default 10),daysAgo(1–30, default 30) - Lifecycle number: at least one of
number,lead_number,quote_number,order_number,dispatch_number,tracking_number;limitper entity table (1–500, default 100)
Entity buckets include leads, quotes, orders, dispatches, carriers, customers, invoices, knowledge base, campaigns, calendar events, users, plus nav modules filtered by permission.
curl --request GET "{{base_url}}/api/v1/tenant/search?term=civic&limit=10" \
--header "Authorization: Bearer {{access_token}}"Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/search/routes.js |
| Schemas | tenant-api/src/modules/search/schemas.js |
| Service | tenant-api/src/modules/search/service.js |
Knowledge base
Internal articles. authGuard only. Prefix is /api/v1/knowledge-base/entries, not under /tenant/.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /knowledge-base/entries |
authGuard |
| POST | /knowledge-base/entries |
authGuard |
| PATCH | /knowledge-base/entries/:id |
authGuard |
| DELETE | /knowledge-base/entries/:id |
authGuard |
List / query fields
category,tag
Required create fields
title,content. Optionaltags[],category.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/knowledge-base/routes.js |
| Schemas | tenant-api/src/modules/knowledge-base/schemas.js |
AI pricing
Quote helper. Route preHandler is authGuard; the controller also calls authGuard and requires tenant context. Configure the AI key in Settings integrations (ai_api_key, stored encoded). Related helper: GET /tenant/quotes/:id/aqua-pricing (quotes.read).
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| POST | /ai-pricing/quote |
authGuard |
Required body fields
origin,destination. Optionalmake,model,year(1900–2100),type,brokerFee(min 50),quoteId.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/ai-pricing/routes.js |
| Schemas | tenant-api/src/modules/ai-pricing/schemas.js |
Analytics
Role-aware dashboards. All routes require analytics.view. Team dashboard SSE (analytics.team_dashboard.updated) is emitted from sales/dispatch mutations, not from these GET routes.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/analytics/dashboard |
authGuard + analytics.view |
| GET | /tenant/analytics/departments |
authGuard + analytics.view |
| GET | /tenant/analytics/roles |
authGuard + analytics.view |
| GET | /tenant/analytics/users/me |
authGuard + analytics.view |
| GET | /tenant/analytics/users/:userId |
authGuard + analytics.view |
Dashboard query fields
shipmentStatisticsYear,shippingLocationYear,shippingLocationMonthrevenueYear,commissionStatisticsYear,commissionStatisticsMonthstatsYear,statsMonth
SSE
analytics.team_dashboard.updated with { team: sales|dispatch, date } (tenant timezone default America/Chicago).
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/analytics/routes.js |
| Schemas | tenant-api/src/modules/analytics/schemas.js |
| Team dashboard SSE | src/common/team-dashboard-sse.js |
Nav counts
Pipeline totals for CRM sidebar badges (leads, quotes, orders, dispatch). authGuard only. Each bucket is filled only if the JWT has the matching *.read permission; otherwise that key stays 0. Totals use the same stats/list-scope as the module lists (active pipeline defaults).
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/nav-counts |
authGuard |
Response data shape: { leads, quotes, orders, dispatch } numbers.
curl --request GET "{{base_url}}/api/v1/tenant/nav-counts" \
--header "Authorization: Bearer {{access_token}}"Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/nav-counts/routes.js |
| Service | tenant-api/src/modules/nav-counts/service.js |
Templates
Owner-published document templates cached on the tenant. View, diff, history. Apply is on Admin (POST /admin/apply-template), not here.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/templates |
authGuard + templates.view |
| GET | /tenant/templates/diff |
authGuard + templates.view |
| GET | /tenant/templates/history |
authGuard + templates.view |
| GET | /tenant/templates/:key |
authGuard + templates.view |
Query fields
- List:
typeoptional - History:
since,limit(numeric)
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/templates/routes.js |
| Schemas | tenant-api/src/modules/templates/schemas.js |
Settings
Tenant settings, notification channels, integrations (stored encoded; returned encoded), VAPID key, and sourceOrigins / source_origins for public lead capture. Origin values are scheme + host, no path. Team sourceOrigins override tenant origins when both match.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/settings |
authGuard |
| GET | /tenant/settings/email-template-department-keys |
authGuard |
| PATCH | /tenant/settings |
authGuard + roleGuard(admin,sub-admin) |
| PATCH | /tenant/settings/notifications |
authGuard + roleGuard(admin,sub-admin) |
| PATCH | /tenant/settings/integrations |
authGuard + roleGuard(admin,sub-admin) |
| GET | /tenant/settings/vapid-public-key |
authGuard |
PATCH fields
- Settings:
commissionValue,commissionType(flat|percentage),organizationName,ownerName,timezone,ipRestrictionEnabled,allowedIps[](max 100),companyInformation{...},sourceOrigins[]/source_origins[](urls),notifications{ email, inApp, push },integrations - Notifications:
email,inApp,pushbooleans - Integrations:
ai_api_key,zoom{ apiKey, apiSecret, accountId },googleMaps{ apiKey },emailService{ provider, apiKey, fromEmail, fromName, smtpHost, smtpPort, user, password }
SSE
settings.updated, settings.integrations_updated.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/settings/routes.js |
| Schemas | tenant-api/src/modules/settings/schemas.js |
| Constants | tenant-api/src/modules/settings/constants.js |
Public profiles
Unauthenticated profile by public unique id (the personal URL slug). No authGuard. Invalid slug format → 400; missing → 404.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/public/profiles/:uniqueId |
Public |
Response includes data.user (presented profile + uniqueId, successDispatches, roles) and organizationName.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/public-profiles/routes.js |
| Unique id format | tenant-api/src/modules/users/unique-id.js |
eDocs
Send a sign link from the CRM (quotes.update). Public verify/submit/me/refresh are origin-gated for the eDocs site. Session JWT must have tokenType === 'edoc_access' and edocId for /public/me.
flowchart LR SendLink --> OpenDoc OpenDoc --> VerifyCustomer VerifyCustomer --> Sign Sign --> SignedPdf
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| POST | /tenant/edocs-platform/send-sign-link |
authGuard + quotes.update |
| GET | /tenant/edocs-platform/public/:edocNumber |
Public, origin-gated (edocNumber 10 alphanumeric) |
| POST | /tenant/edocs-platform/public/:edocNumber/submit |
Public, origin-gated; body submission object |
| POST | /tenant/edocs-platform/public/:id/verify-customer |
Public, origin-gated; id uuid; email or phone + edocNumber |
| POST | /tenant/edocs-platform/public/auth/refresh |
Public, origin-gated; refreshToken min 20 |
| GET | /tenant/edocs-platform/public/me |
Public origin + eDoc access JWT |
SSE
On signed submit: edoc.signed plus quote.updated / order.updated / lead.updated / dispatch.updated when those ids exist on the eDoc.
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/edocs-platform/routes.js |
| Schemas | tenant-api/src/modules/edocs-platform/schemas.js |
| Signed PDF | tenant-api/src/modules/edocs-platform/edoc-signed-pdf.js |
Desktop app
Authenticated download of the Windows installer. File is public/uploads/dispatchly-v1.0.0.exe. 404 if missing. Content-Disposition: attachment.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /desktop-app/download |
authGuard |
curl --request GET "{{base_url}}/api/v1/desktop-app/download" \
--header "Authorization: Bearer {{access_token}}" \
--output dispatchly-v1.0.0.exeImplementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/desktop-app/routes.js |
| Controller | tenant-api/src/modules/desktop-app/controller.js |
Activity logs
Privileged audit of user actions. List is Admin / Sub Admin only. Creates also SSE activity-log.created to privileged viewers.
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/activity-logs |
authGuard + roleGuard(admin,sub-admin) |
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/activity-logs/routes.js |
| Service | tenant-api/src/modules/activity-logs/service.js |
Request logs
HTTP request audit for Admin / Sub Admin. Filter by method, status, risk flags. status list-query key is omitted so it does not collide with HTTP status filtering (statusCode).
Routes
All paths below are relative to /api/v1 unless noted. Send Authorization: Bearer {{access_token}} on every authGuard route.
| Method | Path | Access |
|---|---|---|
| GET | /tenant/request-logs |
authGuard + roleGuard(admin,sub-admin) |
| GET | /tenant/request-logs/:id |
authGuard + roleGuard(admin,sub-admin) |
List / query fields
- Common window (
search,dateFrom/dateTo,month+year,limit,offset) minusstatus method(max 12 chars),statusCode,flaggedOnly(true/false),riskMin(0–100)orderBy:createdAt|riskScore
Implementation map
| Piece | Path |
|---|---|
| Routes | tenant-api/src/modules/request-logs/routes.js |
| Schemas | tenant-api/src/modules/request-logs/schemas.js |