@quaillogistics/adapters
v0.4.1
Published
TMS adapter library — fetch and map data from 3PL Systems, Turvo, and other TMS platforms
Readme
@quaillogistics/adapters
TMS adapter library for fetching and mapping data from external TMS systems. Used internally by QUAIL Logistics.
Install
npm install @quaillogistics/adaptersUsage
Each TMS adapter is a separate subpath export. Import only what you need:
import { ThreePLClient, mapThreePLLoad } from '@quaillogistics/adapters/3pl'
import { TurvoClient } from '@quaillogistics/adapters/turvo'
import { chunkDateRange } from '@quaillogistics/adapters/common'3PL Systems
const client = new ThreePLClient({
apiHost: '3pl.hyperiontms.com',
clientId: '...',
clientSecret: '...',
globalClientId: '...',
globalClientSecret: '...',
})
// Layer 1: typed raw API responses
const rawLoads = await client.getLoads({ startDate, endDate })
// Layer 2: opt-in mapped intermediaries
const loads = rawLoads.map(mapThreePLLoad)Writing loads to 3PL
There is no 3PL sandbox, so the API's own dry run is the safety net: dryRun sends test: true,
which makes the server validate the real body, return the placeholder id 10000 and persist
nothing. Preflight every live write with it.
Live writes need pushEnabled: true — without it they throw ThreePLPushDisabledError. Dry
runs do not: they persist nothing, so you can validate an entire create flow against the live API
with writes still switched off. Flipping pushEnabled is then the only thing that ever makes a
write real.
import { ThreePLClient, buildCreateShipment } from '@quaillogistics/adapters/3pl'
const client = new ThreePLClient({ ...credentials, pushEnabled: true })
const body = buildCreateShipment({
customerId: 1624635,
equipmentType: 'DRY_VAN', // canonical — mapped to the write literal "Van"
mode: 'LTL',
status: 'QUOTED',
items: [{ description: 'General Freight', freightClass: '70', pieces: 2, weight: 500 }],
stops: [
{ stopType: 'PICKUP', sequence: 0, zipCode: '90755', appointmentEarly, appointmentLate },
{ stopType: 'DELIVERY', sequence: 1, zipCode: '90802', appointmentEarly, appointmentLate },
],
})
const preflight = await client.createShipment(body, { dryRun: true })
// { loadId: null, dryRun: true, returnedId: 10000 } — validated, nothing written
const created = await client.createShipment(body)
// { loadId: 95674, dryRun: false, returnedId: 95674 }
await client.updateShipment({ loadid: created.loadId!, poReference: 'PO-1' }) // merges
await client.cancelShipment(created.loadId!) // restatus to "Canceled"; nothing deletes a loadWhat the dry run actually checks, from a negative-controlled sweep: equipmentType, shipmentMode
and the zips are validated (a bogus value 400s, so a 200 is real evidence), and shipmentStatus
is not — a nonsense status came back 200. Preflight a body and you have proven its lane and
equipment, not its status.
Four things the API does that are worth knowing before you build on it:
- The write body is flat and two-stop.
shipperX/consigneeXscalars, not the read path'sstops[]. A multi-stop load cannot be expressed at all —buildCreateShipmentrefuses one rather than silently dropping the middle stops. UpdateShipmentmerges. Send only what changed; the rest of the load survives. The mappers omit keys you did not set rather than sending nulls.- Cancel is a restatus, not a delete. There is no Global cancel endpoint, and nothing on any
surface deletes a shipment — a cancelled load keeps coming back from
GetLoads. - The write enum is not the read vocabulary. Reads emit
Dry Van,Step Deck,Hot Shot; the write API rejects every literal with a space and matches its own, narrower enum. Two canonical types — BOX_TRUCK and POWER_ONLY — have no write literal at all, andbuildCreateShipmentrefuses them rather than sending the nearest wrong one.
Paths, verbs and semantics were verified live against the MLT tenant on 2026-09-08
(quaillogistics/quail-integrations → evals/2026-09-08-threepl-writes.md). Writes return no
rate-limit headers, so pace them blind, and never blind-retry a create — it mints duplicate loads.
Turvo
const client = new TurvoClient({
apiHost: 'app.turvo.com',
username: '...',
password: '...',
apiKey: '...',
clientId: '...',
clientSecret: '...',
})
const shipment = await client.getShipment(123)Mock mode
Every adapter supports mock: true for testing without hitting real APIs:
const client = new ThreePLClient({
...credentials,
mock: true,
})
const loads = await client.getLoads({ startDate, endDate }) // deterministic test dataObservability
Pass logging callbacks to see HTTP activity:
const client = new ThreePLClient({
...credentials,
onRequest: (method, path) => console.log(`>> ${method} ${path}`),
onResponse: (method, path, status, ms) => console.log(`<< ${status} ${path} (${ms}ms)`),
})Adapters
| Adapter | Import | Capabilities |
|---------|--------|-------------|
| 3PL Systems | @quaillogistics/adapters/3pl | Pull: loads, carriers, customers, contacts, commissions, AP invoices, vendor payments, tracking, documents. Push: create / update / cancel shipment, with a live-API dry run |
| Turvo | @quaillogistics/adapters/turvo | Push: shipment CRUD. Read: shipments, locations |
Development
npm install
npm run test # run tests
npm run test:watch # watch mode
npm run typecheck # type check
npm run build # build to dist/
npm run check-deps # verify zero production dependenciesPublishing
- Bump version in
package.json - Commit:
git commit -m "v0.1.0" - Tag:
git tag v0.1.0 - Push:
git push && git push --tags
CI will run checks and publish to npm automatically.
