Skip to main content

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​

ModuleMethods
addresseslookup, reverse (with landmark directions), parse (free-text addresses and directions), search, autocomplete
routingroute, plan (fleet planning, including several trips per rider)
tripscreate, get, list, update, ping, events, recordAttempt, attempts, track, reply, notifications, newTrackingLink, recordCollection, collections, voidCollection, setAmountToCollect
cashrecordRemittance, remittances, voidRemittance, balances, reconciliation
deliveryquote, quoteBulk, getQuote, getPricing, setPricing, deletePricing
podissue, confirm, get, verify
serviceAreascreate, list, get, delete, check, bulkCheck
hexencode, decode, children, neighbors, parent, geojson, distance, path, bulk, compact, tiles
bridgefromGPS, toGPS
landmarksgeocode
accounttestKey, createTestKey, smsUsage
webhooksconfigure, list, remove, verify (also exported as verifyWebhook)
Banking Suitekyc.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.