Docs

Overview

Public documentation4 core address endpointsAPI key authentication

Documentation built for shipping a production geocoding integration quickly.

Wherabouts gives you a clean HTTP interface for address autocomplete, reverse geocoding, nearby lookup, and canonical address retrieval. This page is the opinionated path through the API: what to call first, how authentication works, what validation happens on the server, and how to structure a reliable implementation.

Search-first flow

Use autocomplete to capture intent early, then persist the selected address ID for deterministic follow-up calls.

Coordinate-aware endpoints

Reverse and nearby endpoints validate coordinate ranges and return distance-aware results for operational workflows.

Simple auth model

Authenticate with `Authorization: Bearer` or `X-API-Key`. Successful `2xx` responses are automatically recorded against the key that made the request.

First request

Quickstart

If you are integrating Wherabouts for the first time, start with autocomplete. It exercises the authentication path, the public API hostname, and the response envelope you will see across the address search surface.

curl

curl "https://api.wherabouts.com/api/v1/addresses/autocomplete?q=123+Main+St&country=AU" \
  -H "Authorization: Bearer wh_live_your_api_key"
What to expect
Autocomplete returns a `results` array plus a `count` field.

Auth

Pass the key in Authorization: Bearer <key> or X-API-Key.

Validation

Queries shorter than 2 characters fail fast with `400`.

Usage tracking

Only successful `2xx` responses increment daily usage. Production requests and interactive explorer tests are recorded separately.

Contract

Use the published OpenAPI spec

The public REST contract is also published as an OpenAPI document so SDK work, contract review, and external tooling can target the same source of truth as the docs page.

Spec URL
Fetch the machine-readable contract directly from the app.
https://api.wherabouts.com/api/openapi.json

Use this document for SDK generation, contract reviews, Postman imports, or CI checks against the current v1 surface.

What it covers
The spec mirrors the currently supported public address endpoints.

Every documented endpoint includes authentication requirements, parameters, error schemas, and a response summary.

If you are building a client library, prefer the OpenAPI document plus the interactive explorer together so you can validate both static contracts and live behavior.

SDK preview

Use the first-party JS/TS client

Phase 2 starts by giving JavaScript and TypeScript consumers a typed client instead of forcing every integration to hand-roll `fetch` wrappers. The current preview SDK is a lightweight REST client over the same public v1 endpoints documented here.

Workspace package
The SDK currently lives in this repo as a typed workspace package.
@wherabouts/sdk

It exposes a single `createWheraboutsClient()` entrypoint, stable response types for the public address API, and a normalized `WheraboutsApiError` for non-`2xx` failures.

Why it exists
The public platform contract is REST, so the SDK targets the documented v1 endpoints directly.

This keeps public client adoption separate from the internal dashboard's ORPC transport and makes the SDK a clean fit for Node runtimes, browser apps, workers, and framework wrappers.

The first preview covers autocomplete, reverse geocoding, nearby search, and canonical address lookup by ID.

TypeScript SDK

import { createWheraboutsClient } from "@wherabouts/sdk";

const client = createWheraboutsClient({
  apiKey: process.env.WHERABOUTS_API_KEY!,
});

const payload = await client.addresses.autocomplete({
  q: "123 Main St",
  country: "AU",
  limit: 5,
});

Authentication

Send the API key on every request

The public address API is protected with API keys rather than session auth. That keeps server-to-server usage simple while still allowing the dashboard to manage keys and inspect usage safely.

Accepted headers

Preferred

Authorization: Bearer wh_live_your_api_key

Also supported

X-API-Key: wh_live_your_api_key
Failure behavior

Missing keys return `401` with guidance to send a bearer token or `X-API-Key`.

Invalid or revoked keys also return `401`. The address handler is never called when authentication fails.

Successful requests update `lastUsedAt` and feed daily usage accounting automatically. Explorer-originated test requests are marked separately from production traffic.

How interactive testing works

When you are signed in, the dashboard explorer defaults to a managed-key proxy flow. You choose one of your active keys and the explorer sends the request server-side without requiring you to paste the raw secret.

If you need to validate a specific bearer token manually, the explorer also supports a raw-key mode. Raw keys are kept only for the current browser session and are never persisted by the explorer.

Every explorer request is labeled as `Test traffic`, so it shows up separately from production usage in the dashboard.

API surface

Core endpoints

These are the four address endpoints currently exposed in the app. The docs below mirror the actual path names and request validation behavior implemented on the server today.

GET/api/v1/addresses/autocomplete

Autocomplete

Autocomplete returns up to 20 ranked address candidates for a partial query. It is the best default starting point for signup forms, checkout flows, address capture, and internal tools that need fast suggestions without embedding a map SDK.

Why you would use it
Search for matching addresses as a user types.

Requests with a query shorter than 2 characters return `400`.

Use `country` and `state` filters to keep search results tight for known regions.

Successful `2xx` requests are counted toward usage for the calling API key.

Parameters
Validate and normalize these values before you send the request.
qRequiredstring

Free-form address input. Must be at least 2 characters.

countryOptionalstring

Optional country filter such as `AU`.

stateOptionalstring

Optional state filter such as `VIC` or `NSW`.

limitOptionalnumber

Maximum results to return. Defaults to 10 and is capped at 20.

Example Response

{
  "results": [
    {
      "id": 104233,
      "formattedAddress": "123 Main St, Melbourne VIC 3000, AU",
      "streetAddress": "123 Main St",
      "locality": "Melbourne",
      "state": "VIC",
      "postcode": "3000",
      "country": "AU",
      "latitude": -37.8136,
      "longitude": 144.9631
    }
  ],
  "count": 1
}
GET/api/v1/addresses/reverse

Reverse Geocoding

Reverse geocoding searches for the closest address within 200 meters of the provided coordinate pair and returns a single best match. This is a strong fit for mobile location capture, driver tooling, and QA flows that need to validate map-selected points.

Why you would use it
Resolve a coordinate pair to the nearest address.

Invalid or out-of-range coordinates return `400`.

If no address exists within 200 meters, the API returns `404`.

The response includes `distance` so you can reason about match quality.

Parameters
Validate and normalize these values before you send the request.
latRequirednumber

Latitude value between -90 and 90.

lngRequirednumber

Longitude value between -180 and 180.

Example Response

{
  "address": {
    "id": 104233,
    "formattedAddress": "123 Main St, Melbourne VIC 3000, AU",
    "streetAddress": "123 Main St",
    "locality": "Melbourne",
    "state": "VIC",
    "postcode": "3000",
    "country": "AU",
    "longitude": 144.9631,
    "latitude": -37.8136,
    "confidence": 92
  },
  "distance": 18,
  "query": {
    "lat": -37.8136,
    "lng": 144.9631
  }
}
GET/api/v1/addresses/nearby

Nearby Search

Nearby search returns multiple address records ordered by distance from the query point. It is useful when you need to inspect candidate addresses near an asset, confirm catchment coverage, or build operational tooling for support and logistics teams.

Why you would use it
Find addresses inside a radius around a coordinate.

Results are ordered by computed geographic distance from the query point.

Country filters are normalized to uppercase before execution.

Use smaller radii for operational tooling to avoid noisy address lists.

Parameters
Validate and normalize these values before you send the request.
latRequirednumber

Latitude value between -90 and 90.

lngRequirednumber

Longitude value between -180 and 180.

radiusOptionalnumber

Radius in meters. Defaults to 1000 and is capped at 50000.

limitOptionalnumber

Maximum results to return. Defaults to 10 and is capped at 50.

countryOptionalstring

Optional country filter such as `AU`.

Example Response

{
  "results": [
    {
      "id": 104233,
      "country": "AU",
      "state": "VIC",
      "locality": "Melbourne",
      "postcode": "3000",
      "streetName": "Main",
      "streetType": "St",
      "numberFirst": "123",
      "longitude": 144.9631,
      "latitude": -37.8136,
      "distance": 42
    }
  ],
  "count": 1,
  "query": {
    "lat": -37.8136,
    "lng": 144.9631,
    "radius": 500
  }
}
GET/api/v1/addresses/{id}

Address by ID

After you have selected an address from autocomplete or another indexed flow, use the address ID endpoint to retrieve the underlying record again without re-running search. This keeps downstream workflows deterministic and removes ambiguity around free-form input.

Why you would use it
Fetch the canonical address record for a known identifier.

Non-numeric IDs return `400`.

Unknown IDs return `404`.

The full address payload includes canonical fields such as `gnafPid` and `confidence` when available.

Parameters
Validate and normalize these values before you send the request.
idRequirednumber

Numeric address identifier returned by search responses.

Example Response

{
  "id": 104233,
  "country": "AU",
  "state": "VIC",
  "locality": "Melbourne",
  "postcode": "3000",
  "streetName": "Main",
  "streetType": "St",
  "streetSuffix": null,
  "buildingName": null,
  "flatType": null,
  "flatNumber": null,
  "levelType": null,
  "levelNumber": null,
  "numberFirst": "123",
  "numberLast": null,
  "longitude": 144.9631,
  "latitude": -37.8136,
  "confidence": 92,
  "gnafPid": "GAVIC123456789"
}
GET/api/v1/addresses/geocode

Forward Geocode

Forward geocoding converts a human-readable address into a canonical address record with latitude and longitude. Pass an unstructured query in `q` or set `structured=true` and supply individual fields (`street`, `locality`, `state`, `postcode`). The server returns the single best match.

Why you would use it
Resolve an address string or structured fields to a coordinate.

Either `q` or `structured=true` with at least one field is required.

Returns `404` when no match can be found.

The response shape mirrors the autocomplete candidate object.

Parameters
Validate and normalize these values before you send the request.
qOptionalstring

Unstructured address text (minimum 5 characters). Omit when using structured mode.

structuredOptionalstring

Set to `true` to use structured field inputs instead of `q`.

streetOptionalstring

Street address line (structured mode).

localityOptionalstring

Suburb or city name (structured mode).

stateOptionalstring

State abbreviation such as `VIC` (structured mode).

postcodeOptionalstring

Postcode (structured mode).

countryOptionalstring

Country code such as `AU`.

Example Response

{
  "id": 104233,
  "formattedAddress": "123 Main St, Melbourne VIC 3000, AU",
  "streetAddress": "123 Main St",
  "locality": "Melbourne",
  "state": "VIC",
  "postcode": "3000",
  "country": "AU",
  "latitude": -37.8136,
  "longitude": 144.9631,
  "confidence": 92
}
POST/api/v1/geocode/batch

Batch Submit

Batch geocoding accepts an array of address strings and processes them asynchronously. The endpoint returns a `jobId` immediately; poll `GET /api/v1/geocode/batch/{jobId}` until `status` is `completed`, then fetch results.

Why you would use it
Submit a list of addresses for background geocoding.

Submits as `POST` with a JSON body — not executable in the try-it explorer.

Returns `202 Accepted` with a `jobId` for polling.

See the Batch Lifecycle section for the full submit → poll → results flow.

Parameters
Validate and normalize these values before you send the request.
addressesRequiredstring[]

JSON array of address strings to geocode.

Example Response

{
  "jobId": "job_abc123xyz"
}
GET/api/v1/geocode/batch/{jobId}

Batch Poll

Poll this endpoint after submitting a batch job. When `status` is `completed` the results endpoint is ready. When `status` is `failed` the job encountered an unrecoverable error.

Why you would use it
Check the status of a running batch geocoding job.

Poll at a reasonable interval (e.g. every 2 seconds) — avoid tight loops.

Possible statuses: `pending`, `processing`, `completed`, `failed`.

`processed` and `total` fields are present once processing begins.

Parameters
Validate and normalize these values before you send the request.
jobIdRequiredstring

Job ID returned by the batch submit endpoint.

Example Response

{
  "jobId": "job_abc123xyz",
  "status": "processing",
  "total": 500,
  "processed": 142
}
GET/api/v1/geocode/batch/{jobId}/results

Batch Results

Once `status` is `completed`, fetch the full result set from this endpoint. Results are ordered to match the input array — each entry contains either a resolved address or a `null` match with an error reason.

Why you would use it
Retrieve geocoded results for a completed batch job.

Returns `400` if the job is not yet in `completed` state.

Each result entry includes the original input address and the matched record (or `null`).

Parameters
Validate and normalize these values before you send the request.
jobIdRequiredstring

Job ID returned by the batch submit endpoint.

Example Response

{
  "results": [
    {
      "input": "123 Main St Melbourne VIC",
      "match": {
        "id": 104233,
        "formattedAddress": "123 Main St, Melbourne VIC 3000, AU",
        "latitude": -37.8136,
        "longitude": 144.9631,
        "confidence": 92
      }
    },
    {
      "input": "not a real address xyz",
      "match": null,
      "error": "no_match"
    }
  ]
}
POST/api/v1/zones

Create Zone

Creates a named zone defined by a GeoJSON Polygon geometry. Zones are used for point-in-polygon tests, address enumeration, and triggering webhook events when devices enter or exit. Each project may have up to 500 zones.

Why you would use it
Create a geofence zone for a project.

Submits as `POST` with a JSON body — not executable in the try-it explorer.

Returns `400` with code `zone_limit_exceeded` when the project has 500 zones.

PostGIS validates the geometry; invalid or unclosed rings return `400`.

Parameters
Validate and normalize these values before you send the request.
projectIdRequiredstring

Project identifier.

nameRequiredstring

Human-readable zone name.

geometryRequiredobject

GeoJSON Polygon geometry — coordinates must form a closed ring.

Example Response

{
  "id": "zone_01HX3K9R",
  "projectId": "proj_abc",
  "name": "Melbourne CBD",
  "createdAt": "2026-06-05T00:00:00.000Z"
}
GET/api/v1/zones

List Zones

Returns the full list of zones for the given project. Zone geometry (GeoJSON) is included so the map can render all polygons in one request.

Why you would use it
List all zones for a project.

Geometry is included in every zone record for map rendering.

Up to 500 zones are returned (the per-project limit).

Parameters
Validate and normalize these values before you send the request.
projectIdRequiredstring

Project identifier.

Example Response

{
  "zones": [
    {
      "id": "zone_01HX3K9R",
      "name": "Melbourne CBD",
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[144.95, -37.82], [144.97, -37.82], [144.97, -37.81], [144.95, -37.81], [144.95, -37.82]]]
      },
      "createdAt": "2026-06-05T00:00:00.000Z"
    }
  ]
}
GET/api/v1/zones/{id}

Get Zone

Returns the full zone record including geometry. Useful for re-rendering a specific zone on the map or verifying the stored polygon.

Why you would use it
Fetch a single zone by ID.

Returns `404` for unknown zone IDs.

Geometry is returned as a GeoJSON Polygon.

Parameters
Validate and normalize these values before you send the request.
idRequiredstring

Zone identifier.

Example Response

{
  "id": "zone_01HX3K9R",
  "projectId": "proj_abc",
  "name": "Melbourne CBD",
  "geometry": {
    "type": "Polygon",
    "coordinates": [[[144.95, -37.82], [144.97, -37.82], [144.97, -37.81], [144.95, -37.81], [144.95, -37.82]]]
  },
  "createdAt": "2026-06-05T00:00:00.000Z"
}
PUT/api/v1/zones/{id}

Update Zone

Replaces the zone's name, geometry, or both. Partial updates are supported — omit fields you do not want to change. The geometry is re-validated by PostGIS on every update.

Why you would use it
Update a zone's name or geometry.

Submits as `PUT` with a JSON body — not executable in the try-it explorer.

Returns `404` for unknown zone IDs.

Invalid geometry returns `400`.

Parameters
Validate and normalize these values before you send the request.
idRequiredstring

Zone identifier (path parameter).

nameOptionalstring

New zone name.

geometryOptionalobject

Replacement GeoJSON Polygon geometry.

Example Response

{
  "id": "zone_01HX3K9R",
  "projectId": "proj_abc",
  "name": "Melbourne CBD (updated)",
  "updatedAt": "2026-06-05T01:00:00.000Z"
}
DELETE/api/v1/zones/{id}

Delete Zone

Deletes the zone record. Any webhook subscriptions listening to this zone's boundary events will stop firing. This action cannot be undone.

Why you would use it
Permanently delete a zone.

Submits as `DELETE` — not executable in the try-it explorer.

Returns `204 No Content` on success.

Returns `404` for unknown zone IDs.

Parameters
Validate and normalize these values before you send the request.
idRequiredstring

Zone identifier.

Example Response

204 No Content
GET/api/v1/zones/contains

Zone Contains

Point-in-polygon test using PostGIS ST_Contains. Provide a project ID and a coordinate; the API returns all zones that contain the point. Returns an empty array when the point is outside all zones.

Why you would use it
Test whether a coordinate falls inside any zone.

Returns an empty `zones` array (not `404`) when no zones contain the point.

Uses PostGIS ST_Contains — points exactly on the boundary are included.

Parameters
Validate and normalize these values before you send the request.
projectIdRequiredstring

Project identifier.

latRequirednumber

Latitude (-90 to 90).

lngRequirednumber

Longitude (-180 to 180).

Example Response

{
  "zones": [
    {
      "id": "zone_01HX3K9R",
      "name": "Melbourne CBD"
    }
  ]
}
GET/api/v1/zones/{id}/addresses

Zone Addresses

Returns paginated address records from the GNAF dataset that are spatially contained within the zone polygon. Large zones may contain tens of thousands of addresses; the response is capped at 10 000 per request and includes a `total` count.

Why you would use it
List addresses whose coordinates fall inside a zone.

When `total` exceeds 10 000 the response is truncated — paginate using `offset`.

Returns `404` for unknown zone IDs.

Parameters
Validate and normalize these values before you send the request.
idRequiredstring

Zone identifier.

limitOptionalnumber

Maximum addresses to return. Defaults to 100, capped at 10 000.

offsetOptionalnumber

Pagination offset. Defaults to 0.

Example Response

{
  "addresses": [
    {
      "id": 104233,
      "formattedAddress": "123 Main St, Melbourne VIC 3000, AU",
      "latitude": -37.8136,
      "longitude": 144.9631
    }
  ],
  "total": 3412
}
POST/api/v1/devices/{deviceId}/location

Push Location

Upserts the device's latest position. The server runs a PostGIS point-in-polygon check against all project zones and computes enter/exit events relative to the device's previous position. Any zone crossings trigger configured webhook subscriptions.

Why you would use it
Record a device's current location and detect zone crossings.

Submits as `POST` with a JSON body — not executable in the try-it explorer.

Zone crossings in the response are computed relative to the previous position.

Webhook delivery is asynchronous — crossings are returned synchronously here.

Parameters
Validate and normalize these values before you send the request.
deviceIdRequiredstring

Caller-assigned device identifier (path parameter).

projectIdRequiredstring

Project identifier (request body).

latRequirednumber

Latitude (-90 to 90) (request body).

lngRequirednumber

Longitude (-180 to 180) (request body).

timestampOptionalstring

ISO-8601 timestamp. Defaults to server time.

Example Response

{
  "deviceId": "truck-42",
  "recorded": true,
  "crossings": [
    { "zoneId": "zone_01HX3K9R", "event": "zone.enter" }
  ]
}
GET/api/v1/devices/{deviceId}/zones

Device Zones

Returns the zones that contain the device's most recently recorded position. Useful for real-time dashboards that need to display a device's current zone membership without re-running a PIP query client-side.

Why you would use it
Get the zones a device is currently inside.

Returns `404` when the device has no recorded position.

Returns an empty `zones` array when the device is outside all zones.

Parameters
Validate and normalize these values before you send the request.
deviceIdRequiredstring

Caller-assigned device identifier.

projectIdRequiredstring

Project identifier.

Example Response

{
  "deviceId": "truck-42",
  "zones": [
    {
      "id": "zone_01HX3K9R",
      "name": "Melbourne CBD"
    }
  ]
}
POST/api/v1/webhooks

Create Webhook

Creates a webhook subscription that receives POST requests whenever a device crosses a zone boundary. The response includes a `signingSecret` that is shown exactly once — store it securely. All subsequent deliveries are signed with HMAC-SHA256 using that secret.

Why you would use it
Subscribe to zone boundary crossing events.

Submits as `POST` with a JSON body — not executable in the try-it explorer.

`signingSecret` is returned only once — store it immediately.

The subscription URL must be reachable over HTTPS.

Parameters
Validate and normalize these values before you send the request.
projectIdRequiredstring

Project identifier.

urlRequiredstring

HTTPS endpoint that will receive webhook POST requests.

eventsRequiredstring[]

Event types to subscribe to: `zone.enter`, `zone.exit`, or both.

Example Response

{
  "id": "wh_01HX9AB",
  "projectId": "proj_abc",
  "url": "https://your-app.example.com/webhooks/wherabouts",
  "events": ["zone.enter", "zone.exit"],
  "signingSecret": "whsec_abc123xyz",
  "createdAt": "2026-06-05T00:00:00.000Z"
}
GET/api/v1/webhooks

List Webhooks

Returns all active and failing webhook subscriptions for the project. The `signingSecret` is never included in list responses — it is only returned once at creation time.

Why you would use it
List webhook subscriptions for a project.

Subscriptions marked `failing` have exceeded the 3-retry delivery threshold.

Use `DELETE /api/v1/webhooks/{id}` to remove a failing subscription.

Parameters
Validate and normalize these values before you send the request.
projectIdRequiredstring

Project identifier.

Example Response

{
  "webhooks": [
    {
      "id": "wh_01HX9AB",
      "url": "https://your-app.example.com/webhooks/wherabouts",
      "events": ["zone.enter", "zone.exit"],
      "status": "active",
      "createdAt": "2026-06-05T00:00:00.000Z"
    }
  ]
}
DELETE/api/v1/webhooks/{id}

Delete Webhook

Permanently deletes the webhook subscription. No further deliveries will be attempted. Use this to clean up failing subscriptions or remove subscriptions that are no longer needed.

Why you would use it
Remove a webhook subscription.

Submits as `DELETE` — not executable in the try-it explorer.

Returns `204 No Content` on success.

Returns `404` for unknown webhook IDs.

Parameters
Validate and normalize these values before you send the request.
idRequiredstring

Webhook subscription identifier.

Example Response

204 No Content
GET/api/v1/regions

Classify Coordinate

Classifies a latitude/longitude into the official ABS/ASGS regions that contain it — state, SA1–SA4, LGA, postcode, electoral divisions, and mesh block — keyed by layer. Outside Australia the regions object is empty.

Why you would use it
Return the administrative regions that contain a coordinate.

Outside Australia the `regions` object is empty — not a `404`.

Use the `layers` parameter to limit the response to only the layers you need.

curl example: curl "https://api.wherabouts.com/api/v1/regions?lat=-37.8136&lng=144.9631" -H "Authorization: Bearer YOUR_API_KEY"

Parameters
Validate and normalize these values before you send the request.
latRequirednumber

Latitude of the coordinate to classify (e.g. -37.8136).

lngRequirednumber

Longitude of the coordinate to classify (e.g. 144.9631).

layersOptionalstring

Comma-separated list of region layers to return (e.g. `sa2,lga,poa`). Omit to return all available layers.

Example Response

{
  "query": { "lat": -37.8136, "lng": 144.9631 },
  "regions": {
    "state": { "code": "2", "name": "Victoria" },
    "sa2": { "code": "206041122", "name": "Melbourne" },
    "lga": { "code": "24600", "name": "Melbourne (C)" },
    "poa": { "code": "3000", "name": "3000" }
  }
}

Async lifecycle

Batch geocoding lifecycle

Batch geocoding is a three-step async flow. Submit the job, poll for completion, then fetch results.

1Submit

POST your array of addresses to POST /api/v1/geocode/batch. The server enqueues the job and returns a jobId immediately.

2Poll

Call GET /api/v1/geocode/batch/{jobId} every few seconds until status is completed or failed.

3Fetch results

Once completed, retrieve the full result array from GET /api/v1/geocode/batch/{jobId}/results. Each entry maps to the original input in order.

Batch lifecycle (JavaScript)

// Step 1 — submit
const { jobId } = await fetch("https://api.wherabouts.com/api/v1/geocode/batch", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "wh_live_your_api_key",
  },
  body: JSON.stringify({ addresses: ["123 Main St Melbourne VIC", "456 George St Sydney NSW"] }),
}).then((r) => r.json());

// Step 2 — poll until completed
let status = "pending";
while (status !== "completed" && status !== "failed") {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const poll = await fetch(`https://api.wherabouts.com/api/v1/geocode/batch/${jobId}`, {
    headers: { "X-API-Key": "wh_live_your_api_key" },
  }).then((r) => r.json());
  status = poll.status;
}

// Step 3 — fetch results
const { results } = await fetch(
  `https://api.wherabouts.com/api/v1/geocode/batch/${jobId}/results`,
  { headers: { "X-API-Key": "wh_live_your_api_key" } }
).then((r) => r.json());

Webhook delivery

Webhook delivery and HMAC verification

Wherabouts delivers webhook events by POSTing to your subscription URL when a device crosses a zone boundary. Use the HMAC signature to verify authenticity.

Delivery format

On a zone boundary crossing, Wherabouts sends a signed POST to your subscription URL with a JSON body:

Webhook payload

{
  "event": "zone.enter",
  "zone": { "id": "zone_01HX3K9R", "name": "Melbourne CBD" },
  "device": { "id": "truck-42" },
  "timestamp": "2026-06-05T10:30:00.000Z"
}
Retries and failure

Wherabouts retries failed deliveries up to 3 times with exponential backoff. After 3 consecutive failures the subscription is marked failing and no further deliveries are attempted until the subscription is deleted and recreated.

Return a 2xx response within 5 seconds to acknowledge delivery. Non-2xx or timeouts count as failures.

HMAC-SHA256 signature verification
Every delivery includes an X-Wherabouts-Signature: hmac-sha256=<hex> header. Verify it using the signingSecret shown once at webhook creation time.

Verify HMAC (Node.js)

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string
): boolean {
  const expected = createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");
  const received = signatureHeader.replace("hmac-sha256=", "");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}

// In your Express / TanStack Start handler:
const rawBody = await request.text();
const sig = request.headers.get("x-wherabouts-signature") ?? "";
if (!verifyWebhookSignature(rawBody, sig, process.env.WEBHOOK_SECRET!)) {
  return new Response("Unauthorized", { status: 401 });
}
const payload = JSON.parse(rawBody);

Operational behavior

Errors and constraints

The API keeps failures explicit and predictable. The main categories are authentication failures, request validation failures, and not-found responses for queries that do not resolve to an address.

401 Unauthorized

Returned when the API key is missing, invalid, revoked, or expired. Error responses use a stable envelope: `error.code` plus `error.message`.

400 Validation

Used for short search queries, malformed IDs, missing coordinates, or coordinates outside valid geographic bounds.

404 Not Found

Used when a numeric address ID does not exist or reverse geocoding cannot find an address within 200 meters.

Current request constraints

Autocomplete

`q` must be at least 2 characters. `limit` defaults to 10 and is capped at 20.

Nearby

`radius` defaults to 1000m and is capped at 50000m. `limit` defaults to 10 and is capped at 50.

Reverse

Searches the nearest address within 200m and returns exactly one match.

Coordinates

Latitude must remain inside `-90..90` and longitude inside `-180..180`.

Error Envelope

{
  "error": {
    "code": "bad_request",
    "message": "Query parameter 'q' must be at least 2 characters."
  }
}

Observability

Health checks and timing

Baseline platform observability starts with a public health surface and response timing data. These routes are intended for synthetic checks, deployment verification, and integration smoke tests.

Health endpoint
Use this route for uptime probes and deployment checks.
https://api.wherabouts.com/api/health

The health response includes the published platform SLO targets for latency and uptime, plus a timestamped service status result.

Server-Timing
Core API responses expose request duration directly in the response headers.

Check the `Server-Timing` header to inspect endpoint latency while testing in the explorer, from your own client, or through synthetic monitoring.

This is the baseline measurement layer for the v1 API surface while richer dashboard analytics continue to mature.

Delivery checklist

Integration checklist

If you want the cleanest production implementation, treat the docs as an execution checklist rather than a reference page you skim once.

Recommended flow

Start with autocomplete for user-entered text so the user selects a canonical address rather than submitting free-form input.

Store the returned address `id` and use the by-ID endpoint whenever you need to rehydrate that record later.

Use reverse or nearby only when you already trust the source coordinates or want map-adjacent workflows.

Implementation details

Handle `400` and `401` explicitly in your client layer so forms can recover gracefully without masking validation issues.

Keep API keys on trusted infrastructure or server routes when possible instead of exposing them directly in a public client.

Use the API explorer after sign-in when you want to validate request shapes and inspect responses interactively.

Next steps

Move from docs to implementation

Once your first request works, the fastest next move is to generate a dedicated API key for the environment you are shipping and test the exact endpoint mix you expect to use in production.

Create a key
Generate the key you will use for development or staging.
Inspect requests
Use the protected explorer to verify parameters and outputs.
Track usage
Watch successful request volume once your integration is live.