Audience: External merchant engineering teams. Status: Production. Version: v1.1 Last updated: 2026-05.

1. Introduction

The Akwaan Express B2B Integration lets merchants:

Integration is bidirectional:

DirectionChannelTrigger
Merchant → AkwaanREST API call to /api/merchant/b2b/...The merchant initiates (e.g. creates an order).
Akwaan → MerchantHTTPS POST to merchant-configured webhook URLAkwaan initiates whenever an order status changes.

Supported capabilities

2. Architecture overview

+---------------------+         +---------------------------+
|  Merchant System    |  REST   |  Akwaan Express Backend   |
|                     +-------->+  /api/merchant/b2b/...    |
|                     |         |                           |
|  Webhook Listener   |<--------+  IntegrationService       |
|  (your URL)         |  POST   |  BroadcastOrderDetailsBulk|
+---------------------+         +---------------------------+

3. Authentication

3.1 API Key

All requests to /api/merchant/b2b/* MUST include:

HeaderValue
ApiKeyThe 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

ConditionHTTPResponse body
Header missing400{ "code": 400, "message": "API Key is missing" }
Header present but unknown / revoked400{ "code": 400, "message": "Invalid API key" }
Header present, merchant deleted400Same 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

4. Outbound webhook integration

4.1 Configuration

Provide Akwaan operations with:

FieldDescription
UrlYour 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.
HasOTPWhether your orders require customer OTP verification before delivery.

4.2 Lifecycle

  1. An order's status changes inside Akwaan (e.g. delegate marks Delivered).
  2. The status transition triggers IntegrationService.BroadcastOrderDetailsBulk.
  3. Events are batched by merchant — each merchant receives one HTTP POST carrying all the events that concern them.
  4. Events for non-broadcastable statuses are silently dropped (see §5).
  5. The HTTP POST is dispatched with up to 3 attempts, 15s timeout per attempt, linear backoff (2s, 4s).
  6. 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

ResponseRetried?
2xxNo (success).
408 Request TimeoutYes.
429 Too Many RequestsYes.
4xx (other)No (assumed permanent — fix your endpoint).
5xxYes.
Network error / DNS / TLS failureYes.
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

4.5 Webhook security

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)statusKeyEnglishArabicMeaning
0PendingPendingقيد الانتظارOrder created, awaiting pickup assignment.
1InPickUpShipmentIn Pickup Shipmentفي قائمة الاستلامAdded to a pickup manifest assigned to a delegate.
4ReceivedReceivedتم الاستلامPicked up from the merchant.
5NotReceivedNot Receivedلم يتم الاستلامPickup attempted but failed (merchant unavailable, refused, etc).
6InWarehouseIn Warehouseفي المخزنArrived at warehouse, awaiting delivery routing.
7InDeliveryShipmentIn Delivery Shipmentفي قائمة التسليمLoaded onto a delivery manifest.
8InDeliveryProgressIn Delivery Progressجاري التسليمDelegate is actively delivering.
10DeliveredDeliveredتم التسليمSuccessfully delivered to customer.
12CancelledCancelledملغيCancelled before completion.
13CompletedCompletedمكتملFinal accounting state — money settled.
14RescheduledInWarehouseRescheduled In Warehouseإعادة جدولة في المخزنFailed delivery; back to warehouse for re-routing.
15RescheduledDelegateRescheduled With Delegateإعادة جدولة مع المندوبFailed delivery; delegate will retry.
16RefundedInWarehouseRefunded In Warehouseمرتجع في المخزنRefund initiated; package at warehouse.
17RefundedDelegateRefunded With Delegateمرتجع مع المندوبRefund initiated; delegate carrying it.
19WarehouseTransferWarehouse Transferتحويل بين مخازنMoving between warehouses.
27InRefundShipmentIn Refund Shipmentفي قائمة الإرجاعOn a refund manifest heading back to merchant.
28InRefundProgressIn Refund Progressجاري الإرجاعRefund delegate actively delivering back to merchant.
29RefundedToMerchantRefunded To Merchantتم الإرجاع للتاجرRefund handed back to merchant.
30WarehouseTransferRefundWarehouse Transfer (Refund)تحويل بين مخازن (مرتجع)Refund package moving between warehouses.
31WarehouseTransferRefundDelegateWarehouse Transfer Refund Delegateتحويل مرتجع مع المندوبRefund package with a transfer delegate.

Stability contract:

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": { ... }
  }
]
FieldTypeRequiredDescription
EventTypestringyesOne of OrderStatusCreated, OrderStatusUpdated, OTPSend, OTPResend.
EventDataobjectyesEvent-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"
}
FieldTypeNullableDescription
orderIdstringyesThe merchant-provided ExternalId from order creation. Use this to correlate with your own DB.
statusintegernoNumeric value of OrderStatus. Stable.
statusKeystringnoEnum identifier. Stable.
statusNameEnstringnoEnglish display name. Display-only.
statusNameArstringnoArabic display name. Display-only.
eventTimestampISO 8601 UTCnoWhen 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:

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 bodyResponseAuth
None200 OK with array of NewWalletDtoApiKey header required

7.2 POST /order

Create a new order in Akwaan.

Request body — OrderCreationIntegrationForm:

FieldTypeRequiredNotes
CodestringyesYour internal code. Must be unique within your merchant.
PinstringnoSecondary 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.
CustomerIdstringnoYour internal customer id.
CustomerNamestringyes
CustomerPhoneNumberstringyes
CustomerSecondPhoneNumberstringno
ExternalIdstringyesThe id Akwaan will quote back to you in every webhook. Make it stable.
ContentstringyesDescription of package contents.
DeliveryZoneNamestringyesMust match an Akwaan zone name in the destination governorate.
PickupZoneNamestringyesMust match an Akwaan zone name in the source governorate.
PickUpGovernorateIdintyes
DeliveryGovernorateIdintyes
Notestringno
AmountdecimalyesThe 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.
NearestLandmarkstringno
SizeenumnoSmall (default), Medium, Large.
IsPaidboolnoSet 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).
PickUpLocationobjectno{ "lat": ..., "lng": ... }.
DeliveryLocationobjectno{ "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": [ ... ]
}
FieldTypeWhen present
codeintAlways (HTTP status mirror).
messagestringAlways.
dataTOn success.
paginationobjectOn paginated list responses.
errorsstring[]On validation failures.

9.2 Common error codes

HTTPCauseAction
400Missing ApiKey header, malformed body, validation failure, business rule violation.Inspect message / errors.
401 / 403Currently not used — the API key flow returns 400 on auth failure.
404Resource not found (order, zone, merchant).Verify ids.
500Unexpected server error.Retry with backoff; contact operations if persistent.

9.3 Webhook receiver error handling on your side

10. Security best practices

API key protection

Webhook validation

Replay protection

Timeout / availability

Recommended rate limits on your endpoint

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

  1. 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.
  2. Create test orders via POST /api/merchant/b2b/order.
  3. Trigger status transitions (Akwaan ops can step a sandbox order through the lifecycle).
  4. 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.
  5. 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

ScenarioBehavior
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:

EndpointPurposeWhy 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/syncBulk lookup — given a list of externalIds, return current status for each.Nightly reconciliation jobs.
GET /api/merchant/b2b/orders/{id}/status-historyFull timeline of status transitions for one order.Customer service, dispute resolution.
POST /api/merchant/b2b/webhooks/replayAsk Akwaan to re-fire webhooks for orders updated in a time window.Disaster recovery after a merchant-side outage.
GET /api/merchant/b2b/webhooks/failedList webhook delivery attempts that failed after retries.Visibility into integration health.
POST /api/merchant/b2b/webhooks/testSend a synthetic webhook payload to the configured URL.Smoke-test the webhook listener during onboarding.
GET /api/merchant/b2b/webhooks/healthReturns last-successful-delivery timestamp + recent failure count.Dashboard / alerting.
POST /api/merchant/b2b/webhooks/signature-rotateRotate the shared webhook secret without ops involvement.Self-serve credential rotation.
POST /api/merchant/b2b/order/{id}/cancelMerchant-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

DateChange
2026-05v1.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-05v1.0: Initial release.

For credential issuance, integration support, or to report a webhook delivery issue, contact Akwaan operations.