BestCare travel units for Australian field service sites, priced by the same calculator that runs inside BestConnect Quote. Send a list of sites, get back the dispatching Point of Presence, the travel zone and the billable units for each, plus a summary.
Three calls cover the whole service. GET /health needs no key and tells you the service is up and which calculator version it is running. GET /v1/openapi.json is the machine-readable specification. POST /v1/travel does the work.
curl -s https://api-demo.best-ts.com.au/v1/travel \
-H "Authorization: Bearer $TRAVEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sites":[{"reference":"SITE-001","suburb":"North Sydney","postcode":"2060"}]}'
Replace the key with the one Best Technology Services issued to you. Keys are environment-specific: a demo key does not work in production.
Every call to /v1/travel carries an API key, in either header form:
| Header | Value |
|---|---|
Authorization | Bearer <key> (preferred) |
X-Api-Key | <key> |
Call the API from your server, never from a browser, so the key stays out of client code. Keys can be rotated on request: a new key is issued, both work for an agreed period, then the old one is retired. A missing or unknown key returns 401.
The body is a JSON object with exactly one of sites or text, and an optional unitRate.
| Field | Type | Meaning |
|---|---|---|
sites | array, 1 to 500 | Structured sites. Each needs suburb and postcode; state is optional but resolves suburbs that exist in more than one state; reference (up to 100 characters) is your own id, echoed back on the matching result. |
text | string | A pasted site list instead of sites: one site per line, suburb and postcode on each. Lines that cannot be read come back in skipped. |
unitRate | number, optional | AUD ex GST per BestCare unit the quote is priced at. Defaults to the list rate. Units do not depend on it; only the dollar figures in the summary and the drive-overnight conversion do. |
{
"unitRate": 390,
"sites": [
{ "reference": "SITE-001", "suburb": "North Sydney", "postcode": "2060", "state": "NSW" },
{ "reference": "SITE-002", "suburb": "Mudgee", "postcode": "2850" },
{ "reference": "SITE-003", "suburb": "Coober Pedy", "postcode": "5723", "state": "SA" }
]
}
{
"requestId": "6f1c0a3e-...",
"calculator": { "api": "v1", "appVersion": "2.16.9", "ratesReviewed": "2026-08-15", "environment": "demo" },
"unitRate": 390,
"sites": [
{
"reference": "SITE-001",
"input": { "suburb": "North Sydney", "postcode": "2060", "state": "NSW" },
"pop": "Sydney", "zone": "METRO", "km": 0, "driveHours": 0,
"bcUnits": 0, "zoneUpliftUnits": 0, "matchType": "exact",
"flyIn": false, "regionalPlus": false, "review": false, "error": false,
"notes": ""
},
{
"reference": "SITE-002",
"input": { "suburb": "Mudgee", "postcode": "2850" },
"pop": "Dubbo", "zone": "REGIONAL+", "km": 140, "driveHours": 1.75,
"bcUnits": 0.5, "zoneUpliftUnits": 0.25, "matchType": "exact",
"flyIn": false, "regionalPlus": true, "review": false, "error": false,
"notes": "Regional+ — beyond 100km of a regional PoP"
},
{
"reference": "SITE-003",
"input": { "suburb": "Coober Pedy", "postcode": "5723", "state": "SA" },
"pop": "FLY-IN", "zone": "FLY-IN", "km": 850, "driveHours": null,
"bcUnits": 7.75, "zoneUpliftUnits": 0, "matchType": "exact",
"flyIn": true, "regionalPlus": false, "review": false, "error": false,
"flyInDetail": {
"airport": "CPD", "airportName": "Coober Pedy", "originPop": "Adelaide",
"flightHours": 1.6, "airportKm": 10, "fareClass": "REGIONAL", "nightsClass": "REGIONAL",
"fareAllowance": 650, "nights": 1, "travelHours": 10, "passSales": 1171.5
},
"notes": "Fly-in Adelaide to Coober Pedy (Regional, 1.6h), 10 km from airport, 1 night(s) at 1 unit: 10.0h travel on the day-rate ladder + $1171.50 pass-through incl. $650 fare"
}
],
"summary": {
"totalSites": 3, "matched": 3, "notFound": 0, "mismatch": 0, "errorCount": 0,
"flyIn": 1, "reviewCount": 0, "regionalPlus": 1,
"byZone": { "METRO": 1, "REGIONAL": 0, "REGIONAL+": 1, "REMOTE": 0, "DRIVE-OVERNIGHT": 0, "FLY-IN": 1 },
"totalBcUnits": 8.25, "zoneUpliftUnits": 0.25, "exGst": 3217.5, "gst": 321.75, "incGst": 3539.25
}
}
Abridged from a real demo response. Results come back in input order, one per site; a site inside its PoP's included distance returns zero units and an empty notes. The header X-Request-Id carries the same id as the body.
| Field | Meaning |
|---|---|
reference | Your reference, when you sent one. |
input | The site as it was read: suburb, postcode, state. |
pop | The dispatching Point of Presence: the Best location the engineer travels from. null when the address could not be matched. |
zone | Travel zone (see below). null when unmatched. |
km, driveHours | One-way road distance and drive time from the PoP. |
bcUnits | Billable BestCare travel units for one attendance by one engineer: the road distance beyond the PoP's included band, or the whole fly-in or drive-overnight itinerary. Excludes the zone uplift. null means the site needs a custom quote from Best; do not price it yourself. |
zoneUpliftUnits | The zone uplift to add per attendance on top of bcUnits: 0 metro, 0.25 regional and regional+, 1 remote, 0 for drive-overnight and fly-in (their itinerary is already in bcUnits). Travel per attendance at a site is bcUnits + zoneUpliftUnits. |
matchType | exact suburb and postcode matched · postcode suburb unknown, priced from the postcode · mismatch both real but they do not belong together · not_found neither recognised. |
error | The address cannot be trusted. Set for mismatch and not_found. Correct the address and resubmit; a quote must not be built from an error row. suggestion, wrongField and suggestedInput tell you what to fix and offer the corrected address. |
review | A person must look at this row before it is quoted. Set on every address error, and on sites the calculator could not price: a fly-in with no scheduled airport within reach, or a site missing distance data. Those come back with bcUnits: null and Best quotes the travel. |
flyIn, flyInDetail | The site is reached by air, with the itinerary the units came from: arrival airport, origin PoP, flight hours, airport-to-site km, fare allowance, nights, travel hours and the pass-through amount. |
regionalPlus | Dispatched from a regional PoP with travel beyond 100 km. |
notes | How the units were arrived at, in words. Suitable to show to a person. |
| Field | Meaning |
|---|---|
totalSites, matched, notFound, mismatch | Counts by match outcome. |
errorCount | Sites with error: true. If this is not zero, the quote is blocked until the addresses are corrected. |
reviewCount, flyIn, regionalPlus | Sites flagged for review, reached by air, or regional-plus. |
byZone | Site count per zone. |
totalBcUnits | Sum of bcUnits across priced sites, travel only. Sites with bcUnits: null are excluded. |
zoneUpliftUnits | Sum of zoneUpliftUnits across matched sites, for one attendance each. |
exGst, gst, incGst | Dollar figures for totalBcUnits at the unitRate returned alongside. Uplifts are not included. |
The zone follows from the dispatching PoP's class and the road drive time. Distances and zones are precomputed from real road routing and never estimated on the fly.
| Zone | Meaning |
|---|---|
METRO | Served from a metro PoP within its included distance band. |
REGIONAL | Served by road from a metro or regional PoP, beyond the metro band and within a day's drive. |
REGIONAL+ | Regional dispatch with travel beyond 100 km. |
REMOTE | Within reach of a remote PoP. |
DRIVE-OVERNIGHT | Reachable by road, but 3.5 to 5 hours one way, so the engineer stays overnight. Priced deterministically from the drive time, nights, car and fuel; notes spells out the itinerary. |
FLY-IN | Reached by air. Priced from the airport table where a scheduled service exists; otherwise bcUnits is null and the site is a custom quote. |
Travel beyond the PoP's included distance (60 km from a metro PoP, 100 km regional, 20 km remote) accrues in quarter-unit steps per started 20 km. Regional and remote attendances also carry a zone uplift, returned separately as zoneUpliftUnits. The notes field on each site spells out the working.
Every error is JSON with the same shape. details appears on validation failures and names each offending field by JSON path.
{
"error": {
"code": "validation_failed",
"message": "The request did not pass validation.",
"requestId": "6f1c0a3e-...",
"details": [
{ "path": "sites[1].postcode", "message": "postcode is required." }
]
}
}
| Status | error.code | When |
|---|---|---|
| 400 | invalid_json | The body could not be parsed. |
| 400 | validation_failed | A field is missing or malformed. See details. |
| 401 | unauthorized | Missing or unknown API key. |
| 404 | not_found | No such route. |
| 405 | method_not_allowed | Wrong HTTP method for the route. |
| 413 | payload_too_large | Body over 1 MB. |
| 429 | rate_limited | Over 120 requests in a minute. Wait Retry-After seconds. |
| 500 | internal_error | The calculator failed. Quote the requestId when you report it. |
A 400 never means the sites were wrong, only the request. Address problems come back as 200 with error: true on the affected sites, so one bad address does not fail the whole list.
X-RateLimit-Limit and X-RateLimit-Remaining are on every priced response.500 with the same body, and reuse your X-Request-Id so both attempts are traceable to one call.The path carries the major version. Additive changes (new optional fields, new zones) ship within v1 without notice; anything that would change the meaning of an existing field ships as v2 alongside v1, with at least three months before v1 is retired. The calculator version and the date the rates were last reviewed are on every response, so you can tell when the figures moved.
Questions, keys and problem reports: info@best-ts.com.au, or your Best Technology Services contact. Include the requestId.
Consider it done.