MedsGHLogistics Provider Standardv0.1 · Draft

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.

Base URL
{provider_base} (e.g. sandbox.2point…)
Auth
provider-issued key
Callbacks
signed, per-job URL
Stable
quote · book · callbacks — build against these; cancel windows may tighten

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.

Key check
POST{provider_base}/ping
What to send us at onboarding
// 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.

Request
GET/rates?pickup_lat=…&pickup_lng=…&dropoff_lat=…&dropoff_lng=…&weight_kg=…&service=standard
Response · 200
{
  "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.Header and idempotency_key field carry the same value: req_{request_id}_pharm_{pharmacy_id}.
Request
POST/jobs Idempotency-Key: req_1042_pharm_17
Body · the job packet
{
  "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
}
Response · 201
{ "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.

Proof rule.delivered requires otp_verified: true OR a photo_url. Unrecognised statuses are rejected with 422 UNKNOWN_STATUS and logged — never guessed.
Event payload
{
  "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

StatusMeaningTriggers on our side
acceptedJob takenAssigned; pharmacy + customer notified
picked_upCollected from pharmacyCustomer tracking updates
in_transitOn the wayCustomer tracking updates
deliveredHanded over + proofPayout triggered; order completed
failedNot deliverable + reasonJob re-offered; pharmacy notified
cancelledStood down pre-pickupJob 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.

Request
POST/jobs/:id/cancel { "reason": "customer_requested" }

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.

Request
GET/wallet/balance

08 · Reliability

Timeouts and retries

CallTimeoutRetries
rates10sno retry — a fresh quote on next open is cheaper than a stale one
create job15s3 attempts, backoff 1s → 2s → 4s, same idempotency key
track10son demand (callbacks are the live channel; track is fallback)
cancel10scancel is idempotent by reference — safe to re-send
Timeout ≠ rejection.A timeout means “unknown” — verify with track before acting. Never assume the job was or wasn't created.

09 · Errors

Error contract

All failures return { code, message }. Only these codes exist:

CodeHTTPWhen
INVALID_QUOTE422Quote reference missing, expired or mismatched
UNKNOWN_REFERENCE404Booking reference not found
TOO_LATE_TO_CANCEL409Cancel attempted after pickup
NO_COVERAGE422Zone not served by any provider
PROVIDER_DOWN502Upstream 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_url pattern 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.