{
  "openapi": "3.1.0",
  "info": {
    "title": "BestConnect Travel API",
    "version": "1.0.0",
    "summary": "BestCare travel units for Australian field service sites.",
    "description": "Prices the travel component of a Best Technology Services field outcome for a list of Australian sites. Each site is matched against the national suburb and postcode table, assigned the dispatching Point of Presence (PoP), a travel zone and the billable BestCare (BC) travel units, with the fly-in and drive-overnight itinerary detail where it applies. The calculator is the same code the BestConnect Quote tool runs (tool version 2.16.9).",
    "contact": {
      "name": "Best Technology Services",
      "email": "info@best-ts.com.au",
      "url": "https://best-ts.com.au"
    }
  },
  "servers": [
    {
      "url": "https://api-demo.best-ts.com.au"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Travel",
      "description": "Travel costing"
    },
    {
      "name": "Service",
      "description": "Health and discovery"
    }
  ],
  "paths": {
    "/v1/travel": {
      "post": {
        "tags": [
          "Travel"
        ],
        "operationId": "calculateTravel",
        "summary": "Cost travel for a list of sites",
        "description": "Send up to 500 sites as structured objects, or a pasted site list as text (one site per line, suburb and postcode on each). Returns one result per site plus a summary. Sites with `error: true` have an address that cannot be trusted and must be corrected before quoting; sites with `review: true` are priced but should be checked by a person.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TravelRequest"
              },
              "examples": {
                "structured": {
                  "summary": "Structured sites with your own references",
                  "value": {
                    "unitRate": 390,
                    "sites": [
                      {
                        "reference": "SITE-001",
                        "suburb": "North Sydney",
                        "postcode": "2060",
                        "state": "NSW"
                      },
                      {
                        "reference": "SITE-002",
                        "suburb": "Dubbo",
                        "postcode": "2830"
                      },
                      {
                        "reference": "SITE-003",
                        "suburb": "Broome",
                        "postcode": "6725",
                        "state": "WA"
                      }
                    ]
                  }
                },
                "pasted": {
                  "summary": "Pasted list",
                  "value": {
                    "text": "North Sydney 2060\nDubbo NSW 2830\nBroome 6725"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Travel costed for every site that could be read.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/XRequestId"
              },
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests allowed per minute for this key."
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Requests left in the current minute."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TravelResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Service"
        ],
        "operationId": "health",
        "summary": "Liveness check",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    },
                    "service": {
                      "type": "string"
                    },
                    "api": {
                      "type": "string"
                    },
                    "appVersion": {
                      "type": "string",
                      "description": "BestConnect Quote tool version the calculator was built from."
                    },
                    "commit": {
                      "type": "string"
                    },
                    "builtAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "ratesReviewed": {
                      "type": "string",
                      "format": "date",
                      "description": "Date the rate table was last reviewed."
                    },
                    "environment": {
                      "type": "string",
                      "enum": [
                        "demo",
                        "production",
                        "development"
                      ]
                    },
                    "uptimeSec": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1": {
      "get": {
        "tags": [
          "Service"
        ],
        "operationId": "describe",
        "summary": "Service descriptor",
        "security": [],
        "responses": {
          "200": {
            "description": "Links to the documentation, the specification and the endpoints."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer <api key>`"
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key"
      }
    },
    "headers": {
      "XRequestId": {
        "schema": {
          "type": "string"
        },
        "description": "Unique id for this call. Quote it when reporting a problem. You may supply your own (8 to 80 characters, letters, digits, . _ : -) and it is echoed back."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The body is not valid JSON or failed validation. `error.details` lists each problem with a JSON path.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or unknown API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "The body exceeds 1 MB.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests this minute. `Retry-After` says how long to wait.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InternalError": {
        "description": "The calculator failed. Quote `error.requestId` when reporting it.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "TravelRequest": {
        "type": "object",
        "description": "Exactly one of `sites` or `text` is required.",
        "properties": {
          "unitRate": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "AUD ex GST per BestCare unit the quote is priced at. Defaults to the list rate. Only the dollar figures in the summary and the drive-overnight conversion depend on it; units do not."
          },
          "sites": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/SiteInput"
            }
          },
          "text": {
            "type": "string",
            "description": "A pasted site list, one site per line. Each line needs a suburb and a postcode; a state is optional. Lines that cannot be read are returned in `skipped`."
          }
        }
      },
      "SiteInput": {
        "type": "object",
        "required": [
          "suburb",
          "postcode"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "maxLength": 100,
            "description": "Your own id for the site. Echoed back on the matching result."
          },
          "suburb": {
            "type": "string",
            "description": "Suburb or locality. Common abbreviations (Mt, Nth, Sth, Pt) are understood."
          },
          "postcode": {
            "type": "string",
            "pattern": "^\\d{3,4}$",
            "description": "Australian postcode. NT postcodes may be sent with or without the leading zero."
          },
          "state": {
            "type": "string",
            "enum": [
              "NSW",
              "VIC",
              "QLD",
              "WA",
              "SA",
              "TAS",
              "NT",
              "ACT"
            ],
            "description": "Optional. Used to disambiguate suburbs that exist in more than one state."
          }
        }
      },
      "TravelResponse": {
        "type": "object",
        "required": [
          "requestId",
          "calculator",
          "unitRate",
          "sites",
          "summary"
        ],
        "properties": {
          "requestId": {
            "type": "string"
          },
          "calculator": {
            "type": "object",
            "properties": {
              "api": {
                "type": "string",
                "const": "v1"
              },
              "appVersion": {
                "type": "string",
                "description": "BestConnect Quote tool version the calculator was built from."
              },
              "ratesReviewed": {
                "type": "string",
                "format": "date"
              },
              "environment": {
                "type": "string"
              }
            }
          },
          "unitRate": {
            "type": "number",
            "description": "The unit rate the dollar figures were calculated at."
          },
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteResult"
            },
            "description": "One entry per input site, in input order."
          },
          "skipped": {
            "type": "array",
            "description": "Only for `text` input: lines that could not be read as a site.",
            "items": {
              "type": "object",
              "properties": {
                "raw": {
                  "type": "string"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          },
          "summary": {
            "$ref": "#/components/schemas/TravelSummary"
          }
        }
      },
      "SiteResult": {
        "type": "object",
        "required": [
          "input",
          "pop",
          "zone",
          "km",
          "bcUnits",
          "matchType",
          "flyIn",
          "regionalPlus",
          "review",
          "error",
          "notes"
        ],
        "properties": {
          "reference": {
            "type": "string",
            "description": "Your reference, when one was sent."
          },
          "input": {
            "type": "object",
            "description": "The site as it was read.",
            "properties": {
              "suburb": {
                "type": "string"
              },
              "postcode": {
                "type": "string"
              },
              "state": {
                "type": "string"
              },
              "raw": {
                "type": "string"
              }
            }
          },
          "pop": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dispatching Point of Presence (the Best location the engineer travels from). Null when the address could not be matched."
          },
          "zone": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "METRO",
              "REGIONAL",
              "REGIONAL+",
              "REMOTE",
              "DRIVE-OVERNIGHT",
              "FLY-IN",
              null
            ],
            "description": "Travel zone. See the documentation for what each means."
          },
          "km": {
            "type": [
              "number",
              "null"
            ],
            "description": "One-way road distance from the PoP to the site."
          },
          "driveHours": {
            "type": [
              "number",
              "null"
            ],
            "description": "One-way road drive time in hours."
          },
          "bcUnits": {
            "type": [
              "number",
              "null"
            ],
            "description": "Billable BestCare travel units for one attendance by one engineer, from road distance (or the fly-in / drive-overnight itinerary). Excludes the zone uplift, which is in `zoneUpliftUnits`. Null when the site needs a custom quote."
          },
          "zoneUpliftUnits": {
            "type": [
              "number",
              "null"
            ],
            "description": "Zone uplift per attendance, added on top of bcUnits: 0 metro, 0.25 regional and regional+, 1 remote, 0 drive-overnight and fly-in (their itinerary is already in bcUnits). Null when unmatched."
          },
          "matchType": {
            "type": "string",
            "enum": [
              "exact",
              "postcode",
              "mismatch",
              "not_found"
            ],
            "description": "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."
          },
          "flyIn": {
            "type": "boolean",
            "description": "The site is reached by air."
          },
          "regionalPlus": {
            "type": "boolean",
            "description": "Dispatched from a regional PoP with travel beyond 100 km."
          },
          "review": {
            "type": "boolean",
            "description": "Priced, but a person should check this row before it is quoted."
          },
          "error": {
            "type": "boolean",
            "description": "The address cannot be trusted. Correct it and resubmit; do not quote from this row."
          },
          "suggestion": {
            "type": "string",
            "description": "Plain-English fix for an address error, for example 'Robina is postcode 4226'."
          },
          "wrongField": {
            "type": "string",
            "enum": [
              "suburb",
              "postcode",
              "state",
              "both"
            ],
            "description": "Which input field to correct."
          },
          "suggestedInput": {
            "type": "object",
            "description": "The corrected address, ready to resubmit.",
            "properties": {
              "suburb": {
                "type": "string"
              },
              "state": {
                "type": "string"
              },
              "postcode": {
                "type": "string"
              }
            }
          },
          "flyInDetail": {
            "$ref": "#/components/schemas/FlyInDetail"
          },
          "notes": {
            "type": "string",
            "description": "How the units were arrived at, in words."
          }
        }
      },
      "FlyInDetail": {
        "type": "object",
        "description": "Present on FLY-IN sites priced from the airport table.",
        "properties": {
          "airport": {
            "type": "string",
            "description": "IATA code of the arrival airport."
          },
          "airportName": {
            "type": "string"
          },
          "originPop": {
            "type": "string",
            "description": "Capital PoP the engineer flies from."
          },
          "flightHours": {
            "type": "number",
            "description": "One-way flight hours."
          },
          "airportKm": {
            "type": "number",
            "description": "Road km from the airport to the site."
          },
          "viaStrip": {
            "type": "string",
            "description": "Local strip with no bookable fare, when the fare was flown to a nearby airport instead."
          },
          "fareClass": {
            "type": "string",
            "enum": [
              "TRUNK",
              "REGIONAL",
              "PILBARA",
              "REMOTE"
            ]
          },
          "nightsClass": {
            "type": "string",
            "enum": [
              "TRUNK",
              "REGIONAL",
              "PILBARA",
              "REMOTE"
            ]
          },
          "fareAllowance": {
            "type": "number",
            "description": "Return fare allowance for one engineer, before uplift."
          },
          "nights": {
            "type": "integer",
            "description": "Nights in the base itinerary."
          },
          "travelHours": {
            "type": "number",
            "description": "Billable travel hours door to door, both ways."
          },
          "passSales": {
            "type": "number",
            "description": "Pass-through at partner price (fare, nights, car, fuel, taxi), including uplift, AUD ex GST."
          }
        }
      },
      "TravelSummary": {
        "type": "object",
        "properties": {
          "totalSites": {
            "type": "integer"
          },
          "matched": {
            "type": "integer"
          },
          "notFound": {
            "type": "integer"
          },
          "mismatch": {
            "type": "integer"
          },
          "errorCount": {
            "type": "integer",
            "description": "Sites with `error: true`. These block a quote until corrected."
          },
          "flyIn": {
            "type": "integer"
          },
          "reviewCount": {
            "type": "integer"
          },
          "byZone": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Site count per zone."
          },
          "regionalPlus": {
            "type": "integer"
          },
          "totalBcUnits": {
            "type": "number",
            "description": "Sum of bcUnits across priced sites (travel only, no uplifts)."
          },
          "zoneUpliftUnits": {
            "type": "number",
            "description": "Sum of zoneUpliftUnits across matched sites, for one attendance each."
          },
          "exGst": {
            "type": "number",
            "description": "totalBcUnits × unitRate, AUD (travel only; uplifts are not included)."
          },
          "gst": {
            "type": "number"
          },
          "incGst": {
            "type": "number"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "requestId"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "not_found",
                  "method_not_allowed",
                  "unauthorized",
                  "rate_limited",
                  "payload_too_large",
                  "invalid_json",
                  "validation_failed",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "requestId": {
                "type": "string"
              },
              "details": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "path": {
                      "type": "string",
                      "description": "JSON path of the offending field, for example `sites[2].postcode`."
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}