Safe catalog
Sell products online in Ghana without holding inventory — syndicated catalog + order endpoints, paid by mobile money or card.
The Safe catalog service lets your app sell products online in Ghana without holding inventory: list products from vendors who've opted into syndication, and place orders against them — prepaid (mobile money or card through the platform's gateway) or cash on delivery. Full schemas are in the API Reference.
List syndicated products
GET /api/v1/safe-catalog/catalog.list — requires safe:read.
Lists products from vendors opted into syndication. Optionally scope to a single vendor_id,
capped at limit (default 50, max 200).
Get a single product
GET /api/v1/safe-catalog/catalog.get — requires safe:read.
Returns one product by product_id if it's active and its vendor is syndicatable. Otherwise
returns a uniform PRODUCT_NOT_FOUND.
Create an order
POST /api/v1/safe-catalog/orders.create — requires safe:write and a production API
key (this endpoint moves real money and real stock; see How money
works).
Single-vendor order creation: validates items, reserves stock, and quotes delivery. With
payment_method: "online" it starts a gateway charge and returns a checkout_url; with
payment_method: "cod" no charge is started — the rider collects the cash at the door (see
Cash on delivery).
{
"vendor_id": "vendor_abc",
"payment_method": "online",
"external_reference": "your-own-order-id-123",
"items": [
{ "product_id": "prod_123", "variant_id": "var_456", "quantity": 2 }
],
"buyer": {
"name": "Ama Owusu",
"phone": "0244000000"
},
"delivery": {
"address": "12 Ring Road, Accra",
"lat": 5.6037,
"lng": -0.187,
"area_id": "accra-central"
},
"reseller_markup_ghs": 0
}| Field | Required | Notes |
|---|---|---|
vendor_id | Yes | All items in one order must belong to this vendor — single-vendor orders only. |
payment_method | Yes | "online" (prepaid via the gateway checkout_url) or "cod" (cash on delivery — the buyer pays the rider subtotal + delivery fee at the door). Any other value is rejected with 400 VALIDATION_ERROR. |
external_reference | Yes | Your own idempotency key for this order, 3–64 characters. Reusing one that already succeeded returns 409 DUPLICATE_REFERENCE — call orders.get instead of retrying. |
items | Yes | Array of { product_id, variant_id, quantity }. Quantity must be a positive integer. |
buyer.name, buyer.phone | Yes | Who the order is for. |
delivery.address | Yes | Drop-off address used to quote the delivery fee. |
delivery.lat, delivery.lng, delivery.area_id | No | Improves delivery-fee accuracy when available. |
reseller_markup_ghs | No | Your markup on top of the vendor's price, 0 to the order subtotal. Defaults to 0. Not supported with "cod": a markup > 0 on a cash-on-delivery order is rejected with 400 MARKUP_NOT_SUPPORTED_FOR_COD — collect your markup from your buyer directly. |
A successful call returns 201 with sale_id, reference, checkout_url, and the
subtotal_ghs / delivery_fee_ghs / markup_ghs / total_ghs breakdown:
{
"status": "success",
"data": {
"sale_id": "order_abc123",
"reference": "DWO-000123",
"checkout_url": "https://checkout.example/...",
"subtotal_ghs": 120,
"delivery_fee_ghs": 15,
"markup_ghs": 0,
"total_ghs": 135
}
}Send the buyer to checkout_url to complete payment, then call orders.verify with the returned
sale_id.
Cash on delivery
Pass payment_method: "cod" to orders.create and the platform starts no charge at all:
checkout_url comes back null, the response carries state: "awaiting_delivery", and the rider
collects exactly total_ghs (subtotal + delivery fee) in cash at the door. Production API keys
only — COD moves real stock.
{
"status": "success",
"data": {
"sale_id": "order_abc123",
"reference": "SAFE-2607-000123",
"checkout_url": null,
"state": "awaiting_delivery",
"subtotal_ghs": 120,
"delivery_fee_ghs": 15,
"markup_ghs": 0,
"total_ghs": 135
}
}How COD differs from prepaid:
- No checkout step. There is nothing to redirect the buyer to — skip straight to polling.
orders.verifyverifies the DELIVERY, not a gateway. Poll it with thesale_id: it returnsstate: "awaiting_delivery"until the order is delivered, then settles it (state: "confirmed"). Like the prepaid path, verification is idempotent — repeat calls never settle twice.- Markup is rejected.
reseller_markup_ghs > 0returns400 MARKUP_NOT_SUPPORTED_FOR_COD; the rider only collects subtotal + delivery fee, so collect your markup from your own buyer. - Failed deliveries restock. If the delivery terminally fails (cancelled/failed),
orders.verifyreturnsstate: "failed", cancels the order, and puts the reserved stock back. Create a new order with a freshexternal_referenceif the buyer still wants it.
Verify + settle an order
POST /api/v1/safe-catalog/orders.verify — requires safe:write and a production API
key.
Verifies the order and, once settled, appends the sale's settlement decomposition to the ledger. Prepaid orders are verified against the payment gateway; cash-on-delivery orders are verified against the delivery (they settle once the order is delivered). This endpoint is idempotent — safe to call again if you're unsure whether the first verification succeeded.
{
"sale_id": "sale_abc123"
}Get order status
GET /api/v1/safe-catalog/orders.get — requires safe:read. Works on staging (read-only,
moves no money).
Read-only status check (payment + delivery state) for a syndicated order owned by the calling partner.
Typical flow
catalog.list/catalog.getto build your product surface.orders.createwhen the customer checks out — this returns asale_id(and, for prepaid orders, starts the gateway charge).orders.verifyto settle the order: for prepaid, once payment completes (from a redirect or webhook); for cash on delivery, poll until the order is delivered.orders.getto poll or display status afterward.
Wallet API — check balance & top up (mobile money)
How to check your Dawurobo delivery wallet balance and top it up with mobile money via the API, then verify the top-up before funding partner-paid delivery orders.
Delivery webhooks — payloads, signatures & retries
How Dawurobo delivers signed order webhooks to your app — event payload shape, HMAC signature verification, retries, and dead-lettering.