{"openapi":"3.0.0","paths":{"/api/v1/ping":{"get":{"description":"Returns the workspace and key identity the credential resolves to, including the key's scopes. Use this to test connectivity and confirm which workspace a key belongs to.","operationId":"ApiV1Controller_ping","parameters":[],"responses":{"200":{"description":"Key is valid","content":{"application/json":{"schema":{"example":{"ok":true,"tenantId":"cmrpgygdl00xcqyrjt7871dbr","user":{"email":"dispatch@acmelogistics.com","role":"BROKER"},"key":{"name":"erp-sync","prefix":"hkpt_AbC12de","scopes":["read","write"]}}}}}},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Validate your API key","tags":["Customer API v1"]}},"/api/v1/verifications":{"get":{"description":"Carrier verifications in your workspace, newest first. `riskScore` is the **Trust score**: 0–100, higher = safer, null until the checks finish (a running verification has no score and no `decision` yet). The app always shows it with its band — 70 or higher is Low risk, above 30 Medium risk, 30 or below High risk (a 30 is High risk).\n\n**Pagination is cursor-based.** Pass the previous response's `nextCursor` back as `cursor` while `hasMore` is true. There is no `page` parameter and no `total` — sending `page` returns 400.\n\n**Two status vocabularies — this trips people up.** The `status` *filter* takes four stable keys (values to send, not words to show); each row's `status` *field* is the raw lifecycle value, and `decision` (green/yellow/red) only refines a FINISHED row — on a running one it is provisional and changes as each step completes. The mapping, with the verdict the app shows for those rows:\n\n| `status` filter | matches rows where | the app shows |\n|---|---|---|\n| `DRAFT` | `status = draft` | Draft |\n| `PENDING` | `status` is `initiated` or `in_progress` (any `decision`), **or** `status = completed` with `decision` `yellow` or null | In progress, or Needs your review once finished |\n| `APPROVED` | `status = completed` and `decision = green` | OK to book — Expired once `validUntil` has passed |\n| `REJECTED` | `status = failed`, **or** `status = completed` and `decision = red` | Do not book |\n\nA cancelled verification (`status = cancelled`) matches none of the four. The raw `status` field is one of: `draft`, `initiated`, `in_progress`, `completed`, `failed`, `cancelled`. To decide whether you may book a carrier, branch on `status` first (only `completed` and `failed` are decided), then `decision`, then `validUntil`.","operationId":"ApiV1Controller_listVerifications","parameters":[{"name":"cursor","required":false,"in":"query","description":"The previous response's `nextCursor`. Omit for the first page.","schema":{}},{"name":"limit","required":false,"in":"query","description":"Page size, 1–100 (default 50). Out of range returns 400.","schema":{}},{"name":"sort","required":false,"in":"query","description":"Sort order (default updatedAt:desc)","schema":{"enum":["updatedAt:asc","updatedAt:desc","riskScore:asc","riskScore:desc"],"type":"string"}},{"name":"minRisk","required":false,"in":"query","description":"Only verifications with riskScore ≥ this value (0–100)","schema":{}},{"name":"q","required":false,"in":"query","description":"Free-text search across carrier name, MC/DOT, and dispatcher name","schema":{}},{"name":"status","required":false,"in":"query","description":"Pipeline status filter (stable keys) — see the mapping in the description: APPROVED = OK to book, REJECTED = Do not book, PENDING = In progress or Needs your review. NOT the same vocabulary as the response `status` field","schema":{"enum":["DRAFT","PENDING","APPROVED","REJECTED"],"type":"string"}}],"responses":{"200":{"description":"Cursor-paginated verification list","content":{"application/json":{"schema":{"example":{"verifications":[{"id":"cmrpgyhaj01ksqyrjzmza5xzx","brokerId":"cmrpgyghh00xjqyrj68shgikr","carrierId":"cmrpgygko00yjqyrjen141lcf","dispatcherName":"Angela Doss","status":"initiated","decision":null,"riskScore":null,"ownerCompleted":false,"createdAt":"2026-07-17T09:00:00.000Z","updatedAt":"2026-07-17T21:47:50.155Z","carrier":{"mcNumber":"1499417","dotNumber":"3931949","legalName":"Cumberland Freight Systems Inc","dbaName":"CFS Trucking"}}],"hasMore":true,"nextCursor":"cmrpgyhaj01ksqyrjzmza5xzx","limit":50}}}}},"400":{"description":"Unknown query param, out-of-range `limit`, malformed `cursor`, or `page` (unsupported — use `cursor`)"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"List verifications","tags":["Customer API v1"]},"post":{"description":"Starts a carrier verification exactly as the app's New verification wizard does — same credit check (402 when the company is out of credits), same daily limit, same carrier lookup and checks, same emails to the carrier's owner. Send `mcNumber` OR `dotNumber`. For a carrier that books through a dispatcher (the default), `dispatcherName` and a 10-digit `dispatcherPhone` are required — the owner is asked to confirm them. Send `operatingModel: \"owner_operator\"` for a carrier whose owner books their own freight. If the carrier was already verified today the answer is a 409 `VERIFICATION_DUPLICATE_TODAY` — send `confirmDuplicate: true` to start another one (another credit). `start: false` saves a draft instead (no checks, no credit). Requires a key with the \"verifications:create\" scope (Settings → API Keys → \"Create and start verifications\", company admins only). Send an `Idempotency-Key` header to make retries safe.","operationId":"ApiV1Controller_createVerification","parameters":[{"name":"Idempotency-Key","in":"header","description":"Same rules as POST /v1/loads.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"example":{"mcNumber":"1499417","dispatcherName":"Angela Doss","dispatcherPhone":"(614) 555-0142","shipperEmail":"ops@prairieharvest.example"}}}}},"responses":{"201":{"description":"The verification (status `initiated`, or `draft` when start is false)"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}},"403":{"description":"Key lacks the \"verifications:create\" scope (403 MISSING_API_SCOPE)"}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Create and start a verification","tags":["Customer API v1"]}},"/api/v1/verifications/{id}":{"get":{"description":"The full verification record: carrier snapshot at verification time, dispatcher/owner confirmation state, insurance checks, the Trust score (riskScore — 0–100, higher is safer) with its contributing signals, decision, and timestamps.","operationId":"ApiV1Controller_getVerification","parameters":[{"name":"id","required":true,"in":"path","description":"Verification id (from the list endpoint or the app URL)","schema":{"type":"string"}}],"responses":{"200":{"description":"Verification detail (superset of the list row)","content":{"application/json":{"schema":{"example":{"verification":{"id":"cmrpgyhaj01ksqyrjzmza5xzx","brokerId":"cmrpgyghh00xjqyrj68shgikr","carrierId":"cmrpgygko00yjqyrjen141lcf","dispatcherName":"Angela Doss","status":"initiated","decision":null,"riskScore":null,"ownerCompleted":false,"createdAt":"2026-07-17T09:00:00.000Z","updatedAt":"2026-07-17T21:47:50.155Z","carrier":{"mcNumber":"1499417","dotNumber":"3931949","legalName":"Cumberland Freight Systems Inc","dbaName":"CFS Trucking"}}}}}}},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Get one verification","tags":["Customer API v1"]}},"/api/v1/carriers":{"get":{"description":"Your workspace's carrier roster (derived from its verifications). Underscore-prefixed fields are workspace-level aggregates: `_verifiedAt` (latest verification), `_decision`, `_loadsCount`, `_onTimeRate`, `_blacklisted`.","operationId":"ApiV1Controller_listCarriers","parameters":[{"name":"offset","required":false,"in":"query","description":"Offset for pagination (default 0)","schema":{}},{"name":"limit","required":false,"in":"query","description":"Page size, 1–200 (default 50). Out of range returns 400.","schema":{}},{"name":"equipment","required":false,"in":"query","description":"Equipment type filter (e.g. van, reefer, flatbed)","schema":{}},{"name":"status","required":false,"in":"query","description":"Carrier operating-status filter","schema":{}},{"name":"decision","required":false,"in":"query","description":"Filter by the verdict of each carrier's NEWEST verification: `green` = OK to book (finished green, `validUntil` not passed), `yellow` = Needs your review (finished yellow or undecided), `red` = Do not book (failed, or finished red). A still-running or expired verification matches none of them.","schema":{}},{"name":"verifiedOnly","required":false,"in":"query","description":"Set to \"true\" to only return carriers that are OK to book — the same as `decision=green`","schema":{}},{"name":"q","required":false,"in":"query","description":"Free-text search (name, MC, DOT)","schema":{}}],"responses":{"200":{"description":"Paginated carrier list","content":{"application/json":{"schema":{"example":{"items":[{"id":"cmrpgygko00yjqyrjen141lcf","mcNumber":"1499417","dotNumber":"3931949","legalName":"Cumberland Freight Systems Inc","status":"Y","insuranceOnFile":true,"_verifiedAt":"2026-07-17T09:00:00.000Z","_decision":null,"_loadsCount":4,"_onTimeRate":null,"_grade":null,"_rating":null,"_blacklisted":false,"_equipment":["van"]}],"total":6,"limit":50,"offset":0}}}}},"400":{"description":"Unknown query param or out-of-range `limit`"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"List carriers","tags":["Customer API v1"]}},"/api/v1/carriers/{id}":{"get":{"description":"Carrier detail aggregate: authority and operating status, insurance on file, filtered contacts (insurance-agent and fax noise removed), verification history, and the monitored change log (phone/address/authority changes over time).","operationId":"ApiV1Controller_getCarrier","parameters":[{"name":"id","required":true,"in":"path","description":"Carrier id (from the list endpoint or a verification's carrierId)","schema":{"type":"string"}}],"responses":{"200":{"description":"Carrier detail"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Get one carrier","tags":["Customer API v1"]}},"/api/v1/customers":{"get":{"description":"Your CRM parties — shipper customers, prospects, and vendors. Use the returned `id` as `partyId` when creating loads. Requires the TMS CRM module on your plan (403 CRM_MODULE otherwise). Archived parties are excluded unless `includeArchived=true`.","operationId":"ApiV1Controller_listCustomers","parameters":[{"name":"offset","required":false,"in":"query","description":"Offset for pagination (default 0)","schema":{}},{"name":"limit","required":false,"in":"query","description":"Page size, 1–200 (default 50)","schema":{}},{"name":"includeArchived","required":false,"in":"query","description":"\"true\" to include archived parties","schema":{}},{"name":"updatedAfter","required":false,"in":"query","description":"Only parties updated after this ISO date (incremental sync)","schema":{}},{"name":"tag","required":false,"in":"query","description":"Tag filter (repeatable)","schema":{}},{"name":"industry","required":false,"in":"query","description":"Industry filter","schema":{}},{"name":"creditTier","required":false,"in":"query","description":"Credit tier filter (repeatable)","schema":{}},{"name":"status","required":false,"in":"query","description":"Party status filter (e.g. active)","schema":{}},{"name":"type","required":false,"in":"query","description":"Party type: customer, prospect, vendor (repeatable)","schema":{}},{"name":"q","required":false,"in":"query","description":"Free-text search on name/legal name","schema":{}}],"responses":{"200":{"description":"Paginated customer list with facet counts","content":{"application/json":{"schema":{"example":{"parties":[{"id":"cmrpgygm900yxqyrjy42el8a3","name":"Bluegrass Beverage Distributors","type":"customer","status":"active","creditTier":"unrated","legalName":"Bluegrass Beverage Distributors","dbaName":null,"mcNumber":null,"dotNumber":null,"taxExempt":false,"tags":[]}],"total":12,"limit":50,"offset":0,"facets":{"byType":{"customer":10,"prospect":2},"byStatus":{"active":12},"byCreditTier":{"unrated":9,"good":3}}}}}}},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"List customers","tags":["Customer API v1"]}},"/api/v1/customers/{id}":{"get":{"description":"Customer detail: contacts, addresses, billing rules, credit standing (limit, terms, balance), and activity.","operationId":"ApiV1Controller_getCustomer","parameters":[{"name":"id","required":true,"in":"path","description":"Customer (party) id","schema":{"type":"string"}}],"responses":{"200":{"description":"Customer detail"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Get one customer","tags":["Customer API v1"]}},"/api/v1/loads":{"get":{"description":"TMS loads for your workspace — same filters as the web load list. Requires the TMS loads module on your plan (403 otherwise).\n\n**Pagination.** Cursor is preferred: pass `nextCursor` from a response back as `cursor` while `hasMore` is true. `offset` is also honoured for warehouse-style paging (`?limit=50&offset=100`); if both are supplied, `cursor` wins. `total` is -1 by design — it is not computed, for performance.\n\n**Unknown query params are rejected with a 400** — a misspelled filter used to be ignored and returned the whole unfiltered list.\n\nLoad statuses: draft, available, posted, committed, assigned, dispatched, at_pickup, picked_up, enroute, at_delivery, delivered, invoice_audit, invoice_pending, invoiced, carrier_paid, customer_paid, completed, missed_pickup, tonu, ops_hold, accounting_hold, claim_filed, cancelled, voided.\n\n`dispatchStage` is a *separate, narrower* vocabulary derived from `status` — pending, available, committed, dispatched, at_pickup, in_transit, at_delivery, delivered. Note `enroute` is a status, never a stage (it derives to `in_transit`).","operationId":"ApiV1Controller_listLoads","parameters":[{"name":"offset","required":false,"in":"query","description":"Row offset for non-cursor paging (default 0). Ignored when `cursor` is supplied.","schema":{}},{"name":"cursor","required":false,"in":"query","description":"Cursor from a previous response's nextCursor. Malformed values return 400 (they used to return an empty page).","schema":{}},{"name":"limit","required":false,"in":"query","description":"Page size, 1–200 (default 50). Out of range returns 400.","schema":{}},{"name":"sortDir","required":false,"in":"query","schema":{"enum":["asc","desc"],"type":"string"}},{"name":"sortBy","required":false,"in":"query","schema":{"enum":["createdAt","updatedAt","pickupDate","deliveryDate","loadNumber"],"type":"string"}},{"name":"pickupTo","required":false,"in":"query","description":"Pickup date window end (ISO date)","schema":{}},{"name":"pickupFrom","required":false,"in":"query","description":"Pickup date window start (ISO date)","schema":{}},{"name":"destinationState","required":false,"in":"query","description":"Destination state (2-letter)","schema":{}},{"name":"originState","required":false,"in":"query","description":"Origin state (2-letter)","schema":{}},{"name":"reference","required":false,"in":"query","description":"Any reference number (PO, sales order, container, …)","schema":{}},{"name":"bolNumber","required":false,"in":"query","description":"BOL number filter","schema":{}},{"name":"mode","required":false,"in":"query","description":"ftl or ltl","schema":{}},{"name":"equipmentType","required":false,"in":"query","description":"Equipment filter (van, reefer, flatbed, …)","schema":{}},{"name":"mcNumber","required":false,"in":"query","description":"Carrier MC number filter","schema":{}},{"name":"carrierId","required":false,"in":"query","description":"Filter to one carrier","schema":{}},{"name":"partyId","required":false,"in":"query","description":"Filter to one customer","schema":{}},{"name":"loadNumber","required":false,"in":"query","description":"Exact/partial load number","schema":{}},{"name":"q","required":false,"in":"query","description":"Free-text search","schema":{}},{"name":"dispatchStage","required":false,"in":"query","description":"Operational pipeline stage filter (repeatable) — derived from status, not the same vocabulary","schema":{"enum":["pending","available","committed","dispatched","at_pickup","in_transit","at_delivery","delivered"],"type":"string"}},{"name":"status","required":false,"in":"query","description":"Status filter (repeatable, see list in description)","schema":{}}],"responses":{"200":{"description":"Cursor-paginated load list with status facets","content":{"application/json":{"schema":{"example":{"loads":[{"id":"cmrr6gyru000ld16gducdz4op","loadNumber":"LD-R6GYRA-GIKR","mode":"ftl","status":"enroute","dispatchStage":"in_transit","originCity":"Cleveland","originState":"OH","destinationCity":"Savannah","destinationState":"GA","pickupDate":"2026-08-01T08:00:00.000Z","deliveryDate":"2026-08-03T14:00:00.000Z","equipmentType":"van","weight":24000,"customerRate":1850,"carrierRate":1500,"margin":350,"marginPct":18.92,"rateCurrency":"USD","shipperName":"Prairie Harvest Foods LLC","carrierId":null,"partyId":"cmrpgygm900yxqyrjy42el8a3"}],"total":-1,"hasMore":true,"nextCursor":"cmrr6gyru000ld16gducdz4op","limit":50,"facets":{"byStatus":{"enroute":3,"delivered":8,"draft":1}}}}}}},"400":{"description":"Unknown query param, out-of-range `limit`, or malformed `cursor`"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"List loads","tags":["Customer API v1"]},"post":{"description":"Create a load from your system — same validation as the web load form. Link the customer with `partyId` (from GET /v1/customers). Defaults to `draft` status with `margin`/`marginPct` computed automatically; pass `status` to import a load that is already underway (this sets the status directly and does not run the transition graph that `PATCH /loads/{id}/status` enforces). Pass `carrierId` to attach a carrier directly. The carrier must be bookable for your company — an approved, unexpired verification, a carrier your company accepted without verification (a company setting, off by default), or your own fleet — or the load is NOT created and the answer is a 400 with a stable `code`: `carrier_not_verified`, `verification_expired`, `carrier_blacklisted` or `insurance_below_minimum`, plus `message` (words) and `carrierId`. A carrier booked on your company's word comes back with `carrierWarning` (red words to show your users). All fields optional except that unknown fields are rejected (strict schema). Requires a key with the \"write\" scope and the TMS loads module. Send an `Idempotency-Key` header (e.g. a UUID) to make retries safe: a retry with the same key and the same body returns the original response (`Idempotent-Replayed: true`) instead of creating a second load; the same key with a different body is a 422. Keys are remembered per API key for 24 hours.","operationId":"ApiV1Controller_createLoad","parameters":[{"name":"Idempotency-Key","in":"header","description":"Up to 255 visible ASCII characters (a UUID is ideal). Same key + same body within 24 h → the original response is replayed; same key + different body → 422; same key while the first request is still running → 409.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"example":{"partyId":"cmrpgygm900yxqyrjy42el8a3","originCity":"Cleveland","originState":"OH","destinationCity":"Savannah","destinationState":"GA","pickupDate":"2026-08-01","deliveryDate":"2026-08-03","equipmentType":"van","weight":24000,"commodity":"Packaged foods","customerRate":1850,"carrierRate":1500,"referenceNumber":"PO-88231"}}}}},"responses":{"201":{"description":"The created load","content":{"application/json":{"schema":{"example":{"load":{"id":"cmrr6gyru000ld16gducdz4op","loadNumber":"LD-R6GYRA-GIKR","mode":"ftl","status":"draft","dispatchStage":"pending","originCity":"Cleveland","originState":"OH","destinationCity":"Savannah","destinationState":"GA","pickupDate":"2026-08-01T08:00:00.000Z","deliveryDate":"2026-08-03T14:00:00.000Z","equipmentType":"van","weight":24000,"customerRate":1850,"carrierRate":1500,"margin":350,"marginPct":18.92,"rateCurrency":"USD","shipperName":"Prairie Harvest Foods LLC","carrierId":null,"partyId":"cmrpgygm900yxqyrjy42el8a3"},"created":true}}}}},"400":{"description":"Validation failed. The body carries `details` — a flat `[{ path, message }]` list naming each offending field — alongside the raw Zod `issues`.","content":{"application/json":{"schema":{"example":{"statusCode":400,"message":"Validation failed — weight: Expected number, received string","path":"/api/v1/loads","details":[{"path":"weight","message":"Expected number, received string"}]}}}}},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}},"403":{"description":"Key lacks the \"write\" scope","content":{"application/json":{"schema":{"example":{"statusCode":403,"code":"MISSING_API_SCOPE","message":"This API key does not have the \"write\" scope. Create a key with write access under Settings → API Keys.","path":"/api/v1/loads"}}}}},"409":{"description":"A request with this Idempotency-Key is still being processed","content":{"application/json":{"schema":{"example":{"statusCode":409,"code":"IDEMPOTENCY_KEY_IN_PROGRESS","message":"A request with this Idempotency-Key is still being processed. Retry in a few seconds.","path":"/api/v1/loads"}}}}},"422":{"description":"The Idempotency-Key was already used for a different request body","content":{"application/json":{"schema":{"example":{"statusCode":422,"code":"IDEMPOTENCY_KEY_REUSED","message":"This Idempotency-Key was already used for a different request. Use a new key for a new request.","path":"/api/v1/loads"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Create a load","tags":["Customer API v1"]}},"/api/v1/loads/{id}":{"get":{"description":"The full load record: stops, charges, references, linked customer and carrier summary, status history, claims, tracking state and margin (customerRate − carrierRate).","operationId":"ApiV1Controller_getLoad","parameters":[{"name":"id","required":true,"in":"path","description":"Load id","schema":{"type":"string"}}],"responses":{"200":{"description":"Load detail (superset of the list row)","content":{"application/json":{"schema":{"example":{"load":{"id":"cmrr6gyru000ld16gducdz4op","loadNumber":"LD-R6GYRA-GIKR","mode":"ftl","status":"enroute","dispatchStage":"in_transit","originCity":"Cleveland","originState":"OH","destinationCity":"Savannah","destinationState":"GA","pickupDate":"2026-08-01T08:00:00.000Z","deliveryDate":"2026-08-03T14:00:00.000Z","equipmentType":"van","weight":24000,"customerRate":1850,"carrierRate":1500,"margin":350,"marginPct":18.92,"rateCurrency":"USD","shipperName":"Prairie Harvest Foods LLC","carrierId":null,"partyId":"cmrpgygm900yxqyrjy42el8a3"}}}}}},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Get one load","tags":["Customer API v1"]}},"/api/v1/loads/{id}/status":{"patch":{"description":"Move a load through its lifecycle. Transitions are validated against the same status graph as the web board — an illegal jump returns 400 listing the allowed next statuses, so integrations can discover legal moves at runtime. The derived `dispatchStage` is updated automatically. Requires a key with the \"write\" scope.","operationId":"ApiV1Controller_changeLoadStatus","parameters":[{"name":"id","required":true,"in":"path","description":"Load id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"example":{"toStatus":"available","note":"Released to dispatch"}}}}},"responses":{"200":{"description":"The updated load","content":{"application/json":{"schema":{"example":{"load":{"id":"cmrr6gyru000ld16gducdz4op","loadNumber":"LD-R6GYRA-GIKR","mode":"ftl","status":"available","dispatchStage":"available","originCity":"Cleveland","originState":"OH","destinationCity":"Savannah","destinationState":"GA","pickupDate":"2026-08-01T08:00:00.000Z","deliveryDate":"2026-08-03T14:00:00.000Z","equipmentType":"van","weight":24000,"customerRate":1850,"carrierRate":1500,"margin":350,"marginPct":18.92,"rateCurrency":"USD","shipperName":"Prairie Harvest Foods LLC","carrierId":null,"partyId":"cmrpgygm900yxqyrjy42el8a3"}}}}}},"400":{"description":"Illegal transition — response lists the allowed next statuses"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}},"403":{"description":"Key lacks the \"write\" scope (403 MISSING_API_SCOPE)"}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Change load status","tags":["Customer API v1"]}},"/api/v1/loads/{id}/check-calls":{"get":{"description":"The load's check-call timeline: dispatcher notes, reported locations, ETAs, and per-stop timeline events, newest first.","operationId":"ApiV1Controller_listCheckCalls","parameters":[{"name":"id","required":true,"in":"path","description":"Load id","schema":{"type":"string"}}],"responses":{"200":{"description":"Check calls, newest first","content":{"application/json":{"schema":{"example":{"checkCalls":[{"id":"cmrr6hnip0013d16gpz2ovajs","loadId":"cmrr6gyru000ld16gducdz4op","loadStopId":null,"eventType":null,"reportedAt":"2026-07-19T02:30:21.264Z","lat":null,"lng":null,"city":"Columbus","state":"OH","etaAt":null,"note":"On I-71 S, on schedule","source":"dispatcher"}]}}}}},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"List check calls for a load","tags":["Customer API v1"]},"post":{"description":"Record a check call on a load from your system — a location update, note, or ETA. `source` attributes where the update came from: dispatcher, driver_app, eld, or carrier. All fields are optional, but unknown fields are rejected with a 400 (a body of pure typos used to return 201 and create a blank row).","operationId":"ApiV1Controller_createCheckCall","parameters":[{"name":"id","required":true,"in":"path","description":"Load id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"example":{"city":"Columbus","state":"OH","note":"On I-71 S, on schedule","etaAt":"2026-08-03T14:00:00Z","source":"eld"}}}}},"responses":{"201":{"description":"The created check call","content":{"application/json":{"schema":{"example":{"checkCall":{"id":"cmrr6hnip0013d16gpz2ovajs","loadId":"cmrr6gyru000ld16gducdz4op","loadStopId":null,"eventType":null,"reportedAt":"2026-07-19T02:30:21.264Z","lat":null,"lng":null,"city":"Columbus","state":"OH","etaAt":null,"note":"On I-71 S, on schedule","source":"dispatcher"}}}}}},"400":{"description":"Validation failed — the body carries `details` naming each offending field"},"401":{"description":"Missing, invalid, or revoked API key","content":{"application/json":{"schema":{"example":{"statusCode":401,"message":"Invalid API key","path":"/api/v1/ping"}}}}},"403":{"description":"Key lacks the \"write\" scope (403 MISSING_API_SCOPE)"}},"security":[{"x-api-key":[]},{"api-key":[]}],"summary":"Add a check call","tags":["Customer API v1"]}}},"info":{"title":"Happy Tactic Customer API","description":"REST API for integrating your systems with Happy Tactic — carrier verifications, carrier data, and TMS loads.\n\n## Authentication\nCreate an API key in the app under **Settings → API Keys**. The full key (`hkpt_...`) is shown once at creation — store it securely.\nSend it on every request in one of two headers:\n```\nAuthorization: Bearer hkpt_your_key_here\nX-Api-Key: hkpt_your_key_here\n```\nThe key acts as the user who created it: it sees the same workspace data, and role/plan restrictions apply (e.g. the loads endpoints require the TMS loads module on your plan).\n\n## Key scopes\nKeys are **read-only** by default. Endpoints that change data (create a load, change load status, add check calls) require a key created with **write access** (Settings → API Keys → \"Allow write access\"; only the account owner or an admin can create one). A read-only key calling a write endpoint gets `403 MISSING_API_SCOPE`.\nKeys expire (chosen at creation; default 1 year) — an expired key gets `401 API key has expired`. A removed or suspended member's keys are revoked.\n\n## Safe retries\n`POST /v1/loads` accepts an `Idempotency-Key` header (a UUID per load). A retry with the same key and body returns the original response (`Idempotent-Replayed: true`) instead of creating a second load; the same key with a different body is `422 IDEMPOTENCY_KEY_REUSED`; while the first request is still running, `409 IDEMPOTENCY_KEY_IN_PROGRESS`. Keys are kept per API key for 24 hours.\n\n## Webhooks\nVerification-decision webhooks carry `X-Happytactic-Timestamp` and `X-Happytactic-Signature-V2: t=<unix seconds>,v2=<hex HMAC-SHA256(secret, \"<t>.<raw body>\")>` — verify it and reject a `t` more than 300 s from now. The v1 `X-Happytactic-Signature` (HMAC of the body alone, no timestamp) is still sent for existing integrations.\n\n## Quick start\n```bash\ncurl -H \"Authorization: Bearer hkpt_...\" https://api.happytactic.com/api/v1/ping\n```\n\n## Errors & limits\nErrors return JSON: `{ \"statusCode\": 401, \"message\": \"Invalid API key\", \"path\": \"/api/v1/ping\" }`.\nValidation errors (400) come from the same schemas the web app uses and add a `details` array — a flat `[{ \"path\": \"weight\", \"message\": \"Expected number, received string\" }]` list ready to display — alongside the raw Zod `issues`. `path` is dotted for nested fields (`\"stops.0.city\"`).\nUnknown query parameters and unknown body fields are rejected with a 400 rather than ignored, so a misspelled filter fails loudly instead of silently returning unfiltered data.\nRequests are rate-limited per user; writes have tighter limits than reads. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until the window resets) for the most-constraining limit that applied, plus per-tier copies suffixed with the tier name. A 429 additionally carries the standard `Retry-After` in seconds — honour it.","version":"1.3.0","contact":{}},"tags":[],"servers":[{"url":"https://api.happytactic.com","description":"Production"}],"components":{"securitySchemes":{"api-key":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"API key (hkpt_...) as a Bearer token"},"x-api-key":{"type":"apiKey","in":"header","name":"X-Api-Key"}},"schemas":{}}}