PROTOTYPE - fake data only, do not enter real information

ymorder integration API v1.0.0

Machine API for accounting, TMS / GPS and spreadsheet systems. Authenticate with a scoped API key created by a planner at /integrations: send `Authorization: Bearer ymk_<id>_<secret>` or `X-API-Key: ymk_<id>_<secret>`. Keys are shown once and stored hashed. 300 requests/min/key. Outgoing events are delivered as signed webhooks (see x-webhooks) or can be pulled from GET /events. Accounting column layouts are NOT yet verified against Yoongmei's real templates and tax codes are blank until configured (wiki/integrations.md).

Machine-readable: /api/v1/openapi.json (OpenAPI 3.1).

Authentication

Authorization: Bearer ymk_<id>_<secret>
# or
X-API-Key: ymk_<id>_<secret>
ScopeAllows
orders:readread orders and the event feed
orders:writecreate / update orders
billing:readpreview and download invoice exports, read payment status
billing:writemark batches exported / unmarked, import payments
tms:readread lorry positions and trips
tms:writepush GPS positions / trip events from a tracking system
master:readread customers, lorries, drivers, charges
master:writeimport / update customers, debtor codes, lorries
webhooks:managemanage own webhook subscriptions

Meta

GET /api/v1/openapi.json

This OpenAPI 3.1 document

Public: no key needed

StatusMeaning
200the spec

GET /api/v1/docs

Human-readable rendering of this spec (HTML)

Public: no key needed

StatusMeaning
200HTML page

Core

GET /api/v1/ping

Check a key: returns its id, name, scopes and the server time

Any valid API key

StatusMeaning
200ok
401missing, malformed, unknown or revoked API key
429rate limit (300 requests/min/key) or too many bad credentials from this IP

Events

GET /api/v1/events

Pull feed of integration events (for systems that cannot receive webhooks)

Scope (any of): orders:read

Ordered by id. Keep the largest returned id and pass it back as since_id. Same event types as the webhooks.

ParameterInTypeNotes
since_idqueryintegerreturn events with id greater than this
limitqueryintegerdefault 100, max 500
typequerystringexact event type, e.g. order.assigned
StatusMeaning
200{events:[{id,type,created_at,tempdo_no,data}], next_since_id}
401missing, malformed, unknown or revoked API key
403key lacks scope: orders:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

Orders

GET /api/v1/orders

List orders (whitelisted fields only)

Scope (any of): orders:read

ParameterInTypeNotes
statusquerystringexact order status
updated_sincequerystringISO timestamp
offsetqueryintegerdefault 0
limitqueryintegerdefault 100, max 500
StatusMeaning
200{total, offset, orders:[...]}
401missing, malformed, unknown or revoked API key
403key lacks scope: orders:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/orders/{no}

One order

Scope (any of): orders:read

ParameterInTypeNotes
no *pathstringTemp DO number, e.g. TEMPDO-0001
StatusMeaning
200the order
401missing, malformed, unknown or revoked API key
403key lacks scope: orders:read
404no such Temp DO
429rate limit (300 requests/min/key) or too many bad credentials from this IP

Webhooks

GET /api/v1/webhooks

List the webhook subscriptions THIS key created (never other keys' or the planner's)

Scope (any of): webhooks:manage

StatusMeaning
200{webhooks:[...]} (secret hint only)
401missing, malformed, unknown or revoked API key
403key lacks scope: webhooks:manage
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/webhooks

Subscribe a URL to events; the signing secret is returned ONCE

Scope (any of): webhooks:manage

The URL must be https (or http) to a public host; private / loopback / link-local addresses are refused (SSRF guard) and re-checked at delivery time.

Body (JSON, required)

{
  "name": "Accounting bridge",
  "url": "https://example.com/hooks/ymorder",
  "events": [
    "invoice.exported",
    "order.status_changed"
  ]
}
StatusMeaning
201subscription + secret ("whsec_...")
400bad name / url / event type
401missing, malformed, unknown or revoked API key
403key lacks scope: webhooks:manage
429rate limit (300 requests/min/key) or too many bad credentials from this IP

DELETE /api/v1/webhooks/{id}

Delete a subscription this key created

Scope (any of): webhooks:manage

ParameterInTypeNotes
id *pathstringnumeric id (webhooks) or batch id such as ACB-202610121530
StatusMeaning
200{ok:true}
401missing, malformed, unknown or revoked API key
403key lacks scope: webhooks:manage
404not found (or not yours)
429rate limit (300 requests/min/key) or too many bad credentials from this IP

Billing

GET /api/v1/billing/profiles

Accounting layouts available (columns, formats, unverified flag)

Scope (any of): billing:read

StatusMeaning
200{profiles:[...]}
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/billing/invoices

PREVIEW an invoice export. Read-only: never marks anything exported

Scope (any of): billing:read

ParameterInTypeNotes
profilequeryautocount | sqlaccount | xero | quickbooks | generic | json
formatquerycsv | xlsx | json
groupqueryorder | customer
include_draftquerybooleanalso export orders whose rate is still a draft
fromquerystringdelivered on/after, YYYY-MM-DD (Malaysia time)
toquerystringdelivered on/before, YYYY-MM-DD
tempdo_nosquerystringcomma separated Temp DO numbers (up to 500) to restrict the export
StatusMeaning
200The export file (text/csv or xlsx attachment) or, for format=json, the canonical JSON. Headers: X-Billing-Skipped.
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/billing/batches

Build an export AND mark its orders exported (never exported twice); returns the file

Scope (any of): billing:write

Only Delivered orders that classify as ready are exported. Response headers X-Billing-Batch (batch id) and X-Billing-Skipped. Emits invoice.exported (one per order from the audit bridge, plus one batch-level event carrying batch_id).

Body (JSON)

{
  "profile": "xero",
  "format": "csv",
  "group": "customer"
}
StatusMeaning
200file or JSON, with X-Billing-Batch
400bad parameter
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:write
422nothing billable (lists skipped orders and why)
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/billing/batches/{id}/ack

Store the accounting system's own document number per DO

Scope (any of): billing:write

Idempotent: the same number again = "unchanged". A different number than the one already stored is refused.

ParameterInTypeNotes
id *pathstringnumeric id (webhooks) or batch id such as ACB-202610121530

Body (JSON, required)

{
  "documents": [
    {
      "tempdo_no": "TEMPDO-0001",
      "external_doc_no": "IV-000123"
    }
  ]
}
StatusMeaning
200{batch_id, acked, rejected, results[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:write
404no exported orders in that batch
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/billing/batches/{id}/unmark

Re-open a batch the accounting system rejected (so its DOs can be exported again)

Scope (any of): billing:write

DOs that already have a payment recorded are skipped and listed under "blocked". Emits invoice.unmarked per order.

ParameterInTypeNotes
id *pathstringnumeric id (webhooks) or batch id such as ACB-202610121530

Body (JSON)

{
  "type": "object",
  "properties": {
    "tempdo_nos": {
      "type": "array",
      "items": {
        "type": "string",
        "description": "Temp DO no"
      },
      "description": "only these (default: whole batch)"
    }
  }
}
StatusMeaning
200{ok, batch_id, cleared[], blocked[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:write
404no exported orders in that batch
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/billing/payments

Import payments received (send dry_run:true first). Idempotent on (order, reference)

Scope (any of): billing:write

A payment can only be applied to a DO that was exported. Same reference + same amount = "duplicate", same reference + different amount = "conflict". external_doc_no works only when it maps to exactly one DO; otherwise send tempdo_no.

Body (JSON, required)

{
  "dry_run": true,
  "payments": [
    {
      "tempdo_no": "TEMPDO-0001",
      "reference": "BANK-8841",
      "amount": 1200.5,
      "paid_at": "2026-10-10"
    }
  ]
}
StatusMeaning
200{dry_run, summary:{rows,applied,would_apply,duplicate,conflict,rejected}, results[]}
400bad payload
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:write
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/billing/payments

Exported DOs with their payment status

Scope (any of): billing:read

ParameterInTypeNotes
statusquerypaid | partial | overpaid | unpaid
limitqueryintegerdefault 100, max 500
offsetqueryintegerdefault 0
StatusMeaning
200{total, offset, payments[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: billing:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GPS / TMS

POST /api/v1/tms/positions

PUSH GPS positions (one object, an array, {positions:[]}, a vendor payload via the field map, or Traccar forwarding)

Scope (any of): tms:write

Batch max 500 per request. Only plates that already exist are accepted. (plate, ts) is the dedupe key. HTTP 200 if anything was accepted or was a duplicate, 422 if every record was rejected, 413 over 500, 400 for an empty/malformed body. Per-record reasons are in rejected[].

ParameterInTypeNotes
formatquerytraccar | canonical
mapquerystringJSON field map for this request only, merged over the key's stored map
tz_offset_minutesqueryintegerminutes east of UTC for timestamps without a zone; default 0

Body (JSON, required)

{
  "positions": [
    {
      "plate": "JKL 9999",
      "lat": 1.4927,
      "lng": 103.7414,
      "speed_kmh": 62,
      "heading_deg": 90,
      "ts": "2026-10-12T08:00:00Z"
    }
  ]
}
StatusMeaning
200{ok, accepted_count, rejected_count, duplicate_count, accepted[], rejected[]}
400bad body
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:write
413too many records
422all rejected
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/tms/positions

Latest position per lorry from the configured TMS provider

Scope (any of): tms:read

ParameterInTypeNotes
sourcequerypushraw stored pushes only
StatusMeaning
200{ok, provider, demo, as_of, last_push_at, positions[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP
502provider error

GET /api/v1/tms/osmand

OsmAnd / Traccar-client style position push by query string (key may be sent as ?key= on THIS route only)

Scope (any of): tms:write

ParameterInTypeNotes
id *querystringplate
latquerynumber
lonquerynumber
timestampquerystringepoch s/ms or ISO
speedquerynumberKNOTS (converted x1.852)
bearingquerynumber
ignitionquerystring
keyquerystringAPI key, only on this route
tz_offset_minutesqueryinteger
StatusMeaning
200{ok, plate, ts} (a duplicate also answers 200)
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:write
404unknown plate
422rejected, with reason
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/tms/events

Trip event from the tracking system: arrived / departed / geofence

Scope (any of): tms:write

Audited and emitted as order.checkpoint when it resolves to an order. It does NOT change order status (see wiki/integrations.md, TMS_AUTO_STATUS).

Body (JSON, required)

{
  "type": "object",
  "properties": {
    "plate": {
      "type": "string",
      "description": ""
    },
    "type": {
      "type": "string",
      "enum": [
        "arrived",
        "departed",
        "geofence"
      ]
    },
    "ts": {
      "type": "string",
      "description": "ISO 8601"
    },
    "tempdo_no": {
      "type": "string",
      "description": "optional; must be assigned to that plate"
    },
    "note": {
      "type": "string",
      "description": "max 300 chars"
    }
  },
  "required": [
    "plate",
    "type",
    "ts"
  ]
}
StatusMeaning
200{ok, plate, type, ts, tempdo_no, duplicate, emitted, auto_status}
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:write
404order not found
422unknown plate / order not on that plate / ts in the future
429rate limit (300 requests/min/key) or too many bad credentials from this IP

PUT /api/v1/tms/field-map

Store THIS key's vendor-field -> canonical-field map (dotted paths)

Scope (any of): tms:write

Body (JSON, required)

{
  "map": {
    "plate": "vehicleNo",
    "lat": "gps.latitude",
    "lng": "gps.longitude",
    "ts": "gpsTime"
  }
}
StatusMeaning
200{ok, key_id, map}
400unknown field / bad path
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:write
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/tms/field-map

Read this key's field map

Scope (any of): tms:read tms:write

StatusMeaning
200{key_id, map, canonical_fields}
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:read or tms:write
429rate limit (300 requests/min/key) or too many bad credentials from this IP

DELETE /api/v1/tms/field-map

Delete this key's field map

Scope (any of): tms:write

StatusMeaning
200{ok:true}
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:write
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/tms/lorries/{plate}/track

Track points for one lorry

Scope (any of): tms:read

ParameterInTypeNotes
plate *pathstringlorry plate; case, spaces and punctuation are ignored (JKL9999 = JKL 9999)
fromquerystringISO date
toquerystringISO date
StatusMeaning
200{ok, plate, provider, count, track[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:read
404unknown plate
429rate limit (300 requests/min/key) or too many bad credentials from this IP
502provider error

GET /api/v1/tms/jobs

Active assigned trips for the TMS to poll (plate, driver, from/to with coordinates, status)

Scope (any of): tms:read

ParameterInTypeNotes
statusquerystringcomma list; default Approved,Picked-Up,Departed,In-Transit
platequerystring
StatusMeaning
200{as_of, count, jobs[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: tms:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

Import / Export

GET /api/v1/datasets

Datasets, columns, aliases, limits

Scope (any of): master:read orders:read

StatusMeaning
200{datasets[], limits, formats, modes, dry_run_default}
401missing, malformed, unknown or revoked API key
403key lacks scope: master:read or orders:read
429rate limit (300 requests/min/key) or too many bad credentials from this IP

GET /api/v1/export/{dataset}

Export a dataset as csv / xlsx / json

Scope (any of): orders:read master:read

The orders dataset needs orders:read; customers, lorries, drivers and charges need master:read.

ParameterInTypeNotes
dataset *pathorders | customers | lorries | drivers | chargesdataset name
formatquerycsv | xlsx | json
statusquerystringonly datasets with a status column
fromquerystringYYYY-MM-DD, datasets with a date column
toquerystringYYYY-MM-DD
StatusMeaning
200file or JSON
401missing, malformed, unknown or revoked API key
403key lacks scope: orders:read or master:read
404unknown dataset
429rate limit (300 requests/min/key) or too many bad credentials from this IP

POST /api/v1/import/{dataset}

Import customers or lorries. Dry run unless dry_run is exactly false; atomic unless partial

Scope (any of): master:write

Body is JSON {format, content, dry_run, mode, partial, strict} (JSON is capped at 1 MB by the server) OR a RAW body (Content-Type text/csv for CSV, application/octet-stream for .xlsx bytes) with the options as query parameters, up to 5 MB. Max 5000 rows. Blank cells never clear a field.

ParameterInTypeNotes
dataset *pathorders | customers | lorries | drivers | chargesdataset name
formatquerycsv | xlsx | json
dry_runquerystringanything but "false" is a dry run
modequeryupsert | create_only | update_only
partialquerystring"true" = skip bad rows and write the rest
strictquerystring"true" = unknown columns are an error

Body (JSON, required)

{
  "type": "object",
  "properties": {
    "format": {
      "type": "string",
      "enum": [
        "csv",
        "xlsx",
        "json"
      ]
    },
    "content": {
      "type": "string",
      "description": "CSV text, base64 for xlsx, or JSON rows"
    },
    "dry_run": {
      "type": "boolean",
      "description": "default true"
    },
    "mode": {
      "type": "string",
      "enum": [
        "upsert",
        "create_only",
        "update_only"
      ]
    },
    "partial": {
      "type": "boolean",
      "description": ""
    },
    "strict": {
      "type": "boolean",
      "description": ""
    }
  },
  "required": [
    "content"
  ]
}
StatusMeaning
200{dataset, mode, dry_run, applied, summary, unknown_columns, ignored_columns, rows[]}
401missing, malformed, unknown or revoked API key
403key lacks scope: master:write
405export-only dataset (orders, drivers, charges)
413too big
422row errors and not partial: nothing written
429rate limit (300 requests/min/key) or too many bad credentials from this IP

Webhooks

Subscribe with POST /webhooks (or a planner at /integrations). Each event is POSTed to your URL as JSON.

{
  "headers": {
    "X-Ymorder-Event": "event type",
    "X-Ymorder-Delivery": "delivery id",
    "X-Ymorder-Timestamp": "unix seconds",
    "X-Ymorder-Signature": "v1=<hex HMAC-SHA256(secret, timestamp + \".\" + rawBody)>"
  },
  "body": {
    "id": "evt_<n> (stable across retries: dedupe on it)",
    "type": "event type",
    "created_at": "ISO time",
    "data": "event payload (order events carry an order snapshot)"
  }
}

Signature: v1=<lowercase hex> where the digest is HMAC-SHA256 of `${X-Ymorder-Timestamp}.${raw request body}` with your subscription secret. Reject timestamps older than 300 seconds. Success = any 2xx within 8 s; retries after 1, 5, 30, 120, 720 minutes, then dead after 6 attempts. Dedupe on the envelope id.

EventMeaning
order.createda new order exists (WhatsApp, portal, planner or debtor API)
order.status_changedstatus moved (Approved, Picked-Up, Departed, In-Transit, Delivered, Rejected ...)
order.assigneda lorry / driver was assigned to an order (carries plate + driver: the TMS dispatch trigger)
order.updatedplanner or customer edited an order
order.checkpointdriver reported a checkpoint / POD
invoice.exportedorders were exported to an accounting batch (batch id + totals)
invoice.unmarkedan exported batch was reopened
payment.importedpayments were imported / reconciled from the accounting system
vehicle.positiona GPS position was received (high volume: subscribe on purpose)
vehicle.near_destinationan in-transit lorry came within 5 km of its destination
webhook.testmanual test ping from the admin page