Lazi
API · v1

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.

bash
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:

read:matches

List + fetch completed and in-progress matches for your org. Includes status, agreed price, pickup + delivery timestamps, counterparty name.

read:transactions

List + fetch payment rows your org touched — paid OR received. Includes gross, fee, GST, payout. Stripe IDs are masked.

read:payouts

Carrier-side subset of transactions. Tailored for bank reconciliation flows.

read:vehicles

Fleet roster — name, registration, equipment type, current status, lazy-since timestamp.

read:loads

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.

json
{
  "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.

json
{
  "error": {
    "type": "permission_error",
    "message": "This key is missing the required scope: read:matches.",
    "code": "missing_scope:read:matches"
  }
}
FieldTypeNotes
401authentication_errorMissing / invalid / revoked / expired bearer. Header WWW-Authenticate: Bearer realm="lazi" is also returned.
403permission_errorAuthenticated but the key lacks the required scope.
400invalid_requestBad cursor, bad date range, or malformed query.
404not_foundThe resource does not exist OR it does but is owned by another org.
429rate_limitToo many requests. Honour the Retry-After header.
500internalServer 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.

GET

/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

json
{
  "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
  }
}
GET

/v1/matches

read:matches

List the org's matches, newest first.

Query

FieldTypeNotes
limitinteger (1..100)Defaults to 25.
cursorstringOpaque pagination cursor from a prior page.
statusstringFilter by match status. Optional.

Response

json
{
  "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
}
GET

/v1/matches/{id}

read:matches

Fetch a single match by id, including pickup + delivery GPS.

Response

json
{
  "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"
}
GET

/v1/transactions

read:transactions

List payment rows — money paid OR received. Stripe IDs are masked to the last 8 characters; the canonical reference stays internal.

Query

FieldTypeNotes
statusstringFilter on status (e.g. completed, refunded). Optional.
GET

/v1/payouts

read:payouts

Carrier-side subset of /transactions, tailored for bank reconciliation.

GET

/v1/vehicles

read:vehicles

Fleet roster — name, registration, equipment type, current status, lazy-since timestamp.

GET

/v1/loads

read:loads

Load 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

http
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):

js
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

json
{
  "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.