Hazchem Connect API
Book, track and prove healthcare logistics from your own systems: lab LIS, pharmacy dispensing software, wholesaler order systems and hospital platforms. REST and JSON over HTTPS, signed webhooks, sandbox first.
Endpoints with request and response examples
Send live requests to the mock API
Events and HMAC-SHA256 signatures
Quick start
In this demo build the sandbox runs on this site at /api/v1. Use the published sandbox key hz_test_console_demo; other keys are refused.
# Sandbox base URL (this demo build serves it on the same site)
export HZ_BASE="https://portal.haz-chem.co.za/api/v1"
export HZ_KEY="hz_test_console_demo" # the published sandbox key
# 1. Check the sandbox is up (no key needed)
curl $HZ_BASE/health
# 2. Price a same-day specimen run, 25 km
curl -X POST $HZ_BASE/quotes \
-H "x-api-key: $HZ_KEY" \
-H "content-type: application/json" \
-d '{"sku":"HZ-LAB-04","km":25,"priority":"same_day"}'
# 3. Book it (Idempotency-Key makes retries safe)
curl -X POST $HZ_BASE/jobs \
-H "x-api-key: $HZ_KEY" \
-H "Idempotency-Key: 7d1c5a7e-3f0b-4d55-9b0e-2c1b8f4a6e11" \
-H "content-type: application/json" \
-d '{"quote_id":"q_8k2m4p1x9z","pickup_site_id":"cli-0","dropoff_site_id":"lab-0"}'
# 4. Track it
curl $HZ_BASE/tracking/HZ-SBX1001 -H "x-api-key: $HZ_KEY"Authentication
OAuth2 client credentials for machine-to-machine integrations: exchange your client id and secret for a short-lived JWT (15 minutes) carrying the scopes you need, then send Authorization: Bearer <token>.
curl -X POST https://api.haz-chem.co.za/oauth/token \
-d grant_type=client_credentials \
-d client_id=$HZ_CLIENT_ID -d client_secret=$HZ_CLIENT_SECRET \
-d scope="quotes:write jobs:write tracking:read"
# => {"access_token":"eyJ...","token_type":"Bearer","expires_in":900,"scope":"quotes:write jobs:write tracking:read"}API keys are allowed for low-risk read scopes and EDI uploads. Send them in the x-api-key header. Sandbox keys start with hz_test_, live keys with hz_live_; a live key on the sandbox is refused.
| Scope | Grants | Typical holder |
|---|---|---|
quotes:write | Create quotes and read service levels | Pharmacy, wholesaler, e-commerce |
jobs:write / jobs:read | Create, update, cancel, STAT upgrade; read jobs | All B2B clients |
tracking:read | Tracking URL, ETA, driver first name and photo, vehicle | All clients, white-label pages |
custody:read / temperature:read | Custody events, seals, temperature traces | Labs, hospitals, wholesalers, auditors |
pod:read / certificates:read | POD artefacts and certificates | Clients and auditors |
invoices:read | Invoices and statements | Finance systems |
admin:users / admin:sites | Users, sites, cost centres, policies | HR and ERP systems |
webhooks:manage | Register endpoints, rotate secrets, replay events | Integrators |
partner:orders / partner:stock | Pharmacy quotes, order readiness, stock feeds | Partners, wholesalers |
telemetry:write | Temperature and telematics ingestion | Logger gateways, CarTrack |
Sandbox key and data
The sandbox accepts one published key: hz_test_console_demo. Any other hz_test_ value is refused with 401 invalid_api_key; live keys are refused with 403.
Quotes, jobs and cancellations you create are private to your caller (the key plus your hazchem-sandbox-session header, or your IP address when you send none) and expire after 6 hours without activity. Seeded sample jobs are shared and read-only: changing one creates a private copy for you.
Environments
| Environment | Base URL | Keys | Data |
|---|---|---|---|
| Sandbox | /api/v1 on this site (dedicated host at launch) | hz_test_console_demo | Fictional sites and jobs; no vehicles move; your data expires after 6 h idle |
| Live | https://api.haz-chem.co.za/v1 | hz_live_... or OAuth2 | Real jobs, billed per the price book; hosted in South Africa |
Idempotency
Every POST accepts an Idempotency-Key header and POST /v1/jobs requires one. Sending the same key again within 24 hours returns the original response with idempotent-replayed: true, so a network retry never double-books a STAT run.
Rate limits
Sandbox: 60 requests per minute per caller. Live: 600 requests per minute per client with burst allowance. Every response carries x-ratelimit-limit and x-ratelimit-remaining; over the limit you get 429 with a retry time. Use webhooks instead of polling for tracking.
Versioning and errors
The major version is in the path (/v1). Additive changes (new fields, new events) are non-breaking. Breaking changes ship as /v2 with a deprecation header and direct notice to registered developers before /v1 is withdrawn.
Amounts are integers in cents with currency: "ZAR" and a vat_inclusive flag. Times are ISO 8601. Errors use RFC 9457 problem+json with a stable code and a request_id to quote to support.
HTTP/1.1 422 Unprocessable Entity
content-type: application/problem+json
{
"type": "https://portal.haz-chem.co.za/developers#errors-unknown_sku",
"title": "Unknown SKU",
"status": 422,
"code": "unknown_sku",
"detail": "No service HZ-LAB-99 in price book 2026.10-v1.0.",
"request_id": "req_4gbktuz9ei"
}| Status | Code | Meaning |
|---|---|---|
| 400 | idempotency_key_required | Idempotency-Key required |
| 400 | invalid_body | Invalid body |
| 400 | invalid_json | Invalid JSON |
| 401 | missing_api_key | Missing API key |
| 401 | invalid_api_key | Invalid API key |
| 403 | live_key_on_sandbox | Live key used on sandbox |
| 404 | not_found | Endpoint not found |
| 404 | job_not_found | Job not found |
| 404 | quote_not_found | Quote not found |
| 404 | tracking_not_found | Tracking reference not found |
| 404 | certificate_not_found | Certificate not found |
| 409 | already_cancelled | Already cancelled |
| 409 | already_delivered | Already delivered |
| 409 | in_custody | Parcel in custody |
| 409 | certificate_pending | Certificate not issued yet |
| 410 | quote_expired | Quote expired |
| 422 | invalid_priority | Invalid priority |
| 422 | invalid_timing | Invalid timing |
| 422 | not_a_delivery | Not a delivery service |
| 422 | reason_required | Reason required |
| 422 | unknown_site | Unknown site |
| 422 | unknown_sku | Unknown SKU |
| 429 | rate_limited | Too many requests |
Data minimisation: the API returns only the fields a scope needs. Medicine names never appear in courier or tracking scopes.
