afrihex 0.1.0
afrihex: ^0.1.0 copied to clipboard
The AfriHex API for Dart and Flutter: GhanaPostGPS codes, landmark directions, routing, delivery tracking, cash on delivery and pricing for Ghana.
afrihex #
The AfriHex API for Dart and Flutter. Use it to:
- find Ghana addresses the way people give them: GhanaPostGPS codes, place names, and directions such as "adjacent Goil filling station, Madina Zongo Junction";
- get directions a rider can follow, for any point: "Opposite Nick Petroleum filling station, Gbatsuna";
- route and plan deliveries;
- track deliveries, record cash on delivery, quote delivery fees, issue proof of delivery and check service areas.
It works in Flutter apps (Android, iOS, web, desktop) and on Dart servers. Every response is typed.
flutter pub add afrihex # or: dart pub add afrihex
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.
- In your AfriHex dashboard, go to Publishable keys and click Create publishable key. Under Mobile apps, add your Android
applicationIdand your iOS bundle ID (for examplecom.kofishop.app). - 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;
@override
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;
}
@override
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
| Module | What it does |
|---|---|
addresses |
lookup, reverse (with landmark directions), search, autocomplete, parse, suggest, complete, atPosition |
landmarks |
find a landmark by name, or near a point or place |
routing |
route for directions by okada, car, foot or truck; plan for fleet planning |
trips |
create, ping, update, events, failed attempts, tracking links, recipient replies, cash collections |
cash |
rider remittances, balances and reconciliation |
delivery |
fee rulebook and quotes |
pod |
proof of delivery: issue, confirm, verify signed receipts |
serviceAreas |
create areas and check whether a point is inside one |
hex, bridge |
AfriHex hex codes, and converting between them and GhanaPostGPS codes |
webhooks, account |
webhook 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 asNOT_FOUND,RATE_LIMITED,APP_NOT_ALLOWED,TIMEOUTorNETWORK_ERROR;- the HTTP
status; - a
requestIdto 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().
Docs: developer.afrihex.com/dart · MIT licence