{"openapi":"3.1.0","info":{"title":"Yoongmei ordering (ymorder) integration API","version":"1.0.0","description":"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)."},"servers":[{"url":"/api/v1","description":"this server"}],"tags":[{"name":"Meta"},{"name":"Core"},{"name":"Events"},{"name":"Orders"},{"name":"Webhooks"},{"name":"Billing"},{"name":"GPS / TMS"},{"name":"Import / Export"}],"paths":{"/openapi.json":{"get":{"tags":["Meta"],"summary":"This OpenAPI 3.1 document","operationId":"get_openapi_json","security":[],"x-required-scopes":[],"x-scope-mode":"any-of","responses":{"200":{"description":"the spec"}}}},"/docs":{"get":{"tags":["Meta"],"summary":"Human-readable rendering of this spec (HTML)","operationId":"get_docs","security":[],"x-required-scopes":[],"x-scope-mode":"any-of","responses":{"200":{"description":"HTML page"}}}},"/ping":{"get":{"tags":["Core"],"summary":"Check a key: returns its id, name, scopes and the server time","operationId":"get_ping","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":[],"x-scope-mode":"any-of","responses":{"200":{"description":"ok","content":{"application/json":{"example":{"ok":true,"key":{"id":3,"name":"AutoCount sync","scopes":["orders:read"]},"server_time":"2026-10-12T08:00:00.000Z","version":"v1"}}}},"401":{"description":"missing, malformed, unknown or revoked API key"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/events":{"get":{"tags":["Events"],"summary":"Pull feed of integration events (for systems that cannot receive webhooks)","operationId":"get_events","description":"Ordered by id. Keep the largest returned id and pass it back as since_id. Same event types as the webhooks.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["orders:read"],"x-scope-mode":"any-of","parameters":[{"name":"since_id","in":"query","required":false,"schema":{"type":"integer","description":"return events with id greater than this","default":0}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"default 100, max 500"}},{"name":"type","in":"query","required":false,"schema":{"type":"string","description":"exact event type, e.g. order.assigned"}}],"responses":{"200":{"description":"{events:[{id,type,created_at,tempdo_no,data}], next_since_id}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: orders:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/orders":{"get":{"tags":["Orders"],"summary":"List orders (whitelisted fields only)","operationId":"get_orders","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["orders:read"],"x-scope-mode":"any-of","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","description":"exact order status"}},{"name":"updated_since","in":"query","required":false,"schema":{"type":"string","description":"ISO timestamp"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","description":"default 0"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"default 100, max 500"}}],"responses":{"200":{"description":"{total, offset, orders:[...]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: orders:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/orders/{no}":{"get":{"tags":["Orders"],"summary":"One order","operationId":"get_orders_no","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["orders:read"],"x-scope-mode":"any-of","parameters":[{"name":"no","in":"path","required":true,"description":"Temp DO number, e.g. TEMPDO-0001","schema":{"type":"string"}}],"responses":{"200":{"description":"the order"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: orders:read"},"404":{"description":"no such Temp DO"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/webhooks":{"get":{"tags":["Webhooks"],"summary":"List the webhook subscriptions THIS key created (never other keys' or the planner's)","operationId":"get_webhooks","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["webhooks:manage"],"x-scope-mode":"any-of","responses":{"200":{"description":"{webhooks:[...]} (secret hint only)"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: webhooks:manage"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}},"post":{"tags":["Webhooks"],"summary":"Subscribe a URL to events; the signing secret is returned ONCE","operationId":"post_webhooks","description":"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.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["webhooks:manage"],"x-scope-mode":"any-of","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"1-80 chars"},"url":{"type":"string","description":"receiver URL"},"events":{"type":"array","items":{"type":"string","description":"event type, \"order.*\" or \"*\""},"description":"default [\"*\"]"}},"required":["name","url"]},"example":{"name":"Accounting bridge","url":"https://example.com/hooks/ymorder","events":["invoice.exported","order.status_changed"]}}}},"responses":{"201":{"description":"subscription + secret (\"whsec_...\")"},"400":{"description":"bad name / url / event type"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: webhooks:manage"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/webhooks/{id}":{"delete":{"tags":["Webhooks"],"summary":"Delete a subscription this key created","operationId":"delete_webhooks_id","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["webhooks:manage"],"x-scope-mode":"any-of","parameters":[{"name":"id","in":"path","required":true,"description":"numeric id (webhooks) or batch id such as ACB-202610121530","schema":{"type":"string"}}],"responses":{"200":{"description":"{ok:true}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: webhooks:manage"},"404":{"description":"not found (or not yours)"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/billing/profiles":{"get":{"tags":["Billing"],"summary":"Accounting layouts available (columns, formats, unverified flag)","operationId":"get_billing_profiles","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:read"],"x-scope-mode":"any-of","responses":{"200":{"description":"{profiles:[...]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/billing/invoices":{"get":{"tags":["Billing"],"summary":"PREVIEW an invoice export. Read-only: never marks anything exported","operationId":"get_billing_invoices","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:read"],"x-scope-mode":"any-of","parameters":[{"name":"profile","in":"query","required":false,"schema":{"type":"string","enum":["autocount","sqlaccount","xero","quickbooks","generic","json"],"default":"autocount"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["csv","xlsx","json"]}},{"name":"group","in":"query","required":false,"schema":{"type":"string","enum":["order","customer"],"default":"order"}},{"name":"include_draft","in":"query","required":false,"schema":{"type":"boolean","description":"also export orders whose rate is still a draft"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","description":"delivered on/after, YYYY-MM-DD (Malaysia time)"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","description":"delivered on/before, YYYY-MM-DD"}},{"name":"tempdo_nos","in":"query","required":false,"schema":{"type":"string","description":"comma separated Temp DO numbers (up to 500) to restrict the export"}}],"responses":{"200":{"description":"The export file (text/csv or xlsx attachment) or, for format=json, the canonical JSON. Headers: X-Billing-Skipped."},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/billing/batches":{"post":{"tags":["Billing"],"summary":"Build an export AND mark its orders exported (never exported twice); returns the file","operationId":"post_billing_batches","description":"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).","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:write"],"x-scope-mode":"any-of","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"profile":{"type":"string","enum":["autocount","sqlaccount","xero","quickbooks","generic","json"]},"format":{"type":"string","enum":["csv","xlsx","json"]},"group":{"type":"string","enum":["order","customer"]},"include_draft":{"type":"boolean","description":"include draft rates"},"from":{"type":"string","description":"YYYY-MM-DD"},"to":{"type":"string","description":"YYYY-MM-DD"},"tempdo_nos":{"type":"array","items":{"type":"string","description":"Temp DO no"},"description":"max 500 per request"}}},"example":{"profile":"xero","format":"csv","group":"customer"}}}},"responses":{"200":{"description":"file or JSON, with X-Billing-Batch"},"400":{"description":"bad parameter"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:write"},"422":{"description":"nothing billable (lists skipped orders and why)"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/billing/batches/{id}/ack":{"post":{"tags":["Billing"],"summary":"Store the accounting system's own document number per DO","operationId":"post_billing_batches_id_ack","description":"Idempotent: the same number again = \"unchanged\". A different number than the one already stored is refused.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:write"],"x-scope-mode":"any-of","parameters":[{"name":"id","in":"path","required":true,"description":"numeric id (webhooks) or batch id such as ACB-202610121530","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"type":"object","properties":{"tempdo_no":{"type":"string","description":""},"external_doc_no":{"type":"string","description":"1-60 chars"}},"required":["tempdo_no","external_doc_no"]},"description":"1-1000 entries"}},"required":["documents"]},"example":{"documents":[{"tempdo_no":"TEMPDO-0001","external_doc_no":"IV-000123"}]}}}},"responses":{"200":{"description":"{batch_id, acked, rejected, results[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:write"},"404":{"description":"no exported orders in that batch"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/billing/batches/{id}/unmark":{"post":{"tags":["Billing"],"summary":"Re-open a batch the accounting system rejected (so its DOs can be exported again)","operationId":"post_billing_batches_id_unmark","description":"DOs that already have a payment recorded are skipped and listed under \"blocked\". Emits invoice.unmarked per order.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:write"],"x-scope-mode":"any-of","parameters":[{"name":"id","in":"path","required":true,"description":"numeric id (webhooks) or batch id such as ACB-202610121530","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"tempdo_nos":{"type":"array","items":{"type":"string","description":"Temp DO no"},"description":"only these (default: whole batch)"}}}}}},"responses":{"200":{"description":"{ok, batch_id, cleared[], blocked[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:write"},"404":{"description":"no exported orders in that batch"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/billing/payments":{"post":{"tags":["Billing"],"summary":"Import payments received (send dry_run:true first). Idempotent on (order, reference)","operationId":"post_billing_payments","description":"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.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:write"],"x-scope-mode":"any-of","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dry_run":{"type":"boolean","description":"default false on this route"},"payments":{"type":"array","items":{"type":"object","properties":{"tempdo_no":{"type":"string","description":"or external_doc_no"},"external_doc_no":{"type":"string","description":""},"reference":{"type":"string","description":"1-80 chars; the idempotency key"},"amount":{"type":"number","description":"MYR, > 0"},"paid_at":{"type":"string","description":"YYYY-MM-DD, DD/MM/YYYY or ISO"}},"required":["reference","amount","paid_at"]},"description":"1-500 rows"}},"required":["payments"]},"example":{"dry_run":true,"payments":[{"tempdo_no":"TEMPDO-0001","reference":"BANK-8841","amount":1200.5,"paid_at":"2026-10-10"}]}}}},"responses":{"200":{"description":"{dry_run, summary:{rows,applied,would_apply,duplicate,conflict,rejected}, results[]}"},"400":{"description":"bad payload"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:write"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}},"get":{"tags":["Billing"],"summary":"Exported DOs with their payment status","operationId":"get_billing_payments","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["billing:read"],"x-scope-mode":"any-of","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["paid","partial","overpaid","unpaid"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","description":"default 100, max 500"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","description":"default 0"}}],"responses":{"200":{"description":"{total, offset, payments[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: billing:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/tms/positions":{"post":{"tags":["GPS / TMS"],"summary":"PUSH GPS positions (one object, an array, {positions:[]}, a vendor payload via the field map, or Traccar forwarding)","operationId":"post_tms_positions","description":"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[].","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:write"],"x-scope-mode":"any-of","parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["traccar","canonical"]}},{"name":"map","in":"query","required":false,"schema":{"type":"string","description":"JSON field map for this request only, merged over the key's stored map"}},{"name":"tz_offset_minutes","in":"query","required":false,"schema":{"type":"integer","description":"minutes east of UTC for timestamps without a zone; default 0"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"plate":{"type":"string","description":"must already exist in the lorries table (never created)"},"lat":{"type":"number","description":"-90..90; 0,0 is rejected"},"lng":{"type":"number","description":"-180..180"},"ts":{"type":"string","description":"ISO 8601, epoch seconds or epoch ms. Naive timestamps (no zone) are read as UTC unless ?tz_offset_minutes= is given. More than 5 min in the future or older than 30 days is rejected"},"speed_kmh":{"type":"number","description":"0..400"},"heading_deg":{"type":"number","description":"0..360"},"ignition":{"description":"true/false, on/off, 1/0"},"odometer_km":{"type":"number","description":"km"},"location_text":{"type":"string","description":"free text, cut at 200 characters"}},"required":["plate","lat","lng","ts"]},{"type":"array","items":{"type":"object","properties":{"plate":{"type":"string","description":"must already exist in the lorries table (never created)"},"lat":{"type":"number","description":"-90..90; 0,0 is rejected"},"lng":{"type":"number","description":"-180..180"},"ts":{"type":"string","description":"ISO 8601, epoch seconds or epoch ms. Naive timestamps (no zone) are read as UTC unless ?tz_offset_minutes= is given. More than 5 min in the future or older than 30 days is rejected"},"speed_kmh":{"type":"number","description":"0..400"},"heading_deg":{"type":"number","description":"0..360"},"ignition":{"description":"true/false, on/off, 1/0"},"odometer_km":{"type":"number","description":"km"},"location_text":{"type":"string","description":"free text, cut at 200 characters"}},"required":["plate","lat","lng","ts"]}},{"type":"object","properties":{"positions":{"type":"array","items":{"type":"object","properties":{"plate":{"type":"string","description":"must already exist in the lorries table (never created)"},"lat":{"type":"number","description":"-90..90; 0,0 is rejected"},"lng":{"type":"number","description":"-180..180"},"ts":{"type":"string","description":"ISO 8601, epoch seconds or epoch ms. Naive timestamps (no zone) are read as UTC unless ?tz_offset_minutes= is given. More than 5 min in the future or older than 30 days is rejected"},"speed_kmh":{"type":"number","description":"0..400"},"heading_deg":{"type":"number","description":"0..360"},"ignition":{"description":"true/false, on/off, 1/0"},"odometer_km":{"type":"number","description":"km"},"location_text":{"type":"string","description":"free text, cut at 200 characters"}},"required":["plate","lat","lng","ts"]}}}}]},"example":{"positions":[{"plate":"JKL 9999","lat":1.4927,"lng":103.7414,"speed_kmh":62,"heading_deg":90,"ts":"2026-10-12T08:00:00Z"}]}}}},"responses":{"200":{"description":"{ok, accepted_count, rejected_count, duplicate_count, accepted[], rejected[]}"},"400":{"description":"bad body"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:write"},"413":{"description":"too many records"},"422":{"description":"all rejected"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}},"get":{"tags":["GPS / TMS"],"summary":"Latest position per lorry from the configured TMS provider","operationId":"get_tms_positions","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:read"],"x-scope-mode":"any-of","parameters":[{"name":"source","in":"query","required":false,"schema":{"type":"string","enum":["push"],"description":"raw stored pushes only"}}],"responses":{"200":{"description":"{ok, provider, demo, as_of, last_push_at, positions[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"},"502":{"description":"provider error"}}}},"/tms/osmand":{"get":{"tags":["GPS / TMS"],"summary":"OsmAnd / Traccar-client style position push by query string (key may be sent as ?key= on THIS route only)","operationId":"get_tms_osmand","security":[{"bearerAuth":[]},{"apiKeyHeader":[]},{"apiKeyQuery":[]}],"x-required-scopes":["tms:write"],"x-scope-mode":"any-of","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","description":"plate"}},{"name":"lat","in":"query","required":false,"schema":{"type":"number","description":""}},{"name":"lon","in":"query","required":false,"schema":{"type":"number","description":""}},{"name":"timestamp","in":"query","required":false,"schema":{"type":"string","description":"epoch s/ms or ISO"}},{"name":"speed","in":"query","required":false,"schema":{"type":"number","description":"KNOTS (converted x1.852)"}},{"name":"bearing","in":"query","required":false,"schema":{"type":"number","description":""}},{"name":"ignition","in":"query","required":false,"schema":{"type":"string","description":""}},{"name":"key","in":"query","required":false,"schema":{"type":"string","description":"API key, only on this route"}},{"name":"tz_offset_minutes","in":"query","required":false,"schema":{"type":"integer","description":""}}],"responses":{"200":{"description":"{ok, plate, ts} (a duplicate also answers 200)"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:write"},"404":{"description":"unknown plate"},"422":{"description":"rejected, with reason"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/tms/events":{"post":{"tags":["GPS / TMS"],"summary":"Trip event from the tracking system: arrived / departed / geofence","operationId":"post_tms_events","description":"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).","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:write"],"x-scope-mode":"any-of","requestBody":{"required":true,"content":{"application/json":{"schema":{"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"]}}}},"responses":{"200":{"description":"{ok, plate, type, ts, tempdo_no, duplicate, emitted, auto_status}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:write"},"404":{"description":"order not found"},"422":{"description":"unknown plate / order not on that plate / ts in the future"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/tms/field-map":{"put":{"tags":["GPS / TMS"],"summary":"Store THIS key's vendor-field -> canonical-field map (dotted paths)","operationId":"put_tms_field_map","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:write"],"x-scope-mode":"any-of","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"map":{"type":"object","properties":{},"additionalProperties":{"type":"string","description":"vendor path, e.g. gps.latitude"},"description":"keys: plate, lat, lng, speed_kmh, heading_deg, ignition, odometer_km, ts, location_text"}}},"example":{"map":{"plate":"vehicleNo","lat":"gps.latitude","lng":"gps.longitude","ts":"gpsTime"}}}}},"responses":{"200":{"description":"{ok, key_id, map}"},"400":{"description":"unknown field / bad path"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:write"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}},"get":{"tags":["GPS / TMS"],"summary":"Read this key's field map","operationId":"get_tms_field_map","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:read","tms:write"],"x-scope-mode":"any-of","responses":{"200":{"description":"{key_id, map, canonical_fields}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:read or tms:write"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}},"delete":{"tags":["GPS / TMS"],"summary":"Delete this key's field map","operationId":"delete_tms_field_map","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:write"],"x-scope-mode":"any-of","responses":{"200":{"description":"{ok:true}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:write"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/tms/lorries/{plate}/track":{"get":{"tags":["GPS / TMS"],"summary":"Track points for one lorry","operationId":"get_tms_lorries_plate_track","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:read"],"x-scope-mode":"any-of","parameters":[{"name":"plate","in":"path","required":true,"description":"lorry plate; case, spaces and punctuation are ignored (JKL9999 = JKL 9999)","schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","description":"ISO date"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","description":"ISO date"}}],"responses":{"200":{"description":"{ok, plate, provider, count, track[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:read"},"404":{"description":"unknown plate"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"},"502":{"description":"provider error"}}}},"/tms/jobs":{"get":{"tags":["GPS / TMS"],"summary":"Active assigned trips for the TMS to poll (plate, driver, from/to with coordinates, status)","operationId":"get_tms_jobs","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["tms:read"],"x-scope-mode":"any-of","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","description":"comma list; default Approved,Picked-Up,Departed,In-Transit"}},{"name":"plate","in":"query","required":false,"schema":{"type":"string","description":""}}],"responses":{"200":{"description":"{as_of, count, jobs[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: tms:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/datasets":{"get":{"tags":["Import / Export"],"summary":"Datasets, columns, aliases, limits","operationId":"get_datasets","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["master:read","orders:read"],"x-scope-mode":"any-of","responses":{"200":{"description":"{datasets[], limits, formats, modes, dry_run_default}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: master:read or orders:read"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/export/{dataset}":{"get":{"tags":["Import / Export"],"summary":"Export a dataset as csv / xlsx / json","operationId":"get_export_dataset","description":"The orders dataset needs orders:read; customers, lorries, drivers and charges need master:read.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["orders:read","master:read"],"x-scope-mode":"any-of","parameters":[{"name":"dataset","in":"path","required":true,"description":"dataset name","schema":{"type":"string","enum":["orders","customers","lorries","drivers","charges"]}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["csv","xlsx","json"],"default":"csv"}},{"name":"status","in":"query","required":false,"schema":{"type":"string","description":"only datasets with a status column"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","description":"YYYY-MM-DD, datasets with a date column"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","description":"YYYY-MM-DD"}}],"responses":{"200":{"description":"file or JSON"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: orders:read or master:read"},"404":{"description":"unknown dataset"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}},"/import/{dataset}":{"post":{"tags":["Import / Export"],"summary":"Import customers or lorries. Dry run unless dry_run is exactly false; atomic unless partial","operationId":"post_import_dataset","description":"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.","security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"x-required-scopes":["master:write"],"x-scope-mode":"any-of","parameters":[{"name":"dataset","in":"path","required":true,"description":"dataset name","schema":{"type":"string","enum":["orders","customers","lorries","drivers","charges"]}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["csv","xlsx","json"]}},{"name":"dry_run","in":"query","required":false,"schema":{"type":"string","description":"anything but \"false\" is a dry run"}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","enum":["upsert","create_only","update_only"],"default":"upsert"}},{"name":"partial","in":"query","required":false,"schema":{"type":"string","description":"\"true\" = skip bad rows and write the rest"}},{"name":"strict","in":"query","required":false,"schema":{"type":"string","description":"\"true\" = unknown columns are an error"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"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"]}}}},"responses":{"200":{"description":"{dataset, mode, dry_run, applied, summary, unknown_columns, ignored_columns, rows[]}"},"401":{"description":"missing, malformed, unknown or revoked API key"},"403":{"description":"key lacks scope: master:write"},"405":{"description":"export-only dataset (orders, drivers, charges)"},"413":{"description":"too big"},"422":{"description":"row errors and not partial: nothing written"},"429":{"description":"rate limit (300 requests/min/key) or too many bad credentials from this IP"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"ymk_<id>_<secret>"},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key"},"apiKeyQuery":{"type":"apiKey","in":"query","name":"key","description":"ONLY on GET /tms/osmand, for trackers that cannot set headers"}},"x-scopes":{"orders:read":"read orders and the event feed","orders:write":"create / update orders","billing:read":"preview and download invoice exports, read payment status","billing:write":"mark batches exported / unmarked, import payments","tms:read":"read lorry positions and trips","tms:write":"push GPS positions / trip events from a tracking system","master:read":"read customers, lorries, drivers, charges","master:write":"import / update customers, debtor codes, lorries","webhooks:manage":"manage own webhook subscriptions"}},"x-webhooks":{"description":"Subscribe with POST /webhooks (or a planner at /integrations). Each event is POSTed to your URL as JSON.","request":{"method":"POST","content_type":"application/json","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)"},"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)>"}},"signature":{"algorithm":"HMAC-SHA256","secret":"the whsec_... value returned once when the subscription was created","signed_payload":"`${X-Ymorder-Timestamp}.${raw request body}`","header_format":"v1=<lowercase hex>","replay_tolerance_seconds":300,"compare":"constant-time"},"delivery":{"success":"any 2xx within 8 seconds","retry_minutes_after_attempts":[1,5,30,120,720],"max_attempts":6,"then":"status dead; a planner can redeliver from /integrations","auto_disable_after_consecutive_failures":20,"ordering":"not guaranteed"},"events":{"order.created":"a new order exists (WhatsApp, portal, planner or debtor API)","order.status_changed":"status moved (Approved, Picked-Up, Departed, In-Transit, Delivered, Rejected ...)","order.assigned":"a lorry / driver was assigned to an order (carries plate + driver: the TMS dispatch trigger)","order.updated":"planner or customer edited an order","order.checkpoint":"driver reported a checkpoint / POD","invoice.exported":"orders were exported to an accounting batch (batch id + totals)","invoice.unmarked":"an exported batch was reopened","payment.imported":"payments were imported / reconciled from the accounting system","vehicle.position":"a GPS position was received (high volume: subscribe on purpose)","vehicle.near_destination":"an in-transit lorry came within 5 km of its destination","webhook.test":"manual test ping from the admin page"}}}