Lazi · API
Read your data + receive signed events
The Lazi public API is read-only over HTTPS, plus outbound webhooks that POST signed event envelopes when something changes in your org. Lazi stays the system of record — integrations pull what they need + react to pushes, but never write back.
Five endpoints today. Cursor-paginated. Stripe-compatible JSON envelope. UTF-8. NZD amounts in integer cents. Timestamps in ISO-8601.
Base URL
https://lazi.co.nz/api/v1
Auth scheme
Authorization: Bearer lazi_sk_live_…
01
Authentication
Every request carries a per-org API key in the Authorization header using the Bearer scheme. Keys are minted by owners + admins from /dashboard/settings/api.
curl https://lazi.co.nz/api/v1/matches \
-H "Authorization: Bearer lazi_sk_live_abcdef0123456789..."Each key carries a set of scopes — endpoints require specific scopes. The full scope vocabulary today:
List + fetch completed and in-progress matches for your org. Includes status, agreed price, pickup + delivery timestamps, counterparty name.
List + fetch payment rows your org touched — paid OR received. Includes gross, fee, GST, payout. Stripe IDs are masked.
Carrier-side subset of transactions. Tailored for bank reconciliation flows.
Fleet roster — name, registration, equipment type, current status, lazy-since timestamp.
Load posts your org created. Includes status, route, equipment, weight, price.
Tokens are stored as a SHA-256 hash on Lazi's side. If you lose the plaintext, revoke the key + mint a new one — there's no recovery flow by design.
02
Pagination
List endpoints are cursor-paginated. Pass ?limit= (1–100, default 25) and the opaque next_cursor returned in the previous page. Treat the cursor as a black box — its shape may change.
{
"object": "list",
"data": [/* … rows … */],
"has_more": true,
"next_cursor": "eyJ0cyI6IjIwMjYtMDUtMjhUMTQ6MzI6MDkuMTIzWiIs..."
}03
Errors
Errors return a single typed object. Status codes follow HTTP conventions; the error.type + error.code fields are the programmatic handle.
{
"error": {
"type": "permission_error",
"message": "This key is missing the required scope: read:matches.",
"code": "missing_scope:read:matches"
}
}| Field | Type | Notes |
|---|---|---|
| 401 | authentication_error | Missing / invalid / revoked / expired bearer. Header WWW-Authenticate: Bearer realm="lazi" is also returned. |
| 403 | permission_error | Authenticated but the key lacks the required scope. |
| 400 | invalid_request | Bad cursor, bad date range, or malformed query. |
| 404 | not_found | The resource does not exist OR it does but is owned by another org. |
| 429 | rate_limit | Too many requests. Honour the Retry-After header. |
| 500 | internal | Server error. Retry with backoff. |
04
Endpoints
All endpoints are scoped to the org behind your API key. You can't read another org's rows even if you guess a valid UUID — 404 is returned in that case.
/v1/me
(any)Self-check. Returns the authenticated org + the scopes granted to your key. Succeeds with any valid, non-revoked, non-expired key. Useful for verifying setup.
Response
{
"object": "me",
"organization": {
"id": "1c9a8d5f-...",
"name": "Kowhai Cartage",
"city": "Palmerston North",
"region": "Manawatu-Wanganui"
},
"scopes": ["read:matches", "read:transactions"],
"abilities": {
"read:matches": true,
"read:transactions": true,
"read:payouts": false,
"read:vehicles": false,
"read:loads": false
}
}/v1/matches
read:matchesList the org's matches, newest first.
Query
| Field | Type | Notes |
|---|---|---|
| limit | integer (1..100) | Defaults to 25. |
| cursor | string | Opaque pagination cursor from a prior page. |
| status | string | Filter by match status. Optional. |
Response
{
"object": "list",
"data": [
{
"object": "match",
"id": "f48e3a2c-...",
"status": "completed",
"role": "carrier",
"counterparty_organization_id": "1c9a8d5f-...",
"load_post_id": "fb2c1a01-...",
"empty_post_id": "ef9c8b21-...",
"agreed_price_cents": 28000,
"pickup_at": "2026-05-28T09:14:00Z",
"delivered_at": "2026-05-28T11:42:00Z",
"created_at": "2026-05-27T18:00:00Z",
"updated_at": "2026-05-28T11:42:00Z"
}
],
"has_more": false,
"next_cursor": null
}/v1/matches/{id}
read:matchesFetch a single match by id, including pickup + delivery GPS.
Response
{
"object": "match",
"id": "f48e3a2c-...",
"status": "completed",
"role": "carrier",
"counterparty_organization_id": "1c9a8d5f-...",
"load_post_id": "fb2c1a01-...",
"empty_post_id": "ef9c8b21-...",
"agreed_price_cents": 28000,
"pickup": {
"at": "2026-05-28T09:14:00Z",
"lat": -40.22150,
"lng": 175.56522
},
"delivery": {
"at": "2026-05-28T11:42:00Z",
"lat": -40.35633,
"lng": 175.61108
},
"created_at": "2026-05-27T18:00:00Z",
"updated_at": "2026-05-28T11:42:00Z"
}/v1/transactions
read:transactionsList payment rows — money paid OR received. Stripe IDs are masked to the last 8 characters; the canonical reference stays internal.
Query
| Field | Type | Notes |
|---|---|---|
| status | string | Filter on status (e.g. completed, refunded). Optional. |
/v1/payouts
read:payoutsCarrier-side subset of /transactions, tailored for bank reconciliation.
/v1/vehicles
read:vehiclesFleet roster — name, registration, equipment type, current status, lazy-since timestamp.
/v1/loads
read:loadsLoad posts your org has created. Includes status, route, equipment, weight, price.
05
Webhooks
Subscribe an HTTPS endpoint to one or more event types from /dashboard/settings/webhooks. We POST a JSON envelope with a signed header. Non-2xx responses are retried with exponential backoff (1m / 5m / 30m / 2h / 6h, max 6 attempts).
Signing header
Lazi-Signature: t=1748467929,v1=ec0f63f9...Concatenate the t= value, a period, and the raw request body. HMAC-SHA256 with your stored secret. Constant- time compare against the v1= value. Reject timestamps older than 5 minutes — that's your replay window.
Reference verification (Node):
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyLaziSignature(args) {
const { header, body, secret, toleranceSeconds = 300 } = args
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=')),
)
const t = Number.parseInt(parts.t, 10)
if (!Number.isFinite(t)) return false
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false
const expected = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex')
if (expected.length !== parts.v1.length) return false
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(parts.v1, 'hex'))
}Event envelope
{
"id": "1e3c7a92-...",
"type": "payment.succeeded",
"created": "2026-05-28T14:32:09.123Z",
"data": {
"transaction_id": "8f9e2d1c-...",
"match_id": "f48e3a2c-...",
"gross_amount_cents": 30466,
"platform_fee_cents": 2240,
"carrier_payout_cents": 27160,
"gst_cents": 3652,
"paid_at": "2026-05-28T14:32:09Z"
}
}Available events
match.completed
A match has been delivered + confirmed by both parties. Use this to mirror the wrapped job into your accounting flow.
payment.succeeded
A Stripe payment has cleared. Includes gross / fee / payout amounts in cents.
payout.sent
Carrier payout dispatched to the receiving bank.
vehicle.went_lazy
A truck flipped to 'lazy' — handy for fleet-management software that wants to mirror availability.
load.posted
A new load post was created by your org. Useful for piping into a separate ops tool.
The id field is stable across retry attempts — dedupe on it server-side to handle the (rare) case where you receive the same event twice.
·
Help
Something broken? Email help@lazi.co.nz — include the request id from the response headers (when available) and a curl example we can reproduce against.
Need a write-direction API (post loads, mark deliveries) for a specific integration? Contact us — we'll build a webhook- receiver path tailored to your system rather than open inbound writes, per Lazi's system-of-record stance.