Dawurobo Partner Platform

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
}
FieldRequiredNotes
vendor_idYesAll items in one order must belong to this vendor — single-vendor orders only.
payment_methodYes"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_referenceYesYour own idempotency key for this order, 3–64 characters. Reusing one that already succeeded returns 409 DUPLICATE_REFERENCE — call orders.get instead of retrying.
itemsYesArray of { product_id, variant_id, quantity }. Quantity must be a positive integer.
buyer.name, buyer.phoneYesWho the order is for.
delivery.addressYesDrop-off address used to quote the delivery fee.
delivery.lat, delivery.lng, delivery.area_idNoImproves delivery-fee accuracy when available.
reseller_markup_ghsNoYour 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.verify verifies the DELIVERY, not a gateway. Poll it with the sale_id: it returns state: "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 > 0 returns 400 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.verify returns state: "failed", cancels the order, and puts the reserved stock back. Create a new order with a fresh external_reference if 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

  1. catalog.list / catalog.get to build your product surface.
  2. orders.create when the customer checks out — this returns a sale_id (and, for prepaid orders, starts the gateway charge).
  3. orders.verify to settle the order: for prepaid, once payment completes (from a redirect or webhook); for cash on delivery, poll until the order is delivered.
  4. orders.get to poll or display status afterward.