Skip to content

Developers

An API you can hold to its word.

REST over JSON, a generated OpenAPI document, integer money, idempotency keys on every write and signed webhooks on every status change.

Authentication

# Every request carries your key. Keys are scoped to one organisation
# and are shown once, at creation — we store only a hash.

curl https://api.ravendelivery.example/api/internal/wallet \
  -H "Authorization: Bearer $RAVEN_API_KEY"

Before you start

Four things the API assumes

Get these right and the rest follows from the OpenAPI document.

Money is never a decimal

Every amount on the wire is an object: an integer in minor units and a currency code. GHS 35.00 is {"amount_minor": 3500, "currency": "GHS"}. There are no floats anywhere in the API, because a rounding difference between your ledger and ours is not a bug anybody enjoys finding.

Idempotency on every write

Send an Idempotency-Key header on every POST. A repeat of the same key returns the original response rather than performing the action twice. Keys are scoped to your organisation and retained long enough to cover any sane retry window.

Errors have codes, not just prose

Failures return {"error": "insufficient_funds", "detail": "…"}. Branch on the code. The prose is written for humans and will be reworded.

Webhooks are signed and replayable

Each delivery carries an HMAC over the timestamp and the raw body. Verify it before parsing. Deliveries are retried with backoff, so your handler must be idempotent on the event id.

Worked example

Price a parcel from Accra to Kumasi

Every service that covers the route comes back as an offer; the ones that do not come back as a refusal with a reason, so you can show the customer why.

Request

curl -X POST https://api.ravendelivery.example/api/internal/quotes \
  -H "Authorization: Bearer $RAVEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "currency": "GHS",
        "origin":      { "country": "GH", "locality": "Accra",  "admin_area": "Greater Accra" },
        "destination": { "country": "GH", "locality": "Kumasi", "admin_area": "Ashanti" },
        "parcels": [{ "weight_grams": 2400, "length_mm": 300, "width_mm": 200, "height_mm": 150 }]
      }'

Response

{
  "offers": [
    {
      "service_code": "NEXT_DAY",
      "service_name": "Next-Day",
      "total": { "amount_minor": 5775, "currency": "GHS" },
      "transit_min_days": 1,
      "transit_max_days": 1,
      "distance_km": 248.4,
      "is_binding": true,
      "lines": [
        { "line_type": "BASE",      "code": "BASE", "description": "Base rate, regional band", 
          "amount": { "amount_minor": 5500, "currency": "GHS" } },
        { "line_type": "SURCHARGE", "code": "FUEL", "description": "Fuel surcharge (5%)",
          "amount": { "amount_minor": 275,  "currency": "GHS" } }
      ]
    }
  ],
  "refusals": [
    { "service_code": "SAME_DAY", "reason": "out_of_range",
      "detail": "248 km exceeds the 30 km same-day radius." }
  ]
}

Reference

Endpoints

Generated from the same OpenAPI document the server serves. Anything not listed here is not part of the contract.

Tracking

Public and unauthenticated. Rate limited by IP, and the response is a redacted projection — never the shipment record itself.

MethodPathSummaryAuth
GET/api/track/{tracking_number}Redacted public tracking snapshotPublic

Quotes

Rate shopping against your card. A quote is issued with an id and an expiry; booking against an expired quote is rejected rather than silently repriced.

MethodPathSummaryAuth
POST/api/internal/quotesPrice a route for every service that covers itAPI key or session

Shipments

Creating a shipment holds the money. Every write takes an Idempotency-Key, so a retried request returns the original result instead of creating a second parcel.

MethodPathSummaryAuth
GET/api/internal/shipments/List your shipmentsAPI key or session
POST/api/internal/shipments/Create a shipment from an accepted quoteAPI key or session
GET/api/internal/shipments/{id}/One shipment, with its legs and eventsAPI key or session
POST/api/internal/shipments/{id}/cancel/Cancel and release the held fundsAPI key or session

Wallet

Balance and top-ups. Every movement is a posting in a double-entry ledger, balanced per currency.

MethodPathSummaryAuth
GET/api/internal/walletCurrent balanceAPI key or session
POST/api/internal/walletRecord a top-upAPI key or session

Rider

Consumed by the rider application. Listed for completeness; these are not part of the merchant surface.

MethodPathSummaryAuth
GET/api/internal/rider/routesToday's assigned routeAPI key or session
POST/api/internal/rider/scansRecord a scanAPI key or session
POST/api/internal/rider/proof-of-deliveryCapture proof of deliveryAPI key or session
POST/api/internal/rider/locationReport locationAPI key or session

Webhooks

Told, not polled

Register an endpoint and we push every state change to it. Deliveries are retried with backoff and carry an event id, so your handler must be idempotent on that id.

  • shipment.created

    A shipment exists and funds are held.

  • shipment.status_changed

    Any movement through the state machine.

  • shipment.exception

    A failed attempt, a delay or damage, with a reason.

  • shipment.delivered

    Delivered, with the proof-of-delivery reference.

  • shipment.cancelled

    Cancelled, and the hold released.

  • wallet.low_balance

    The balance fell below your threshold.

Verifying a delivery

# Signature header, over `{timestamp}.{raw body}`
X-Raven-Signature: t=1774000000,v1=4f2c…

# Verify before parsing. Reject anything older than five minutes.
import hmac, hashlib

signed = f"{timestamp}.{raw_body.decode()}".encode()
expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received):
    raise Reject()

Sandbox keys are not self-service yet

There is no key-issuing console today. Ask us and we will create a sandbox organisation with a seeded rate card and a wallet balance, usually the same day. The sandbox uses the same code paths as production with the carrier adapters stubbed, so a booking there exercises the real state machine.