Skip to main content

Dart and Flutter SDK

flutter pub add afrihex # or: dart pub add afrihex

afrihex is the official package for Flutter apps (Android, iOS, web, desktop) and Dart servers. It covers addresses and landmark directions, routing and fleet planning, delivery tracking, cash on delivery, delivery pricing, proof of delivery, service areas and hex codes. Every response is a typed class, generated from the API's own definitions, so field names match exactly (in camelCase: gps_code is gpsCode).

In a Flutter app: use a publishable key​

Never put your secret key (afx_live_…) in an app, because anyone can pull it out of the APK. Use a publishable key (afx_pub_…) instead. It is locked to your app's package name or bundle ID, and it can only look up addresses. It cannot create trips, read your data or change anything.

  1. In your AfriHex dashboard, go to Publishable keys and click Create publishable key. Under Mobile apps, add your Android applicationId and your iOS bundle ID (for example com.kofishop.app).
  2. Pass the key and the app ID to the client:
import 'package:afrihex/afrihex.dart';

final afx = AfriHex(apiKey: 'afx_pub_…', appId: 'com.kofishop.app');

// What the customer typed: a code, a place, or directions.
final suggestions = await afx.addresses.suggest('adjacent goil filling station, madina zongo junction');
// suggestions.first.title → Goil Fuel Station Madina Zongo Junction
// suggestions.first.subtitle → 276 m from Madina zongo junction bus stop · J.J. Rawlings Avenue

// Once they pick one, add the GhanaPostGPS code and rider directions.
final address = await afx.addresses.complete(suggestions.first.address);
// address.gpsCode → GM-017-3727
// address.directions → At Goil filling station, Madina

// Or use their current position (from the geolocator package, for example).
final here = await afx.addresses.atPosition(position.latitude, position.longitude, accuracyM: position.accuracy);

A publishable key can call addresses.lookup, reverse, parse, autocomplete, suggest, complete, atPosition and landmarks.geocode. Each visitor is limited to 120 calls a minute, and the calls count against your account's daily quota.

A checkout address field​

class AddressField extends StatefulWidget {
const AddressField({super.key, required this.onSelected});
final ValueChanged<AfriHexAddress> onSelected;


State<AddressField> createState() => _AddressFieldState();
}

class _AddressFieldState extends State<AddressField> {
Timer? _debounce;
Completer<List<AddressSuggestion>>? _pending;

// Wait until the customer pauses typing, then ask AfriHex.
Future<List<AddressSuggestion>> _search(String text) {
_debounce?.cancel();
_pending?.complete(const []);
final done = _pending = Completer();
_debounce = Timer(const Duration(milliseconds: 250), () async {
try {
final s = await afx.addresses.suggest(text);
if (!done.isCompleted) done.complete(s);
} on AfriHexException {
if (!done.isCompleted) done.complete(const []);
}
});
return done.future;
}


Widget build(BuildContext context) => Autocomplete<AddressSuggestion>(
optionsBuilder: (v) => v.text.trim().length < 2 ? const <AddressSuggestion>[] : _search(v.text),
displayStringForOption: (s) => s.title,
optionsViewBuilder: (context, onSelected, options) => Material(
elevation: 4,
child: ListView(shrinkWrap: true, children: [
for (final s in options)
ListTile(title: Text(s.title), subtitle: s.subtitle == null ? null : Text(s.subtitle!), onTap: () => onSelected(s)),
]),
),
onSelected: (s) async => widget.onSelected(await afx.addresses.complete(s.address)),
fieldViewBuilder: (context, controller, focus, _) => TextField(
controller: controller,
focusNode: focus,
decoration: const InputDecoration(labelText: 'Delivery address', hintText: 'GhanaPostGPS code, place or directions'),
),
);
}

Send the chosen AfriHexAddress (address.toJson()) to your server with the order. It has lat, lng, gpsCode, street, area, region, directions, and input, which is what the customer typed, worth keeping for the rider.

On your server: use your secret key​

On a Dart server (Dart Frog, Shelf, Serverpod) the client can call every endpoint:

final afx = AfriHex(apiKey: Platform.environment['AFRIHEX_API_KEY']!);

final quote = await afx.delivery.quote(DeliveryQuoteRequest(
pickup: RouteEndpoint(gpsCode: 'GA-183-8164'),
dropoff: RouteEndpoint(point: LatLng(order.lat, order.lng)),
));

final trip = await afx.trips.create(
TripCreateRequest(
destination: RouteEndpoint(point: LatLng(order.lat, order.lng)),
destinationLabel: order.directions,
amountToCollect: 120,
riderRef: 'kwame',
recipientPhone: order.phone,
),
options: RequestOptions(idempotencyKey: order.id), // safe to retry: one order, one trip
);
print(trip.trackingUrl); // send this to your customer
ModuleWhat it does
addresseslookup, reverse (with landmark directions), search, autocomplete, parse, suggest, complete, atPosition
landmarksfind a landmark by name, or near a point or place
routingroute for directions by okada, car, foot or truck; plan for fleet planning
tripscreate, ping, update, events, failed attempts, tracking links, recipient replies, cash collections
cashrider remittances, balances and reconciliation
deliveryfee rulebook and quotes
podproof of delivery: issue, confirm, verify signed receipts
serviceAreascreate areas and check whether a point is inside one
hex, bridgeAfriHex hex codes, and converting between them and GhanaPostGPS codes
webhooks, accountwebhook endpoints, test keys, SMS usage

Webhooks​

// Pass the raw body, exactly as received.
final event = verifyWebhook(await request.readAsString(), request.headers, Platform.environment['AFRIHEX_WEBHOOK_SECRET']!);
if (event.event == 'trip.completed') await markDelivered(event.trip!.externalId);

verifyWebhook checks the X-Webhook-Signature-V2 header, which also covers the timestamp. If the signature doesn't match, or the event is more than 5 minutes old, it throws AfriHexException('INVALID_SIGNATURE'). Reply 400 when that happens.

Errors, retries and timeouts​

Every failure throws AfriHexException, which has:

  • code, such as NOT_FOUND, RATE_LIMITED, APP_NOT_ALLOWED, TIMEOUT or NETWORK_ERROR;
  • the HTTP status;
  • a requestId to quote to support.

Calls are retried twice by default after network errors, timeouts, 408, 429 and 5xx responses. The client waits as long as Retry-After says, up to 30 seconds; a longer wait, such as for a daily quota reset, is returned to you as an error instead. POST and PATCH calls carry an Idempotency-Key, so a retry never creates a second trip. You can change the retry count and timeout:

AfriHex(apiKey: key, maxRetries: 3, timeout: const Duration(seconds: 20));

Test mode​

A test key (afx_test_…) runs every call in a sandbox. It sends no real texts and doesn't use your quota. Create one in the dashboard, or with afx.account.createTestKey().

If a lookup fails in your app​

error.codeWhat to do
APP_NOT_ALLOWEDAdd the exact ID you pass as appId to the key's Mobile apps in the dashboard. Changes apply within 30 seconds.
ORIGIN_REQUIREDThe client has no appId. Pass your package name or bundle ID: AfriHex(apiKey: 'afx_pub_…', appId: 'com.yourshop.app').
ORIGIN_NOT_ALLOWEDOn Flutter web, the browser sends the page's origin: add that site (for example https://yourshop.com) to the key's websites.
PUBLISHABLE_KEY_SCOPEA publishable key only looks up addresses. Create trips, quotes and the rest from your server with your secret key.

Package​

pub.dev/packages/afrihex. For JavaScript and TypeScript, see the TypeScript SDK; for a web checkout field, see <AddressInput />.