SDKs
npm install @afrihex/sdk
The official TypeScript SDK covers addresses, routing and fleet planning, delivery tracking, cash on delivery, delivery pricing, proof of delivery, service areas, hex codes and the Banking Suite. Every method is typed, and the delivery types are generated from the API's own definitions, so field names match exactly. It runs in Node.js 18+ and modern browsers.
Building with Flutter or Dart? See the Dart and Flutter SDK,
afrihex on pub.dev.
To create and read codes with no internet, use the separate
offline toolkit, @afrihex/offline.
Setup
import { AfriHex } from '@afrihex/sdk'
const afrihex = new AfriHex({ apiKey: process.env.AFRIHEX_API_KEY! })
While you build, use a test key: test calls don't use your quota, keep their data apart from live data, and never send texts.
const { key } = await afrihex.account.createTestKey() // afx_test_…, shown once
const test = new AfriHex({ apiKey: key })
What it covers
| Module | Methods |
|---|---|
addresses | lookup, reverse (with landmark directions), parse (free-text addresses and directions), search, autocomplete |
routing | route, plan (fleet planning, including several trips per rider) |
trips | create, get, list, update, ping, events, recordAttempt, attempts, track, reply, notifications, newTrackingLink, recordCollection, collections, voidCollection, setAmountToCollect |
cash | recordRemittance, remittances, voidRemittance, balances, reconciliation |
delivery | quote, quoteBulk, getQuote, getPricing, setPricing, deletePricing |
pod | issue, confirm, get, verify |
serviceAreas | create, list, get, delete, check, bulkCheck |
hex | encode, decode, children, neighbors, parent, geojson, distance, path, bulk, compact, tiles |
bridge | fromGPS, toGPS |
landmarks | geocode |
account | testKey, createTestKey, smsUsage |
webhooks | configure, list, remove, verify (also exported as verifyWebhook) |
| Banking Suite | kyc.verify, consent, location.ping, monitoring, collections, analytics, portfolio.risk |
Trips, cash, fleet planning and delivery pricing need a paid API plan. The Banking Suite needs an account set up for it.
Examples
Addresses and directions
const address = await afrihex.addresses.lookup('GA-183-8164')
const here = await afrihex.addresses.reverse(5.6319, -0.14, { descriptors: true })
here.descriptor?.text // "Opposite Nick Petroleum filling station, Gbatsuna"
const suggestions = await afrihex.addresses.autocomplete('shell', { lat: 5.6, lng: -0.19 })
Ghanaian directions
parse understands addresses written the way people write them, with
"adjacent", "opposite", "behind", "beside", "near", "next to" or "after" a
landmark:
const { parsed } = await afrihex.addresses.parse('Adjacent Goil Filling Station, Madina Zongo Junction')
parsed?.anchors?.[0] // { relation: 'adjacent', name: 'goil filling station' }
parsed?.geocode_query // 'madina zongo junction'
// Find the landmark itself, anchored to the place:
const goil = await afrihex.landmarks.geocode({ q: 'goil', anchor: 'Madina Zongo Junction', kind: 'fuel_station' })
goil.matches[0] // Goil, 276 m from the junction
Routing and fleet planning
const route = await afrihex.routing.route({
from: { point: { lat: 5.6457, lng: -0.075 } },
to: { gps_code: 'GA-183-8164' },
mode: 'motor_scooter',
})
const plan = await afrihex.routing.plan({
vehicles: [{ id: 'kofi', mode: 'motor_scooter', start: { point: { lat: 5.6457, lng: -0.075 } }, capacity: [5], max_trips: 3 }],
jobs: [{ id: 'order-1', location: { gps_code: 'GA-183-8164' }, delivery: [1] }],
})
Tracking a delivery
const trip = await afrihex.trips.create({
destination: { gps_code: 'GA-183-8164' },
rider_ref: 'kofi',
amount_to_collect: 120,
recipient_phone: '0241234567',
sender_name: "Auntie Esi's Kitchen",
})
trip.tracking_url // send this to the customer
await afrihex.trips.ping(trip.id, { lat: 5.64, lng: -0.10 })
await afrihex.trips.recordAttempt(trip.id, { reason: 'customer_unreachable', next_action: 'retry' })
await afrihex.trips.recordCollection(trip.id, { amount: 120, method: 'cash' })
await afrihex.trips.update(trip.id, { status: 'completed' })
const balances = await afrihex.cash.balances() // what each rider collected, handed in and owes
Pricing and proof of delivery
const quote = await afrihex.delivery.quote({
pickup: { point: { lat: 5.6457, lng: -0.075 } },
dropoff: { gps_code: 'GA-183-8164' },
})
quote.amount
const pod = await afrihex.pod.issue({ order_ref: 'order-1', destination: { lat: 5.5421, lng: -0.2074 } })
await afrihex.pod.confirm({ pod_id: pod.pod_id, courier_location: { lat: 5.5421, lng: -0.2074 }, recipient_code: pod.recipient_code })
Hex codes
const hex = await afrihex.hex.encode(5.6221843, -0.1729361, 7)
hex.code // "AF-GH-7-0GXTJJBDZZZZZ" (store this)
hex.short_code // "AF-GH-7-0GXTJJBD" (show this)
KYC verification (Banking Suite)
const result = await afrihex.kyc.verify({
customer_id: 'cust_123',
method: 'gps_code',
location: { gps_code: 'GA-183-8164' },
})
result.verified
Webhooks
Check that each delivery really came from AfriHex before acting on it. Pass the raw body, the headers and your webhook secret:
import { verifyWebhook, isTripEvent } from '@afrihex/sdk'
// Next.js route handler, Hono, Bun or Cloudflare Workers
export async function POST(request: Request) {
let event
try {
event = await verifyWebhook(await request.text(), request.headers, process.env.AFRIHEX_WEBHOOK_SECRET!)
} catch {
return new Response('bad signature', { status: 400 })
}
if (isTripEvent(event) && event.event === 'trip.completed') {
await markDelivered(event.trip.id) // event.trip is fully typed
}
return new Response('ok')
}
With Express, use express.raw({ type: 'application/json' }) on the route and
pass req.body and req.headers. It checks X-Webhook-Signature-V2 and
rejects deliveries more than 5 minutes old; see Webhooks.
Retries and timeouts
The SDK retries failed calls for you: network errors, timeouts, 408, 429,
500, 502, 503 and 504, up to 2 times with backoff, honouring
Retry-After. A 429 whose Retry-After is longer than 30 seconds, such as a
used-up daily quota, comes back as an error straight away.
Retrying a create never does the work twice. Every POST and PATCH carries an
Idempotency-Key, so if the first attempt reached
AfriHex and only the answer was lost, the retry gets the same trip back.
Pass your own key to make your own retries safe too:
const afrihex = new AfriHex({ apiKey, maxRetries: 3, timeout: 20_000 }) // defaults: 2 and 60 000 ms
await afrihex.trips.create(order, { idempotencyKey: `order-${order.id}` })
Errors
The SDK throws AfriHexError with .code, .message, .status and
.requestId from the API response. When no answer arrived,
.code is TIMEOUT or NETWORK_ERROR and .status is 0.
import { AfriHexError } from '@afrihex/sdk'
try {
await afrihex.trips.get('unknown-id')
} catch (err) {
if (err instanceof AfriHexError) console.error(err.code, err.status, err.requestId) // TRIP_NOT_FOUND, 404, …
}
Plain HTTP
No SDK required: every endpoint works with plain curl, as shown throughout
these docs. Send your key in the X-API-Key header.