Customer API Reference
The Customer API lets you integrate Happy Tactic with your own infrastructure — sync verifications, carriers, customers, and loads into your systems, or push loads and status updates from your TMS, ERP, or dispatch tooling. Every endpoint is documented below with its parameters, a runnable example in three languages, and a real (trimmed) response.
Introduction
| Base URL | https://api.happytactic.com/api/v1 |
| Format | JSON over HTTPS. Request bodies use Content-Type: application/json. |
| Authentication | API key in a header — see Authentication |
| Current version | v1.4.1 — see the changelog at the bottom of this page |
| Interactive console | api.happytactic.com/docs |
| OpenAPI spec | /docs/openapi.json |
All data is workspace-scoped: a key only ever sees the workspace it was created in. IDs from one workspace do not resolve in another (you get a 404, not a 403 — the API never confirms that another workspace’s record exists).
Authentication
Create a key in the app under Settings → API Keys. Give it a name that says where it will live (erp-sync, powerbi) — the full key (hkpt_…) is shown once at creation. Store it in a secrets manager and send it on every request in either header:
Authorization: Bearer hkpt_your_key_here
X-Api-Key: hkpt_your_key_hereThe key acts as the user who created it: it sees that user’s workspace data, and the same role and plan restrictions apply. If your plan doesn’t include a TMS module (loads, CRM), the corresponding endpoints return 403 for keys in that workspace. Revoking a key (same settings page) takes effect immediately; each key also shows its Last used time, its expiry and who created it, so you can audit dormant credentials.
Keys expire. Pick the lifetime when you create the key — 30 days, 90 days, 1 year (default), 2 years or no expiry. An expired key gets 401 API key has expired; rotate before the date shown. When a member is removed from the team or suspended, their keys are revoked — create integration keys from a long-lived account.
Verify a key with a ping:
curl "https://api.happytactic.com/api/v1/ping" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"Key scopes
Keys are read-only by default. Endpoints that change data — creating a load, changing a load’s status, adding a check call — require a key created with “Allow write access” checked. Each endpoint below is badged any key or write key. Only the account owner or an admin can create a write key; any member can create a read-only key. A read-only key calling a write endpoint receives:
{
"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"
}Starting verifications needs one more permission, “Create and start verifications” (scope verifications:create, company admins only, includes write) — each verification uses a credit.
Errors
Every error is JSON with a consistent envelope. Some errors add a machine-readable code:
{ "statusCode": 401, "message": "Invalid API key", "path": "/api/v1/ping" }| Status | When | How to handle |
|---|---|---|
| 400 | Validation failed, an unknown parameter, or an illegal load-status transition | The body carries a details array — [{ path: "weight", message: "Expected number, received string" }] — naming every offending field, with a dotted path for nested fields (stops.0.city). Raw Zod issues are included too. Unknown query params and unknown body fields are rejected rather than ignored, so a misspelled filter fails loudly instead of returning unfiltered data. Status-change 400s list the allowed next statuses — use that to discover legal moves at runtime. |
| 401 | Missing, malformed, invalid, revoked or expired key | Distinct messages for each case (Missing API key…, Invalid API key, API key has been revoked, API key has expired). Check the header format and the key’s state in Settings → API Keys. |
| 403 | Key is valid but not allowed | Check code: MISSING_API_SCOPE (needs a write key), suspended account, unverified email, or your plan lacks the required TMS module. |
| 404 | No such record in your workspace | IDs are workspace-scoped; a typo’d or foreign ID looks identical to a missing one. |
| 409 | IDEMPOTENCY_KEY_IN_PROGRESS | The first request with this Idempotency-Key is still running — retry in a few seconds. |
| 422 | IDEMPOTENCY_KEY_REUSED | That Idempotency-Key was already used for a different body — use a new key for a new request. |
| 429 | Rate limited | Honor Retry-After and back off. See Rate limits below. |
| 5xx | Server error | Retry with exponential backoff; contact support if persistent. |
Rate limits
Limits are per user (the key’s creator), so multiple keys created by the same user share a budget, and they are counted separately for each endpoint:
| Operation class | Limit |
|---|---|
| Reads (each GET endpoint) | 120 requests / minute |
| Writes (each POST/PATCH endpoint) | 60 requests / minute |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; a 429 adds Retry-After (seconds). Poll on a schedule (e.g. once a minute with updatedAfter-style filters) rather than tight loops.
Pagination
Three pagination styles are in use — each list endpoint’s block states which:
| Style | Endpoints | How it works |
|---|---|---|
| Cursor | verifications | Pass limit (1–100, default 50); while hasMore is true, pass the response’s nextCursor back as cursor. There is no total and no page — sending page returns 400. |
| Offset + limit | carriers, customers | Pass offset and limit (1–200, default 50). Response includes total. |
| Cursor (or offset) | loads | Pass limit; while hasMore is true, pass the response’s nextCursor back as cursor. offset also works for warehouse-style paging; cursor wins if both are sent. total is always −1 (not computed, for performance). |
A limit outside the documented range returns 400 rather than being silently reduced, and a malformed cursor returns 400 rather than an empty page — so an empty page always means you have reached the end of the data.
Endpoints
Validate your key
/v1/pingany keyReturns the workspace and key identity the credential resolves to, including the key’s scopes. Use it to test connectivity and confirm which workspace a key belongs to before wiring it into an integration.
curl "https://api.happytactic.com/api/v1/ping" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"ok": true,
"tenantId": "cmrpgygdl00xcqyrjt7871dbr",
"user": { "email": "dispatch@acmelogistics.com", "role": "BROKER" },
"key": { "name": "erp-sync", "prefix": "hkpt_AbC12de", "scopes": ["read", "write"] }
}List verifications
/v1/verificationsany keyCarrier verifications in your workspace, newest first. In the app these appear under Verifications with the same status pipeline.
| Parameter | Type | Description |
|---|---|---|
status | enum | Pipeline filter — stable keys to send, not words to show: DRAFT (Draft — never sent), PENDING (In progress, or Needs your review once finished), APPROVED (OK to book — Expired once validUntil has passed), REJECTED (Do not book) |
q | string | Free-text search across carrier name, MC/DOT, and dispatcher name |
minRisk | number | Only verifications whose Trust score (riskScore) is ≥ this value (0–100, higher = safer; 70 or higher is Low risk, 30 or below is High risk) |
sort | enum | updatedAt:asc | updatedAt:desc (default) | riskScore:asc | riskScore:desc |
limit | number | Page size, 1–100 (default 50). Out of range returns 400. |
cursor | string | The previous response’s nextCursor. Omit for the first page. This endpoint has no page parameter. |
curl "https://api.happytactic.com/api/v1/verifications?status=APPROVED&sort=riskScore:desc&limit=25" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"verifications": [
{
"id": "cmrpgyhaj01ksqyrjzmza5xzx",
"carrierId": "cmrpgygko00yjqyrjen141lcf",
"dispatcherName": "Angela Doss",
"status": "completed",
"decision": "green",
"riskScore": 86,
"ownerCompleted": true,
"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": 25
}Key response fields: on a finished verification (status completed or failed), decision is the verdict the app shows — green = OK to book, yellow or null = Needs your review, red (or failed) = Do not book. While it is still running the decision is provisional (the app shows In progress — no decision yet). riskScore is the Trust score (0–100, higher is safer), shown in the app with its band: 86 reads “86 · Low risk”.
Get one verification
/v1/verifications/{id}any keyThe full record behind the app’s verification detail page: carrier snapshot at verification time, the 7-step workflow state (owner call, owner OTP, insurance, dispatcher, info type, driver, decision), insurance checks, Trust score (riskScore, higher is safer) with contributing signals, and the final decision.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Verification id — from the list endpoint or the app URL |
curl "https://api.happytactic.com/api/v1/verifications/cmrpgyhaj01ksqyrjzmza5xzx" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"verification": {
"id": "cmrpgyhaj01ksqyrjzmza5xzx",
"carrierId": "cmrpgygko00yjqyrjen141lcf",
"dispatcherName": "Angela Doss",
"operatingModel": "dispatcher",
"status": "initiated",
"decision": null,
"riskScore": null,
"completionTimestamp": null,
"validUntil": null,
"createdAt": "2026-10-28T09:00:00.000Z",
"updatedAt": "2026-10-28T20:14:56.329Z",
"carrier": {
"id": "cmrpgygko00yjqyrjen141lcf",
"mcNumber": "1499417",
"dotNumber": "3931949",
"legalName": "Cumberland Freight Systems Inc",
"status": "Y"
},
"ownerVerification": null,
"insuranceVerification": null,
"documents": [],
"carrierFacts": {
"authority": { "status": "active", "label": "Active" },
"insurance": { "status": "unknown", "label": "Not checked" },
"verification": { "id": "cmrpgyhaj01ksqyrjzmza5xzx", "status": "initiated", "decision": null, "score": null }
},
"verificationValidity": { "validUntil": null, "verificationExpired": false, "daysUntilVerificationExpiry": null }
}
}validUntil is set once the verification is decided; a decided verification stops counting as OK to book after it. Fields you don’t recognise are additive — ignore them rather than failing on them.
Create and start a verification
/v1/verificationswrite keyStarts a carrier verification exactly as the app’s New verification does — the same credit check (402 when your company is out of credits), daily limit, carrier lookup, checks and emails to the carrier’s owner. Needs a key with “Create and start verifications” ticked (company admins only; it includes write access) — any other key gets 403 MISSING_API_SCOPE. Each one uses a verification credit. 10 per minute per user.
| Parameter | Type | Description |
|---|---|---|
mcNumber | string | MC number — send this OR dotNumber |
dotNumber | string | USDOT number — send this OR mcNumber |
operatingModel | enum | dispatcher (default) | owner_operator |
dispatcherName | string | Required for a dispatcher-model carrier — the owner is asked to confirm it |
dispatcherPhone | string | Required for a dispatcher-model carrier — a 10-digit US number |
dispatcherEmail | string | Optional |
shipperName / shipperEmail | string | Optional — the shipper gets the decision by email |
confirmDuplicate | boolean | The carrier was already verified today (409 VERIFICATION_DUPLICATE_TODAY) — start another one anyway |
start | boolean | false = save a draft only (no checks, no credit). Default true |
curl -X POST "https://api.happytactic.com/api/v1/verifications" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mcNumber":"1499417","dispatcherName":"Angela Doss","dispatcherPhone":"(614) 555-0142"}'{
"verification": {
"id": "cmw013hhq07xel06qv5y3l937",
"status": "initiated",
"createdAt": "2026-11-03T01:48:07.118Z"
}
}status is initiated, or draft when you sent start: false. Read the whole record with GET /v1/verifications/{id}. Errors: 400 when neither or both of mcNumber / dotNumber are sent (or the dispatcher details are missing), 402 out of credits, 403 MISSING_API_SCOPE, 409 VERIFICATION_DUPLICATE_TODAY.
Idempotency-Key header to make retries safe — same rules as POST /v1/loads. Then poll GET /v1/verifications/{id} or subscribe to the decision webhook to sync the result.List carriers
/v1/carriersany keyYour workspace’s carrier roster — the same data as the app’s Carriers page, derived from your verifications. Underscore-prefixed fields are workspace-level aggregates rather than carrier master data.
| Parameter | Type | Description |
|---|---|---|
q | string | Free-text search (name, MC, DOT) |
verifiedOnly | boolean-ish | "true" to only return carriers that are OK to book (the same as decision=green) |
decision | string | Filter by the verdict of each carrier's newest verification: green = OK to book (finished, not expired), yellow = Needs your review, red = Do not book. A running or expired verification matches none. |
status | string | Carrier operating-status filter |
equipment | string | Equipment type (van, reefer, flatbed, …) |
limit | number | Page size, 1–200 (default 50). Out of range returns 400. |
offset | number | Pagination offset (default 0) |
curl "https://api.happytactic.com/api/v1/carriers?verifiedOnly=true&limit=25" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"items": [
{
"id": "cmrpgygko00yjqyrjen141lcf",
"mcNumber": "1499417",
"dotNumber": "3931949",
"legalName": "Cumberland Freight Systems Inc",
"status": "active",
"insuranceOnFile": true,
"_verifiedAt": "2026-07-17T09:00:00.000Z",
"_decision": "green",
"_verificationStatus": "completed",
"_validUntil": "2027-07-17T09:00:00.000Z",
"_trustScore": 86,
"_loadsCount": 4,
"_onTimeRate": null,
"_grade": null,
"_rating": null,
"_blacklisted": false,
"_equipment": ["van"],
"_ownFleet": false,
"_notVerified": false
}
],
"total": 6,
"limit": 25,
"offset": 0
}Get one carrier
/v1/carriers/{id}any keyCarrier detail aggregate: authority and operating status, insurance on file, contacts (pre-filtered — insurance-agent and fax noise removed), verification history, and the monitored change log (phone, address, and authority changes over time — the fraud signals the platform watches).
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Carrier id — from the list endpoint or a verification’s carrierId |
curl "https://api.happytactic.com/api/v1/carriers/cmrpgygko00yjqyrjen141lcf" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"List customers
/v1/customerscrm moduleany keyYour customers — shippers, prospects and vendors (the app’s Customers page). The returned id is what you pass as partyId when creating loads. Archived customers are excluded by default. (The API calls a customer a party: partyId, parties.)
| Parameter | Type | Description |
|---|---|---|
q | string | Free-text search on name / legal name |
type | string[] | Party type: customer, prospect, vendor, shipper, consignee, … (repeatable) |
status | string[] | active, inactive, blocked (repeatable) |
creditTier | string[] | unrated, high_risk, standard, good, excellent, cash_only (repeatable) |
industry | string | Industry filter |
tag | string[] | Tag filter (repeatable) |
updatedAfter | ISO date | Only customers updated after this date — use for incremental sync |
includeArchived | boolean-ish | "true" to include archived customers |
limit | number | Page size, 1–200 (default 50) |
offset | number | Pagination offset (default 0) |
curl "https://api.happytactic.com/api/v1/customers?type=customer&updatedAfter=2026-07-01" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"parties": [
{
"id": "cmrpgygm900yxqyrjy42el8a3",
"name": "Bluegrass Beverage Distributors",
"type": "customer",
"status": "active",
"creditTier": "unrated",
"legalName": "Bluegrass Beverage Distributors",
"taxExempt": false,
"tags": []
}
],
"total": 12,
"limit": 50,
"offset": 0,
"facets": {
"byType": { "customer": 10, "prospect": 2 },
"byStatus": { "active": 12 },
"byCreditTier": { "unrated": 9, "good": 3 }
}
}Get one customer
/v1/customers/{id}crm moduleany keyCustomer detail: contacts, addresses, and the credit profile the app shows on the customer’s Credit tab — credit limit, tier, open A/R exposure, PayNet score, average days-to-pay, and on-time pay rate.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Customer (party) id |
curl "https://api.happytactic.com/api/v1/customers/cmrpgygm900yxqyrjy42el8a3" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"List loads
/v1/loadsloads moduleany keyTMS loads for your workspace, with the same filters as the app’s load list. Cursor-paginated: keep passing nextCursor back as cursor while hasMore is true.
| Parameter | Type | Description |
|---|---|---|
status | string[] | Load status (repeatable) — see the status reference |
dispatchStage | string[] | Operational pipeline stage (repeatable) — a narrower vocabulary derived from status: pending, available, committed, dispatched, at_pickup, in_transit, at_delivery, delivered |
q | string | Free-text search (load #, customer, city, …) |
loadNumber | string | Load number |
partyId | string | Filter to one customer |
carrierId | string | Filter to one carrier |
mcNumber | string | Carrier MC number |
equipmentType | string | van, reefer, flatbed, … |
mode | enum | ftl, ltl, partial or intermodal |
dotNumber | string | Carrier USDOT number |
carrierName | string | Part of the carrier’s legal or DBA name |
commodity / loadType | string | Commodity text; load type (spot, contract, …) |
originCity / originZip / destinationCity / destinationZip | string | Lane filters |
customerId | string[] | One or more customer ids (the same customers as partyId; repeatable) |
salesRepId / teamId / dispatcherId | string[] | Booked by this rep / team / dispatcher (repeatable) |
trackingState | string[] | Tracking state now, e.g. not_started or not_tracked (repeatable) |
needsAttention | boolean-ish | "true" = only loads with an open Needs attention flag |
mine | boolean-ish | "true" = only loads you booked |
bolNumber | string | BOL number |
reference | string | Any reference number (PO, sales order, container, …) |
originState | string | Origin state (2-letter) |
destinationState | string | Destination state (2-letter) |
pickupFrom / pickupTo | ISO date | Pickup date window |
sortBy | enum | createdAt | updatedAt | pickupDate | deliveryDate | loadNumber |
sortDir | enum | asc | desc |
limit | number | Page size, 1–200 (default 50). Out of range returns 400. |
cursor | string | nextCursor from the previous response. Malformed values return 400. |
offset | number | Row offset for non-cursor paging (default 0). Ignored when cursor is supplied. |
curl "https://api.happytactic.com/api/v1/loads?status=enroute&limit=25" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"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",
"partyId": "cmrpgygm900yxqyrjy42el8a3",
"carrierId": null
}
],
"total": -1,
"hasMore": true,
"nextCursor": "cmrr6gyru000ld16gducdz4op",
"limit": 25,
"facets": { "byStatus": { "enroute": 3, "delivered": 8, "draft": 1 } }
}Get one load
/v1/loads/{id}loads moduleany keyEverything the app’s load workspace shows, as data: stops, accessorial charges, reference numbers, the linked customer and carrier (name, MC and credit tier), status history, claims, tracking state, and the margin (customer rate − carrier rate).
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Load id |
curl "https://api.happytactic.com/api/v1/loads/cmrr6gyru000ld16gducdz4op" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"{
"load": {
"id": "cmrr6gyru000ld16gducdz4op",
"loadNumber": "LD-R6GYRA-GIKR",
"status": "enroute",
"dispatchStage": "in_transit",
"stops": [],
"charges": [],
"references": []
}
}Create a load
/v1/loadsloads modulewrite keyCreate a load from your system — the API equivalent of the app’s New Load wizard. The load starts in draft status; margin and marginPct are computed automatically. Link the customer with partyId (from GET /customers) so credit exposure and billing rules apply. The schema is strict: unknown fields are rejected with a 400 naming the field.
| Parameter | Type | Description |
|---|---|---|
partyId | string | CRM customer to book the load for (recommended — enables credit checks and invoicing rules) |
loadNumber | string | Your load number (auto-generated if omitted) |
carrierId | string | Attach a carrier directly. The carrier must be bookable for your company — an approved, unexpired verification, a carrier your company accepted without verification, or your own fleet — otherwise the load is not created and you get a 400 with a stable code (carrier_not_verified, verification_expired, carrier_blacklisted, insurance_below_minimum) and a message in words. A carrier booked on your company's word returns carrierWarning. |
status | enum | Defaults to draft. Pass a status to import a load that is already underway — this sets it directly and does not run the transition graph that PATCH /loads/{id}/status enforces. |
referenceNumber | string | Primary reference (PO, sales order, …) |
bolNumber | string | BOL number |
mode | enum | ftl (default), ltl, partial or intermodal |
shipperName / shipperEmail / shipperPhone | string | Free-text shipper, for loads without a CRM customer (prefer partyId) |
pallets / lengthIn / widthIn / heightIn / freightClass / nmfcCode | mixed | Freight detail |
loadType / distance | string / number | spot or contract; miles |
quickPayDiscount / carrierAdvance | number | Accounting amounts, USD |
trackingMode | enum | app_first | app_required | link_only — omit to use your company default |
trackingRequired | boolean | Print the “tracking required” clause on the rate confirmation; omit for your company default |
salesRepId / teamId / salesSplit / customFields | mixed | Sales rep credited, team, commission split and your custom fields (by name); omit to use your company defaults |
originCity / originState / originZip / originAddress | string | Origin |
destinationCity / destinationState / destinationZip / destinationAddress | string | Destination |
pickupDate / deliveryDate | ISO date | Scheduled dates |
equipmentType | string | van, reefer, flatbed, … |
weight | number | Weight in lbs |
commodity | string | What is being hauled |
pieces / pallets | number | Piece / pallet counts |
hazmat | boolean | Hazmat flag (+ hazmatClass) |
tempMinF / tempMaxF | number | Temperature range for reefer loads |
customerRate | number | What the customer pays |
carrierRate | number | Your target / agreed buy |
rateCurrency | string | 3-letter ISO code (default USD) |
notes | string | Load notes |
curl -X POST "https://api.happytactic.com/api/v1/loads" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"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"}'{
"load": {
"id": "cmrr6gyru000ld16gducdz4op",
"loadNumber": "LD-R6GYRA-GIKR",
"status": "draft",
"dispatchStage": "pending",
"customerRate": 1850,
"carrierRate": 1500,
"margin": 350,
"marginPct": 18.92
},
"created": true
}CREDIT_LIMIT_EXCEEDED, the same gate the app enforces.Safe retries — Idempotency-Key
A timeout or a 5xx doesn’t tell you whether the load was created. Send an Idempotency-Key header (a fresh UUID per load you mean to create) and retry with the same key — you get the original response back, never a second load.
Idempotency-Key: 6f1c2a9e-3b7d-4c55-9e0a-1d2f3a4b5c6d| Situation | Response |
|---|---|
| First request with the key | Runs normally (201) |
| Same key + same body within 24 h | The original response, replayed, with Idempotent-Replayed: true — no second load |
| Same key + a different body | 422 IDEMPOTENCY_KEY_REUSED — use a new key |
| Same key while the first is still running | 409 IDEMPOTENCY_KEY_IN_PROGRESS — retry shortly |
| The first request failed (e.g. 400) | Nothing is stored — fix it and retry with the same key |
Keys are 1–255 visible ASCII characters, scoped to your API key, and remembered for 24 hours. JSON key order doesn’t matter. Without the header nothing changes.
Change load status
/v1/loads/{id}/statusloads modulewrite keyMove a load through its lifecycle. Transitions are validated against the same status graph as the app’s dispatch board — an illegal jump returns 400 listing the allowed next statuses, so your integration can discover legal moves at runtime instead of hard-coding the graph. The derived dispatchStage updates automatically, and the change lands in the load’s activity log attributed to your key’s user.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Load id (URL parameter) |
toStatusrequired | enum | Target status — see the status reference |
note | string | Optional note recorded with the transition (max 500 chars) |
curl -X PATCH "https://api.happytactic.com/api/v1/loads/cmrr6gyru000ld16gducdz4op/status" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"toStatus":"enroute","note":"Picked up, driver rolling"}'{ "load": { "id": "cmrr6gyru000ld16gducdz4op", "status": "enroute", "dispatchStage": "in_transit" } }List check calls
/v1/loads/{id}/check-callsloads moduleany keyThe load’s check-call timeline — dispatcher notes, reported locations, ETAs, and per-stop timeline events, newest first. This is the Check Calls section of the app’s load workspace.
curl "https://api.happytactic.com/api/v1/loads/cmrr6gyru000ld16gducdz4op/check-calls" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY"Add a check call
/v1/loads/{id}/check-callsloads modulewrite keyRecord a location update, note, or ETA on a load from your system — for example, forwarding positions from your own tracking stack.
| Parameter | Type | Description |
|---|---|---|
idrequired | string | Load id (URL parameter) |
city / state | string | Reported location |
lat / lng | number | Coordinates (alternative to city/state) |
etaAt | ISO datetime | Estimated arrival at the next stop |
note | string | Free-text note (max 500 chars) |
source | enum | dispatcher | driver_app | eld | carrier — where the update came from |
loadStopId | string | Attach to a specific stop’s timeline |
eventType | enum | Per-stop timeline event type (arrived, loaded, departed, …) |
reportedAt | ISO datetime | When the observation happened (default: now) |
curl -X POST "https://api.happytactic.com/api/v1/loads/cmrr6gyru000ld16gducdz4op/check-calls" \
-H "Authorization: Bearer $HAPPYTACTIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"city":"Columbus","state":"OH","note":"On I-71 S, on schedule","etaAt":"2026-08-03T14:00:00Z","source":"eld"}'{
"checkCall": {
"id": "cmrr6hnip0013d16gpz2ovajs",
"loadId": "cmrr6gyru000ld16gducdz4op",
"reportedAt": "2026-07-19T02:30:21.264Z",
"city": "Columbus",
"state": "OH",
"etaAt": "2026-08-03T14:00:00.000Z",
"note": "On I-71 S, on schedule",
"source": "eld"
}
}Load status reference
The 24 load statuses, in lifecycle order, with the labels the app displays. Use the machine value (left column) in API calls; illegal transitions return 400 with the allowed next statuses.
| Status | App label | Meaning |
|---|---|---|
draft | Draft | Being built; not yet released |
available | Available | Released, needs a carrier |
posted | Posted | Posted to a load board |
committed | Covered | A carrier is on it (offer accepted or booked direct) |
assigned | Assigned | Driver/truck assigned |
dispatched | Dispatched | Dispatch sheet out |
at_pickup | At pickup | Truck at origin |
picked_up | Picked up | Loaded |
enroute | In Transit | Rolling |
at_delivery | At delivery | Truck at destination |
delivered | Delivered | POD expected |
invoice_audit | Invoice audit | Paperwork audit queue |
invoice_pending | Invoice pending | Approved, awaiting invoicing |
invoiced | Invoiced | Customer invoiced |
carrier_paid | Carrier paid | Carrier settled |
customer_paid | Customer paid | Customer payment received |
completed | Completed | Fully closed |
missed_pickup | Missed pickup | Exception |
tonu | TONU | Truck ordered, not used |
ops_hold | Ops hold | Operational hold (resumable) |
accounting_hold | Accounting hold | Accounting hold (resumable) |
claim_filed | Claim filed | OS&D / claim open |
cancelled | Cancelled | Cancelled |
voided | Voided | Voided |
Webhooks
Configure a URL and a signing secret under Settings → Company controls → Webhooks & services (company admins). When a verification’s decision or status changes (never again for the same state) we POST a JSON body with event = "verification_complete" (a final status, or the final decision changed) or "verification_review_required" (still in progress, turned yellow), the verificationId, decision, riskScore, status, completedAt, the carrier’s MC / DOT / legal name, the phone-verification and insurance summaries, and sentAt.
| Header | Value |
|---|---|
| X-Happytactic-Event | verification_complete or verification_review_required (same as event) |
| X-Happytactic-Timestamp | Unix seconds when this attempt was signed |
| X-Happytactic-Signature-V2 | t=<timestamp>,v2=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")> — use this |
| X-Happytactic-Signature | v1: hex HMAC-SHA256(secret, raw body) — kept for existing integrations; it carries no timestamp, so a captured delivery can be replayed |
Verify v2: parse t and v2 from the header; reject if |now − t| is more than 300 seconds; compute HMAC-SHA256(secret, t + "." + rawBody) over the raw request bytes and compare in constant time.
const crypto = require('crypto');
function verifyHappytactic(secret, headers, rawBody, now = Date.now()) {
const parts = Object.fromEntries(String(headers['x-happytactic-signature-v2'] || '')
.split(',').map((p) => p.split('=').map((s) => s.trim())));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Math.floor(now / 1000) - t) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return /^[a-f0-9]{64}$/.test(parts.v2 || '') &&
crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v2, 'hex'));
}Retries (1 min, 5 min, 30 min, then every 30 min — 5 attempts in all) are re-signed with their own timestamp. Answer 2xx quickly and deduplicate on verificationId + decision.
Load events
The same webhook can send your loads’ events — tick the ones you want under Settings → Company controls → Webhooks & services (a webhook saved before October 2026 sends only the verification events until you do). Signed and retried exactly as above; deduplicate on eventId (the same on every retry). Each event is sent when Happy Tactic learns of it; switching load events on never replays older history. A stop time can be earlier than the send (a driver’s phone that was offline uploads its visit when it reconnects) — read data.at, not the delivery time.
| event | When | data |
|---|---|---|
| load.status_changed | Every status move | { status, previousStatus, at, changedBy } |
| stop.arrived | A stop's arrival was recorded (once per stop) | { stop, at, recordedBy: auto | driver | dispatcher | provider } |
| stop.departed | A stop's departure was recorded (once per stop) | { stop, at, recordedBy } |
| eta.changed | A stop's first ETA, then whenever it moves 15 minutes or more | { stop, etaAt, previousEtaAt, etaSource, etaStatus, minutesLate, includesBreaks } |
| location | The truck's position — at most one every 15 minutes per load | { lat, lng, at } |
{
"event": "stop.arrived",
"eventId": "evt_3f9c…",
"version": 1,
"occurredAt": "2026-10-20T14:42:00.000Z",
"tenantId": "cm…",
"load": { "id": "cm…", "loadNumber": "1042", "status": "at_delivery", "poNumber": "PO-7781",
"customerReference": null, "bolNumber": "BOL-9",
"origin": { "city": "Dallas", "state": "TX" }, "destination": { "city": "Houston", "state": "TX" } },
"data": { "stop": { "id": "cm…", "sequence": 2, "stopType": "delivery", "facilityName": "Gulf DC",
"city": "Houston", "state": "TX" },
"at": "2026-10-20T14:42:00.000Z", "recordedBy": "auto" },
"sentAt": "2026-10-20T14:42:03.120Z"
}AI assistants
Claude, ChatGPT, Cursor and other assistants can use the same API keys through our Model Context Protocol server. Each tool is one of the endpoints above, with the same scopes, validation and rate limits. The server address and install steps are on the AI assistants page.
Best practices
- Store keys like passwords. Secrets manager or environment variables — never source control, spreadsheets, or chat. One key per integration, named for where it lives.
- Prefer read-only keys. Only the systems that genuinely create or update data need write access.
- Sync incrementally. Use
updatedAfter(customers),sort=updatedAt:desc(verifications), or status filters (loads) instead of re-pulling everything. - Handle 400s on status changes as data, not failures. The allowed-next-statuses list in the response is the source of truth for legal moves.
- Audit keys monthly. Settings → API Keys shows each key’s scope and last-used time; revoke what’s dormant.
Need an endpoint that isn’t here — document downloads, invoices, settlements, webhooks for load events? Tell us through the in-app Help & Support page; the API grows in the direction clients pull it.
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.4.1 | Nov 3, 2026 | Load answers no longer include the tracking and driver check-in link secrets (trackingTokenHash, trackingTokenEnc, driverCheckinTokenHash). Reference expanded with real response samples, the verification-create response, more load filters and fields, and a new AI assistants page. |
| 1.4.0 | Oct 27, 2026 | POST /v1/verifications is back behind its own key permission, “Create and start verifications” (scope verifications:create, company admins only, includes write). It runs the app’s own New verification path: credit check, daily limit, checks and owner emails. AI assistants (Claude, ChatGPT, Cursor) can connect with the same keys — see Settings → API Keys → AI assistants. |
| 1.3.0 | Oct 16, 2026 | Idempotency-Key on POST /v1/loads (safe retries; 409 / 422 documented). Webhook signature v2 with a timestamp (X-Happytactic-Signature-V2 + X-Happytactic-Timestamp); v1 unchanged. Load webhooks: load.status_changed, stop.arrived, stop.departed, eta.changed, location. API keys expire (default 1 year) and an expired key answers 401; write keys are created by the account owner or an admin; a removed or suspended member’s keys are revoked. |
| 1.2.1 | Aug 5, 2026 | Documentation-accuracy pass. offset now works on GET /v1/loads (it was parsed and ignored). Unknown query params and unknown body fields now return 400 instead of being silently dropped, as do malformed cursors and out-of-range limit values. GET /v1/loads/:id now returns { load: … } as documented (it returned the load unwrapped). GET /v1/verifications is cursor-paginated and never returned total/page — the docs now say so and page returns 400. 429s now carry the standard Retry-After, and all responses carry unsuffixed X-RateLimit-* headers. /v1/carriers default page size corrected to 50. |
| 1.2.0 | Jul 19, 2026 | Removed POST /v1/verifications (verifications start in the app only). Expanded reference documentation. |
| 1.1.0 | Jul 19, 2026 | Key scopes (read/write). TMS writes: create load, change load status, add check calls. Customers endpoints. Last-used tracking on keys. |
| 1.0.0 | Jul 19, 2026 | Initial release: ping, verifications, carriers, loads (read-only). |