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 URLhttps://api.happytactic.com/api/v1
FormatJSON over HTTPS. Request bodies use Content-Type: application/json.
AuthenticationAPI key in a header — see Authentication
Current versionv1.4.1 — see the changelog at the bottom of this page
Interactive consoleapi.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:

either header works — pick one and stay consistent
Authorization: Bearer hkpt_your_key_here
X-Api-Key: hkpt_your_key_here

The 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:

403 — missing scope
{
  "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"
}
Principle of least privilege: dashboards, BI syncs, and reporting never need write access. Mint one write key per integration that genuinely writes, so a leaked or retired credential can be revoked without breaking anything else.

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:

error envelope
{ "statusCode": 401, "message": "Invalid API key", "path": "/api/v1/ping" }
StatusWhenHow to handle
400Validation failed, an unknown parameter, or an illegal load-status transitionThe 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.
401Missing, malformed, invalid, revoked or expired keyDistinct 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.
403Key is valid but not allowedCheck code: MISSING_API_SCOPE (needs a write key), suspended account, unverified email, or your plan lacks the required TMS module.
404No such record in your workspaceIDs are workspace-scoped; a typo’d or foreign ID looks identical to a missing one.
409IDEMPOTENCY_KEY_IN_PROGRESSThe first request with this Idempotency-Key is still running — retry in a few seconds.
422IDEMPOTENCY_KEY_REUSEDThat Idempotency-Key was already used for a different body — use a new key for a new request.
429Rate limitedHonor Retry-After and back off. See Rate limits below.
5xxServer errorRetry 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 classLimit
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:

StyleEndpointsHow it works
CursorverificationsPass 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 + limitcarriers, customersPass offset and limit (1–200, default 50). Response includes total.
Cursor (or offset)loadsPass 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

GET/v1/pingany key

Returns 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"
200 response
{
  "ok": true,
  "tenantId": "cmrpgygdl00xcqyrjt7871dbr",
  "user": { "email": "dispatch@acmelogistics.com", "role": "BROKER" },
  "key": { "name": "erp-sync", "prefix": "hkpt_AbC12de", "scopes": ["read", "write"] }
}

List verifications

GET/v1/verificationsany key

Carrier verifications in your workspace, newest first. In the app these appear under Verifications with the same status pipeline.

ParameterTypeDescription
statusenumPipeline 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)
qstringFree-text search across carrier name, MC/DOT, and dispatcher name
minRisknumberOnly verifications whose Trust score (riskScore) is ≥ this value (0–100, higher = safer; 70 or higher is Low risk, 30 or below is High risk)
sortenumupdatedAt:asc | updatedAt:desc (default) | riskScore:asc | riskScore:desc
limitnumberPage size, 1–100 (default 50). Out of range returns 400.
cursorstringThe 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"
200 response (trimmed)
{
  "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

GET/v1/verifications/{id}any key

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

ParameterTypeDescription
idrequiredstringVerification id — from the list endpoint or the app URL
curl "https://api.happytactic.com/api/v1/verifications/cmrpgyhaj01ksqyrjzmza5xzx" \
  -H "Authorization: Bearer $HAPPYTACTIC_API_KEY"
200 response (trimmed — the full record has more fields)
{
  "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

POST/v1/verificationswrite key

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

ParameterTypeDescription
mcNumberstringMC number — send this OR dotNumber
dotNumberstringUSDOT number — send this OR mcNumber
operatingModelenumdispatcher (default) | owner_operator
dispatcherNamestringRequired for a dispatcher-model carrier — the owner is asked to confirm it
dispatcherPhonestringRequired for a dispatcher-model carrier — a 10-digit US number
dispatcherEmailstringOptional
shipperName / shipperEmailstringOptional — the shipper gets the decision by email
confirmDuplicatebooleanThe carrier was already verified today (409 VERIFICATION_DUPLICATE_TODAY) — start another one anyway
startbooleanfalse = 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"}'
201 response
{
  "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.

Send an 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

GET/v1/carriersany key

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

ParameterTypeDescription
qstringFree-text search (name, MC, DOT)
verifiedOnlyboolean-ish"true" to only return carriers that are OK to book (the same as decision=green)
decisionstringFilter 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.
statusstringCarrier operating-status filter
equipmentstringEquipment type (van, reefer, flatbed, …)
limitnumberPage size, 1–200 (default 50). Out of range returns 400.
offsetnumberPagination offset (default 0)
curl "https://api.happytactic.com/api/v1/carriers?verifiedOnly=true&limit=25" \
  -H "Authorization: Bearer $HAPPYTACTIC_API_KEY"
200 response (trimmed)
{
  "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

GET/v1/carriers/{id}any key

Carrier 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).

ParameterTypeDescription
idrequiredstringCarrier 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

GET/v1/customerscrm moduleany key

Your 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.)

ParameterTypeDescription
qstringFree-text search on name / legal name
typestring[]Party type: customer, prospect, vendor, shipper, consignee, … (repeatable)
statusstring[]active, inactive, blocked (repeatable)
creditTierstring[]unrated, high_risk, standard, good, excellent, cash_only (repeatable)
industrystringIndustry filter
tagstring[]Tag filter (repeatable)
updatedAfterISO dateOnly customers updated after this date — use for incremental sync
includeArchivedboolean-ish"true" to include archived customers
limitnumberPage size, 1–200 (default 50)
offsetnumberPagination offset (default 0)
curl "https://api.happytactic.com/api/v1/customers?type=customer&updatedAfter=2026-07-01" \
  -H "Authorization: Bearer $HAPPYTACTIC_API_KEY"
200 response (trimmed)
{
  "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

GET/v1/customers/{id}crm moduleany key

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

ParameterTypeDescription
idrequiredstringCustomer (party) id
curl "https://api.happytactic.com/api/v1/customers/cmrpgygm900yxqyrjy42el8a3" \
  -H "Authorization: Bearer $HAPPYTACTIC_API_KEY"

List loads

GET/v1/loadsloads moduleany key

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

ParameterTypeDescription
statusstring[]Load status (repeatable) — see the status reference
dispatchStagestring[]Operational pipeline stage (repeatable) — a narrower vocabulary derived from status: pending, available, committed, dispatched, at_pickup, in_transit, at_delivery, delivered
qstringFree-text search (load #, customer, city, …)
loadNumberstringLoad number
partyIdstringFilter to one customer
carrierIdstringFilter to one carrier
mcNumberstringCarrier MC number
equipmentTypestringvan, reefer, flatbed, …
modeenumftl, ltl, partial or intermodal
dotNumberstringCarrier USDOT number
carrierNamestringPart of the carrier’s legal or DBA name
commodity / loadTypestringCommodity text; load type (spot, contract, …)
originCity / originZip / destinationCity / destinationZipstringLane filters
customerIdstring[]One or more customer ids (the same customers as partyId; repeatable)
salesRepId / teamId / dispatcherIdstring[]Booked by this rep / team / dispatcher (repeatable)
trackingStatestring[]Tracking state now, e.g. not_started or not_tracked (repeatable)
needsAttentionboolean-ish"true" = only loads with an open Needs attention flag
mineboolean-ish"true" = only loads you booked
bolNumberstringBOL number
referencestringAny reference number (PO, sales order, container, …)
originStatestringOrigin state (2-letter)
destinationStatestringDestination state (2-letter)
pickupFrom / pickupToISO datePickup date window
sortByenumcreatedAt | updatedAt | pickupDate | deliveryDate | loadNumber
sortDirenumasc | desc
limitnumberPage size, 1–200 (default 50). Out of range returns 400.
cursorstringnextCursor from the previous response. Malformed values return 400.
offsetnumberRow 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"
200 response (trimmed)
{
  "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

GET/v1/loads/{id}loads moduleany key

Everything 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).

ParameterTypeDescription
idrequiredstringLoad id
curl "https://api.happytactic.com/api/v1/loads/cmrr6gyru000ld16gducdz4op" \
  -H "Authorization: Bearer $HAPPYTACTIC_API_KEY"
200 response (trimmed)
{
  "load": {
    "id": "cmrr6gyru000ld16gducdz4op",
    "loadNumber": "LD-R6GYRA-GIKR",
    "status": "enroute",
    "dispatchStage": "in_transit",
    "stops": [],
    "charges": [],
    "references": []
  }
}

Create a load

POST/v1/loadsloads modulewrite key

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

ParameterTypeDescription
partyIdstringCRM customer to book the load for (recommended — enables credit checks and invoicing rules)
loadNumberstringYour load number (auto-generated if omitted)
carrierIdstringAttach 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.
statusenumDefaults 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.
referenceNumberstringPrimary reference (PO, sales order, …)
bolNumberstringBOL number
modeenumftl (default), ltl, partial or intermodal
shipperName / shipperEmail / shipperPhonestringFree-text shipper, for loads without a CRM customer (prefer partyId)
pallets / lengthIn / widthIn / heightIn / freightClass / nmfcCodemixedFreight detail
loadType / distancestring / numberspot or contract; miles
quickPayDiscount / carrierAdvancenumberAccounting amounts, USD
trackingModeenumapp_first | app_required | link_only — omit to use your company default
trackingRequiredbooleanPrint the “tracking required” clause on the rate confirmation; omit for your company default
salesRepId / teamId / salesSplit / customFieldsmixedSales rep credited, team, commission split and your custom fields (by name); omit to use your company defaults
originCity / originState / originZip / originAddressstringOrigin
destinationCity / destinationState / destinationZip / destinationAddressstringDestination
pickupDate / deliveryDateISO dateScheduled dates
equipmentTypestringvan, reefer, flatbed, …
weightnumberWeight in lbs
commoditystringWhat is being hauled
pieces / palletsnumberPiece / pallet counts
hazmatbooleanHazmat flag (+ hazmatClass)
tempMinF / tempMaxFnumberTemperature range for reefer loads
customerRatenumberWhat the customer pays
carrierRatenumberYour target / agreed buy
rateCurrencystring3-letter ISO code (default USD)
notesstringLoad 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"}'
201 response (trimmed)
{
  "load": {
    "id": "cmrr6gyru000ld16gducdz4op",
    "loadNumber": "LD-R6GYRA-GIKR",
    "status": "draft",
    "dispatchStage": "pending",
    "customerRate": 1850,
    "carrierRate": 1500,
    "margin": 350,
    "marginPct": 18.92
  },
  "created": true
}
Booking against a customer over their credit limit can be blocked depending on your workspace’s credit-gate setting — you’ll get a 400 with 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.

request header
Idempotency-Key: 6f1c2a9e-3b7d-4c55-9e0a-1d2f3a4b5c6d
SituationResponse
First request with the keyRuns normally (201)
Same key + same body within 24 hThe original response, replayed, with Idempotent-Replayed: true — no second load
Same key + a different body422 IDEMPOTENCY_KEY_REUSED — use a new key
Same key while the first is still running409 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

PATCH/v1/loads/{id}/statusloads modulewrite key

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

ParameterTypeDescription
idrequiredstringLoad id (URL parameter)
toStatusrequiredenumTarget status — see the status reference
notestringOptional 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"}'
200 response (trimmed)
{ "load": { "id": "cmrr6gyru000ld16gducdz4op", "status": "enroute", "dispatchStage": "in_transit" } }

List check calls

GET/v1/loads/{id}/check-callsloads moduleany key

The 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

POST/v1/loads/{id}/check-callsloads modulewrite key

Record a location update, note, or ETA on a load from your system — for example, forwarding positions from your own tracking stack.

ParameterTypeDescription
idrequiredstringLoad id (URL parameter)
city / statestringReported location
lat / lngnumberCoordinates (alternative to city/state)
etaAtISO datetimeEstimated arrival at the next stop
notestringFree-text note (max 500 chars)
sourceenumdispatcher | driver_app | eld | carrier — where the update came from
loadStopIdstringAttach to a specific stop’s timeline
eventTypeenumPer-stop timeline event type (arrived, loaded, departed, …)
reportedAtISO datetimeWhen 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"}'
201 response
{
  "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.

StatusApp labelMeaning
draftDraftBeing built; not yet released
availableAvailableReleased, needs a carrier
postedPostedPosted to a load board
committedCoveredA carrier is on it (offer accepted or booked direct)
assignedAssignedDriver/truck assigned
dispatchedDispatchedDispatch sheet out
at_pickupAt pickupTruck at origin
picked_upPicked upLoaded
enrouteIn TransitRolling
at_deliveryAt deliveryTruck at destination
deliveredDeliveredPOD expected
invoice_auditInvoice auditPaperwork audit queue
invoice_pendingInvoice pendingApproved, awaiting invoicing
invoicedInvoicedCustomer invoiced
carrier_paidCarrier paidCarrier settled
customer_paidCustomer paidCustomer payment received
completedCompletedFully closed
missed_pickupMissed pickupException
tonuTONUTruck ordered, not used
ops_holdOps holdOperational hold (resumable)
accounting_holdAccounting holdAccounting hold (resumable)
claim_filedClaim filedOS&D / claim open
cancelledCancelledCancelled
voidedVoidedVoided

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.

HeaderValue
X-Happytactic-Eventverification_complete or verification_review_required (same as event)
X-Happytactic-TimestampUnix seconds when this attempt was signed
X-Happytactic-Signature-V2t=<timestamp>,v2=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")> — use this
X-Happytactic-Signaturev1: 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.

Node.js
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.

eventWhendata
load.status_changedEvery status move{ status, previousStatus, at, changedBy }
stop.arrivedA stop's arrival was recorded (once per stop){ stop, at, recordedBy: auto | driver | dispatcher | provider }
stop.departedA stop's departure was recorded (once per stop){ stop, at, recordedBy }
eta.changedA stop's first ETA, then whenever it moves 15 minutes or more{ stop, etaAt, previousEtaAt, etaSource, etaStatus, minutesLate, includesBreaks }
locationThe truck's position — at most one every 15 minutes per load{ lat, lng, at }
Body
{
  "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

VersionDateChanges
1.4.1Nov 3, 2026Load 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.0Oct 27, 2026POST /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.0Oct 16, 2026Idempotency-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.1Aug 5, 2026Documentation-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.0Jul 19, 2026Removed POST /v1/verifications (verifications start in the app only). Expanded reference documentation.
1.1.0Jul 19, 2026Key scopes (read/write). TMS writes: create load, change load status, add check calls. Customers endpoints. Last-used tracking on keys.
1.0.0Jul 19, 2026Initial release: ping, verifications, carriers, loads (read-only).