Audience: External merchant engineering teams. Status: Production. Version: v1.1 Last updated: 2026-05.
1. Introduction
The Akwaan Express B2B Integration lets merchants:
- Push orders into Akwaan Express via REST API.
- Receive lifecycle status updates for those orders via outbound webhooks.
- Query merchant wallet balances and submit withdraw requests.
Integration is bidirectional:
| Direction | Channel | Trigger |
|---|---|---|
| Merchant → Akwaan | REST API call to /api/merchant/b2b/... | The merchant initiates (e.g. creates an order). |
| Akwaan → Merchant | HTTPS POST to merchant-configured webhook URL | Akwaan initiates whenever an order status changes. |
Supported capabilities
- Order creation
- Merchant profile updates
- Wallet balance retrieval
- Withdraw request submission
- Real-time order status webhooks (20 broadcastable statuses)
- OTP delivery webhooks (when an order requires customer OTP verification)
2. Architecture overview
+---------------------+ +---------------------------+ | Merchant System | REST | Akwaan Express Backend | | +-------->+ /api/merchant/b2b/... | | | | | | Webhook Listener |<--------+ IntegrationService | | (your URL) | POST | BroadcastOrderDetailsBulk| +---------------------+ +---------------------------+
- All REST calls are merchant-initiated and authenticated with a static API Key sent in the
ApiKeyHTTP header. - All webhooks are Akwaan-initiated and POSTed to the merchant's configured base URL with the path
/api/akwaan/order-delivery/webhookappended. - Webhook authentication is configured per merchant: a single custom header
key:valuepair (e.g.X-Webhook-Token: <secret>).
3. Authentication
3.1 API Key
All requests to /api/merchant/b2b/* MUST include:
| Header | Value |
|---|---|
ApiKey | The API key issued to your merchant account |
The API key is mapped to exactly one merchant. The backend uses it both to authenticate the caller and to scope the request to that merchant's data. Merchants cannot read or modify another merchant's resources.
3.2 Example
POST /api/merchant/b2b/order HTTP/1.1
Host: api.akwanexpress.com
Content-Type: application/json
ApiKey: akx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{ ... order body ... }
3.3 Failure modes
| Condition | HTTP | Response body |
|---|---|---|
| Header missing | 400 | { "code": 400, "message": "API Key is missing" } |
| Header present but unknown / revoked | 400 | { "code": 400, "message": "Invalid API key" } |
| Header present, merchant deleted | 400 | Same as above |
Security note: API keys are treated as bearer credentials. Treat them like passwords — never log them, never commit them, rotate on suspected leak. Contact Akwaan operations to issue a replacement and revoke the old key.
3.4 Recommended client hygiene
- Store the key in a secrets manager (AWS Secrets Manager / Azure Key Vault / Vault / Doppler) — never in source control.
- Restrict network egress: only your backend service should be calling Akwaan, never the browser.
- Rotate the key on any suspected leak or staff offboarding.
- Use a dedicated key per environment (sandbox vs. production).
4. Outbound webhook integration
4.1 Configuration
Provide Akwaan operations with:
| Field | Description |
|---|---|
Url | Your webhook base URL (e.g. https://api.example.com). Akwaan will POST to {Url}/api/akwaan/order-delivery/webhook. |
AuthorizationKey | (Optional) A custom header name Akwaan will send on every webhook (e.g. X-Webhook-Secret). |
AuthorizationValue | (Optional) The header value. Use this as a shared secret to verify the request came from Akwaan. |
HasOTP | Whether your orders require customer OTP verification before delivery. |
4.2 Lifecycle
- An order's status changes inside Akwaan (e.g. delegate marks Delivered).
- The status transition triggers
IntegrationService.BroadcastOrderDetailsBulk. - Events are batched by merchant — each merchant receives one HTTP POST carrying all the events that concern them.
- Events for non-broadcastable statuses are silently dropped (see §5).
- The HTTP POST is dispatched with up to 3 attempts, 15s timeout per attempt, linear backoff (2s, 4s).
- A 2xx response = delivered. A 4xx response (except 408/429) = abandoned (your error, no point retrying). A 5xx / network error / timeout = retried up to the limit.
4.3 Retry behavior
| Response | Retried? |
|---|---|
2xx | No (success). |
408 Request Timeout | Yes. |
429 Too Many Requests | Yes. |
4xx (other) | No (assumed permanent — fix your endpoint). |
5xx | Yes. |
| Network error / DNS / TLS failure | Yes. |
| Timeout (15s) | Yes. |
After 3 failed attempts the event is abandoned. There is no built-in catch-up mechanism today; if you suspect missed events, contact operations to schedule a replay or use the manual GET /api/merchant/b2b/order/{externalId} lookup (see §14 for a future bulk sync endpoint suggestion).
4.4 Timing & ordering guarantees
- At-least-once delivery: a webhook may arrive more than once if a retry crosses with a slow ACK on your side. Idempotent handlers are required.
- No strict ordering: webhooks are dispatched concurrently across merchants and across orders. Use the
eventTimestampfield (UTC) and your stored status timestamp to decide whether to apply a state transition. - Best-effort: webhook delivery is fire-and-forget from Akwaan's order processing — a slow or down merchant cannot block order operations. Make your endpoint return 2xx in under a few seconds and queue work asynchronously.
4.5 Webhook security
- Verify the
AuthorizationKey: AuthorizationValuepair on every inbound request. - Reject any request that does not match. Return 401.
- Restrict your webhook endpoint to Akwaan's egress IPs if you require defense-in-depth (contact operations for the current list).
- Treat the payload as untrusted until the auth header is verified.
5. Broadcasted order statuses
Akwaan broadcasts webhooks only for the 20 statuses below. Any other internal status transition does NOT generate a webhook.
| status (int) | statusKey | English | Arabic | Meaning |
|---|---|---|---|---|
| 0 | Pending | Pending | قيد الانتظار | Order created, awaiting pickup assignment. |
| 1 | InPickUpShipment | In Pickup Shipment | في قائمة الاستلام | Added to a pickup manifest assigned to a delegate. |
| 4 | Received | Received | تم الاستلام | Picked up from the merchant. |
| 5 | NotReceived | Not Received | لم يتم الاستلام | Pickup attempted but failed (merchant unavailable, refused, etc). |
| 6 | InWarehouse | In Warehouse | في المخزن | Arrived at warehouse, awaiting delivery routing. |
| 7 | InDeliveryShipment | In Delivery Shipment | في قائمة التسليم | Loaded onto a delivery manifest. |
| 8 | InDeliveryProgress | In Delivery Progress | جاري التسليم | Delegate is actively delivering. |
| 10 | Delivered | Delivered | تم التسليم | Successfully delivered to customer. |
| 12 | Cancelled | Cancelled | ملغي | Cancelled before completion. |
| 13 | Completed | Completed | مكتمل | Final accounting state — money settled. |
| 14 | RescheduledInWarehouse | Rescheduled In Warehouse | إعادة جدولة في المخزن | Failed delivery; back to warehouse for re-routing. |
| 15 | RescheduledDelegate | Rescheduled With Delegate | إعادة جدولة مع المندوب | Failed delivery; delegate will retry. |
| 16 | RefundedInWarehouse | Refunded In Warehouse | مرتجع في المخزن | Refund initiated; package at warehouse. |
| 17 | RefundedDelegate | Refunded With Delegate | مرتجع مع المندوب | Refund initiated; delegate carrying it. |
| 19 | WarehouseTransfer | Warehouse Transfer | تحويل بين مخازن | Moving between warehouses. |
| 27 | InRefundShipment | In Refund Shipment | في قائمة الإرجاع | On a refund manifest heading back to merchant. |
| 28 | InRefundProgress | In Refund Progress | جاري الإرجاع | Refund delegate actively delivering back to merchant. |
| 29 | RefundedToMerchant | Refunded To Merchant | تم الإرجاع للتاجر | Refund handed back to merchant. |
| 30 | WarehouseTransferRefund | Warehouse Transfer (Refund) | تحويل بين مخازن (مرتجع) | Refund package moving between warehouses. |
| 31 | WarehouseTransferRefundDelegate | Warehouse Transfer Refund Delegate | تحويل مرتجع مع المندوب | Refund package with a transfer delegate. |
Stability contract:
- The numeric
statusvalue andstatusKeystring are stable identifiers — branch your business logic on these.- The
statusNameEn/statusNameArare display strings — use them for UI/email/log output only. They may be reworded without notice.- New broadcastable statuses may be added in future releases — your handler must tolerate unknown statuses gracefully (log and ignore is fine).
6. Webhook payload structure
6.1 Top-level shape
The webhook body is a JSON array of events. One HTTP POST may contain multiple events when they concern the same merchant.
[
{
"EventType": "OrderStatusUpdated",
"EventData": { ... }
},
{
"EventType": "OTPSend",
"EventData": { ... }
}
]
| Field | Type | Required | Description |
|---|---|---|---|
EventType | string | yes | One of OrderStatusCreated, OrderStatusUpdated, OTPSend, OTPResend. |
EventData | object | yes | Event-specific payload, see below. |
6.2 EventData for OrderStatusCreated / OrderStatusUpdated
{
"orderId": "MERCHANT-EXTERNAL-ID-123",
"status": 10,
"statusKey": "Delivered",
"statusNameEn": "Delivered",
"statusNameAr": "تم التسليم",
"eventTimestamp": "2026-05-20T13:42:11.4321Z"
}
| Field | Type | Nullable | Description |
|---|---|---|---|
orderId | string | yes | The merchant-provided ExternalId from order creation. Use this to correlate with your own DB. |
status | integer | no | Numeric value of OrderStatus. Stable. |
statusKey | string | no | Enum identifier. Stable. |
statusNameEn | string | no | English display name. Display-only. |
statusNameAr | string | no | Arabic display name. Display-only. |
eventTimestamp | ISO 8601 UTC | no | When Akwaan generated the event. |
6.3 EventData for OTPSend
Same shape as the status events, plus an otp field:
{
"orderId": "MERCHANT-EXTERNAL-ID-123",
"status": 8,
"statusKey": "InDeliveryProgress",
"statusNameEn": "In Delivery Progress",
"statusNameAr": "جاري التسليم",
"eventTimestamp": "2026-05-20T13:42:11.4321Z",
"otp": "4821"
}
6.4 EventData for OTPResend
The resend event carries only the OTP and order id (no status, since the order's status has not changed):
{
"orderId": "MERCHANT-EXTERNAL-ID-123",
"otp": "4821",
"eventTimestamp": "2026-05-20T13:42:11.4321Z"
}
6.5 Required webhook response
Akwaan considers the delivery successful if your endpoint returns:
- HTTP status
200,201,202, or204. - Within 15 seconds.
The response body is ignored. Return 200 OK with an empty body as the conventional ack.
7. Inbound REST API endpoints
Base path: /api/merchant/b2b
7.1 GET /wallets
Returns the merchant's wallet balances.
| Request body | Response | Auth |
|---|---|---|
| None | 200 OK with array of NewWalletDto | ApiKey header required |
7.2 POST /order
Create a new order in Akwaan.
Request body — OrderCreationIntegrationForm:
| Field | Type | Required | Notes |
|---|---|---|---|
Code | string | yes | Your internal code. Must be unique within your merchant. |
Pin | string | no | Secondary identifier for the order — an additional code printed on the package alongside Code. Used as a fallback reference when the primary code cannot be scanned or read. Treat it as a second order code, not an OTP. |
CustomerId | string | no | Your internal customer id. |
CustomerName | string | yes | |
CustomerPhoneNumber | string | yes | |
CustomerSecondPhoneNumber | string | no | |
ExternalId | string | yes | The id Akwaan will quote back to you in every webhook. Make it stable. |
Content | string | yes | Description of package contents. |
DeliveryZoneName | string | yes | Must match an Akwaan zone name in the destination governorate. |
PickupZoneName | string | yes | Must match an Akwaan zone name in the source governorate. |
PickUpGovernorateId | int | yes | |
DeliveryGovernorateId | int | yes | |
Note | string | no | |
Amount | decimal | yes | The order amount the customer pays on delivery, excluding the delivery fee. The delivery fee is calculated separately by Akwaan based on the destination zone — do not include it here. |
NearestLandmark | string | no | |
Size | enum | no | Small (default), Medium, Large. |
IsPaid | bool | no | Set to true when the delivery fee for this order has already been paid by the merchant (e.g. prepaid online). When true, the delivery fee is not collected from the customer and is instead deducted from the merchant's settlement payout. Defaults to false (customer pays the delivery fee on delivery). |
PickUpLocation | object | no | { "lat": ..., "lng": ... }. |
DeliveryLocation | object | no | { "lat": ..., "lng": ... }. |
Response: ExternalOrderDto containing the order's Akwaan id, code, status, and echo of submitted fields.
7.3 PUT /merchant
Update merchant profile.
7.4 POST /withdraw-request
Submit a withdraw request against the merchant's wallet.
8. Request / response examples
8.1 Create order — success
Request:
POST /api/merchant/b2b/order HTTP/1.1
ApiKey: akx_live_xxx
Content-Type: application/json
{
"Code": "SHOP-9912",
"ExternalId": "MERCHANT-EXTERNAL-ID-123",
"CustomerName": "Ahmed Ali",
"CustomerPhoneNumber": "07701234567",
"Content": "Mobile phone case",
"DeliveryZoneName": "Karrada",
"PickupZoneName": "Mansour",
"PickUpGovernorateId": 1,
"DeliveryGovernorateId": 1,
"Amount": 25000,
"Size": "Small"
}
Response — 200 OK:
{
"code": 200,
"message": "Operation successful",
"data": {
"id": "8a7b6c5d-1234-5678-9abc-def012345678",
"code": "SHOP-9912",
"customerName": "Ahmed Ali",
"customerPhoneNumber": "07701234567",
"externalId": "MERCHANT-EXTERNAL-ID-123",
"amount": 25000,
"status": "Pending"
}
}
8.2 Create order — validation error
{
"code": 400,
"message": "Delivery Zone or Pickup Zone cant be null or empty",
"errors": null
}
8.3 Authentication failure
POST /api/merchant/b2b/order HTTP/1.1
Content-Type: application/json
(no ApiKey header)
{
"code": 400,
"message": "API Key is missing"
}
8.4 Webhook — single OrderStatusUpdated event
POST /api/akwaan/order-delivery/webhook HTTP/1.1
Host: api.your-shop.com
Content-Type: application/json; charset=utf-8
X-Webhook-Secret: <your-configured-shared-secret>
[
{
"EventType": "OrderStatusUpdated",
"EventData": {
"orderId": "MERCHANT-EXTERNAL-ID-123",
"status": 10,
"statusKey": "Delivered",
"statusNameEn": "Delivered",
"statusNameAr": "تم التسليم",
"eventTimestamp": "2026-05-20T13:42:11.4321Z"
}
}
]
8.5 Webhook — mixed batch (status + OTP)
[
{
"EventType": "OrderStatusUpdated",
"EventData": {
"orderId": "MERCHANT-EXTERNAL-ID-100",
"status": 8,
"statusKey": "InDeliveryProgress",
"statusNameEn": "In Delivery Progress",
"statusNameAr": "جاري التسليم",
"eventTimestamp": "2026-05-20T13:42:11.4321Z"
}
},
{
"EventType": "OTPSend",
"EventData": {
"orderId": "MERCHANT-EXTERNAL-ID-100",
"status": 8,
"statusKey": "InDeliveryProgress",
"statusNameEn": "In Delivery Progress",
"statusNameAr": "جاري التسليم",
"eventTimestamp": "2026-05-20T13:42:11.5023Z",
"otp": "4821"
}
}
]
9. Error handling
9.1 Response envelope
All API responses use the standard envelope:
{
"code": 200,
"message": "Operation successful",
"data": { ... },
"pagination": { ... },
"errors": [ ... ]
}
| Field | Type | When present |
|---|---|---|
code | int | Always (HTTP status mirror). |
message | string | Always. |
data | T | On success. |
pagination | object | On paginated list responses. |
errors | string[] | On validation failures. |
9.2 Common error codes
| HTTP | Cause | Action |
|---|---|---|
| 400 | Missing ApiKey header, malformed body, validation failure, business rule violation. | Inspect message / errors. |
| 401 / 403 | Currently not used — the API key flow returns 400 on auth failure. | — |
| 404 | Resource not found (order, zone, merchant). | Verify ids. |
| 500 | Unexpected server error. | Retry with backoff; contact operations if persistent. |
9.3 Webhook receiver error handling on your side
- Respond 2xx fast even if you queue the work async. Treat the webhook handler like a message receiver, not a processor.
- Be idempotent: dedupe on
orderId+statusKey+eventTimestamp. - Tolerate unknown
statusKeyvalues — log and skip.
10. Security best practices
API key protection
- Store in a secrets manager, never in code or git history.
- Different key per environment (sandbox / production).
- Rotate on suspected leak or staff turnover. Contact ops to issue a new key and revoke the old one.
Webhook validation
- Always verify the configured
AuthorizationKey: AuthorizationValueheader on every inbound webhook. - Restrict your webhook endpoint to known egress IPs if your security model requires it.
- Use HTTPS only.
Replay protection
- Use the
eventTimestampand your stored last-seen timestamp per order to drop stale events. - Reject events older than your tolerance window (recommend 24h).
Timeout / availability
- Webhook handler must reply within 15 seconds. Queue heavy work, don't process inline.
- Build your handler to survive an Akwaan-side burst (status sweeps can fan-out 100s of events).
Recommended rate limits on your endpoint
- Allow at least 100 req/s sustained from Akwaan to absorb manifest-level transitions.
11. End-to-end integration flow
1. Onboarding
├─ Akwaan issues ApiKey + records your webhook URL & auth header
└─ You whitelist Akwaan in your firewall (optional)
2. Order creation
├─ POST /api/merchant/b2b/order ──→ Akwaan creates order
└─ Akwaan webhook: OrderStatusCreated (status=0, Pending)
3. Lifecycle
├─ Delegate picks up → webhook: Received (4)
├─ Arrives at warehouse → webhook: InWarehouse (6)
├─ Loaded for delivery → webhook: InDeliveryShipment (7)
├─ Out for delivery → webhook: InDeliveryProgress (8)
├─ OTP sent (if applicable) → webhook: OTPSend
└─ Delivered → webhook: Delivered (10)
4. Settlement
├─ Money reconciled → webhook: Completed (13)
└─ Wallet balance updated ← GET /api/merchant/b2b/wallets
5. Refund path (if needed)
├─ Failed delivery → webhook: RescheduledInWarehouse (14) etc.
├─ Refund manifest → webhook: InRefundShipment (27)
└─ Returned to merchant → webhook: RefundedToMerchant (29)
6. Error recovery
├─ Missed webhook? → GET /api/merchant/b2b/order/{externalId} (manual lookup)
└─ Suspect downtime? → contact operations for replay
12. Testing recommendations
Sandbox
Request a sandbox API key + a sandbox webhook URL from Akwaan operations. The sandbox mirrors production schemas but uses isolated data.
Webhook receiver testing
- Stand up a temporary webhook receiver locally:
- ngrok or webhook.site make this trivial.
- Configure the public URL with Akwaan as your sandbox webhook URL.
- Create test orders via
POST /api/merchant/b2b/order. - Trigger status transitions (Akwaan ops can step a sandbox order through the lifecycle).
- Verify your handler:
- Authenticates the header.
- Parses every payload shape.
- Is idempotent (replay the same event and assert no double-write).
- Returns 2xx within 15s.
- Failure cases to exercise:
- Your endpoint returns 500 → Akwaan retries up to 3 times.
- Your endpoint takes 30s → first attempt times out, retry.
- Your endpoint returns 401 → no retry (assumed permanent).
Load testing
In sandbox, ask operations to bulk-transition 1000+ orders. Verify your endpoint sustains the burst.
13. Edge cases & expected behavior
| Scenario | Behavior |
|---|---|
Status transition not in the broadcast list (e.g. InPickUpProgress, DelegateChanged). | No webhook sent. Document expected statuses on your side. |
| Same status fires twice in the same transaction. | Akwaan de-duplicates (orderId, eventType) within a single broadcast batch. Across batches, duplicates can still arrive — be idempotent. |
Order created without an IntegrationDetails record. | No webhooks sent for that order. (Indicates an ops misconfiguration — contact us.) |
| Merchant URL is malformed. | Logged and skipped; no retries. Fix in the merchant profile. |
| Merchant endpoint down for 3 attempts. | Event abandoned. Use the lookup endpoint to catch up. |
ExternalId is empty. | The webhook still fires but orderId will be null. Always set ExternalId on order creation. |
Webhook receives an unknown statusKey (future status). | Treat as informational. Log and ignore. Akwaan will document any new broadcastable statuses in the changelog (§15). |
Webhook receives a status numerically equal but with new statusKey semantics. | This will never happen — both status (int) and statusKey (string) are stable. |
14. Suggested future endpoints
These are NOT implemented yet — they are recommendations for the next integration milestone, prioritized by merchant impact:
| Endpoint | Purpose | Why merchants need it |
|---|---|---|
GET /api/merchant/b2b/order/{externalId} | Look up a single order by the merchant's external id. | Recovery after missed webhooks; manual status reconciliation. |
POST /api/merchant/b2b/orders/sync | Bulk lookup — given a list of externalIds, return current status for each. | Nightly reconciliation jobs. |
GET /api/merchant/b2b/orders/{id}/status-history | Full timeline of status transitions for one order. | Customer service, dispute resolution. |
POST /api/merchant/b2b/webhooks/replay | Ask Akwaan to re-fire webhooks for orders updated in a time window. | Disaster recovery after a merchant-side outage. |
GET /api/merchant/b2b/webhooks/failed | List webhook delivery attempts that failed after retries. | Visibility into integration health. |
POST /api/merchant/b2b/webhooks/test | Send a synthetic webhook payload to the configured URL. | Smoke-test the webhook listener during onboarding. |
GET /api/merchant/b2b/webhooks/health | Returns last-successful-delivery timestamp + recent failure count. | Dashboard / alerting. |
POST /api/merchant/b2b/webhooks/signature-rotate | Rotate the shared webhook secret without ops involvement. | Self-serve credential rotation. |
POST /api/merchant/b2b/order/{id}/cancel | Merchant-initiated cancellation. | Currently requires ops to cancel. |
15. Versioning & changelog
This document describes integration version v1.
Breaking changes (renamed fields, changed semantics of existing values, removed statuses) will be announced via email and a new major version (v2). Additive changes (new fields, new broadcastable statuses) will be released in minor version notes — your handler should already tolerate them.
Changelog
| Date | Change |
|---|---|
| 2026-05 | v1.1: Webhook payloads now include rich status descriptor (status, statusKey, statusNameEn, statusNameAr, eventTimestamp). The legacy Status string field is removed in favor of statusKey. Broadcast filter restricted to 20 explicit statuses (see §5). |
| 2026-05 | v1.0: Initial release. |
For credential issuance, integration support, or to report a webhook delivery issue, contact Akwaan operations.