MedsGH · For provider engineering teams
Logistics Provider Standard
Four calls on your API, one key you issue us, signed status events you POST to the per-job URL we hand you. Same contract for every provider.
01 · Authentication
Credentials come from you
Sandbox key + production key from you — separate, never interchangeable. We attach your key to every call in the header you name at onboarding.
We ping first. A 200 means everything below will work.
Provider callbacks (provider → MedsGH) go to the per-job callback_url included in every job packet — that is our endpoint, not yours. Each callback carries an HMAC-SHA256 signature header computed over the raw body with our shared secret. We reject unsigned calls.
// you issue these; we plug them in and ping "base_url": "https://sandbox.<your-domain>", "api_key": "<key-we-attach-to-every-call>", "auth_header": "Client-Api-Key (or yours — just name it)", "signing_secret":"<shared-secret-for-callback-HMAC>"
02 · Overview
The four calls
flowchart TD
A["1. GET rates — price it"] --> B["2. POST jobs — hand it over"]
B --> C["3. Status updates — track it"]
C --> D["4. Cancel — before pickup only"]
03 · Call 1
Get rates
Price a trip before committing. Send us the trip; we send it to you with pickup + dropoff coords, parcel weight/dims/value, and the slot. You return one number. The quote_reference you return is passed back verbatim at booking — it locks the price so neither side reprices mid-job. Recommended timeout: 10s.
Parcel units: kg, metres, GHS. If your API takes other units, say so at onboarding — the adapter converts.
{
"quote_reference": "Q-2PT-88121",
"amount": 25.00, "currency": "GHS",
"expires_at": "2026-09-13T12:00:00Z",
"breakdown": { "base": 10, "distance": 9, "weight": 2, "priority": 0, "tax": 4 }
}
04 · Call 2
Post a job
Hand over the full job packet. Retries reuse the same Idempotency-Key — duplicates return the original booking (201 with identical reference), never a second job.
idempotency_key field carry the same value: req_{request_id}_pharm_{pharmacy_id}.{
"job_id": "MG-1042-P17",
"idempotency_key": "req_1042_pharm_17",
"quote_reference": "Q-2PT-88121",
"service": { "level": "standard", "zone": "accra-central", "insurance": false },
"pickup": {
"pharmacy_id": 17, "name": "Aseda Pharmacy",
"address": "12 Oxford Street, Osu",
"latitude": 5.5556, "longitude": -0.1833,
"contact": "0244123456", "ready_at": "2026-09-13T10:15:00Z"
},
"dropoff": {
"address": "14 Mango Road, East Legon",
"latitude": 5.6380, "longitude": -0.1720,
"receiver_name": "Ama Serwaa", "receiver_phone": "0551234567",
"note": "Call on arrival. Gate code 4410."
},
"package": {
"description": "Sealed pharmacy parcel", "weight_kg": 0.8,
"length_cm": 20, "width_cm": 15, "height_cm": 10,
"value_ghs": 145.50, "fragile": false, "cold_chain": false
},
"metadata": { // optional provider-specific extras, forwarded as-is
"location_type": "store"
},
"callback_url": "https://api.medsgh.com/api/v1/provider/jobs/MG-1042-P17/status" // OUR endpoint — you POST status updates here
}
{ "provider_reference": "uuu24Rres", "status": "accepted", "accepted_at": "2026-09-13T10:16:02Z" }
05 · Call 3
Status updates
You POST to us. Each job packet carries a callback_url — our endpoint, minted per job. Send X-Event-Id, X-Event-At, and HMAC-SHA256 headers. No callback yet? We poll your tracking endpoint as fallback.
delivered requires otp_verified: true OR a photo_url. Unrecognised statuses are rejected with 422 UNKNOWN_STATUS and logged — never guessed.{
"job_id": "MG-1042-P17", "provider_reference": "uuu24Rres",
"status": "in_transit", "at": "2026-09-13T11:02:00Z",
"rider": { "name": "Kwame M.", "phone": "0559876543", "plate": "GS-4410-22" },
"proof": { "otp_verified": false, "photo_url": "https://…" }
}
The six words
| Status | Meaning | Triggers on our side |
|---|---|---|
accepted | Job taken | Assigned; pharmacy + customer notified |
picked_up | Collected from pharmacy | Customer tracking updates |
in_transit | On the way | Customer tracking updates |
delivered | Handed over + proof | Payout triggered; order completed |
failed | Not deliverable + reason | Job re-offered; pharmacy notified |
cancelled | Stood down pre-pickup | Job returned to pool |
06 · Call 4
Cancel
Valid before pickup only. Post-pickup cancellations return 409 TOO_LATE_TO_CANCEL — end the job as failed with a reason instead.
07 · Settlement
Wallet
We read your balance endpoint for accrued amounts; you confirm transfers via callbacks to us. Reconcile daily; report mismatches with job ids attached.
08 · Reliability
Timeouts and retries
| Call | Timeout | Retries |
|---|---|---|
| rates | 10s | no retry — a fresh quote on next open is cheaper than a stale one |
| create job | 15s | 3 attempts, backoff 1s → 2s → 4s, same idempotency key |
| track | 10s | on demand (callbacks are the live channel; track is fallback) |
| cancel | 10s | cancel is idempotent by reference — safe to re-send |
09 · Errors
Error contract
All failures return { code, message }. Only these codes exist:
| Code | HTTP | When |
|---|---|---|
INVALID_QUOTE | 422 | Quote reference missing, expired or mismatched |
UNKNOWN_REFERENCE | 404 | Booking reference not found |
TOO_LATE_TO_CANCEL | 409 | Cancel attempted after pickup |
NO_COVERAGE | 422 | Zone not served by any provider |
PROVIDER_DOWN | 502 | Upstream provider unreachable after retries |
10 · Onboarding
Go-live checklist
- Hand MedsGH your sandbox base URL, API key, and auth-header name. We
POST /ping→ expect 200. Green means the plumbing works. - Confirm coverage zones and rate card; agree the per-job
callback_urlpattern and signing secret. - Run one sandbox job end to end: rates → job → statuses → delivered with proof.
- Verify timeout/retry behaviour with a forced-slow test.
- Hand over production credentials and go live.