Dawurobo Partner Platform

Sign Dawurobo API requests — auth & HMAC

How to authenticate and sign Dawurobo Partner Platform API requests — required headers, the canonical request string, HMAC-SHA256 signatures, nonces, and scopes.

Required headers

Every request to a partner-platform endpoint requires:

HeaderRequiredDescription
X-API-KeyYesYour app's API key
X-SignatureYesHMAC-SHA256 signature of the canonical request
X-TimestampYesUnix timestamp in seconds
X-NonceYesA unique value per request (UUID recommended)

Authorization: Bearer <api_key> is accepted as a fallback for X-API-Key, but X-API-Key is preferred.

Signature algorithm

Build a canonical string and sign it with HMAC-SHA256 using your key's signing secret:

METHOD\nPATHNAME\nQUERY\nSHA256_BODY\nTIMESTAMP\nNONCE

Where:

  • METHOD — uppercase HTTP method (GET, POST, …)
  • PATHNAME — the exact request path, e.g. /api/v1/delivery/wallet.topup.initiate
  • QUERY — the exact query string including ?, or an empty string if there is none
  • SHA256_BODY — lowercase hex SHA256 of the raw request body (empty string hashed for bodyless requests)
  • TIMESTAMP — the same value sent in X-Timestamp
  • NONCE — the same value sent in X-Nonce

Requests are rejected as stale if X-Timestamp is more than 5 minutes away from server time — keep your clocks NTP-synced.

Sign the exact bytes you send

The signature is computed over the raw body string and raw query string — not a re-derived or re-serialized version of them. If you JSON.stringify an object to build SHA256_BODY, send that exact string as your request body; if some other layer in your HTTP client re-serializes the object before sending (different key order, different whitespace), the bytes on the wire won't match what you signed and the server returns 401 INVALID_SIGNATURE. The safest pattern: build the body string once, sign it, then send that string directly (not the object).

The same applies to the query string: QUERY must byte-match what's actually sent on the wire, including the leading ? and using the same parameter order and URL-encoding you send with. If there's no query string, sign an empty string — don't sign a bare ?.

For a bodyless request (e.g. GET), SHA256_BODY is the SHA256 hex digest of the empty string, which is always the same constant:

sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Signing example (Node.js)

import crypto from "crypto";

function sha256Hex(value: string): string {
  return crypto.createHash("sha256").update(value, "utf8").digest("hex");
}

function buildSignature(args: {
  method: string;
  pathname: string;
  query: string;
  body: string;
  timestamp: string;
  nonce: string;
  signingSecret: string;
}) {
  const canonical = [
    args.method.toUpperCase(),
    args.pathname,
    args.query || "",
    sha256Hex(args.body || ""),
    args.timestamp,
    args.nonce,
  ].join("\n");

  return crypto.createHmac("sha256", args.signingSecret).update(canonical, "utf8").digest("hex");
}

Sign with the signing secret (sk_...) issued alongside your API key — not the API key itself.

Make your first call

The delivery/health endpoint needs only the delivery:read scope (every key has it, including sandbox keys), works on staging, and moves no money — it's the easiest way to prove your signing implementation is correct before you build anything real on top of it.

Node.js

import crypto from "crypto";

// Same base URL for staging and production — your API key's environment decides which one you hit.
// Delivery: https://delivery.dawurobo.com  ·  Safe catalog: https://safe.dawurobo.com
const BASE_URL = process.env.DAWUROBO_BASE_URL ?? "https://delivery.dawurobo.com";
const API_KEY = process.env.DAWUROBO_API_KEY!;
const SIGNING_SECRET = process.env.DAWUROBO_SIGNING_SECRET!;

function sha256Hex(value: string): string {
  return crypto.createHash("sha256").update(value, "utf8").digest("hex");
}

function buildSignature(args: {
  method: string;
  pathname: string;
  query: string;
  body: string;
  timestamp: string;
  nonce: string;
  signingSecret: string;
}) {
  const canonical = [
    args.method.toUpperCase(),
    args.pathname,
    args.query || "",
    sha256Hex(args.body || ""),
    args.timestamp,
    args.nonce,
  ].join("\n");

  return crypto.createHmac("sha256", args.signingSecret).update(canonical, "utf8").digest("hex");
}

async function main() {
  const method = "GET";
  const pathname = "/api/v1/delivery/health";
  const query = ""; // no query string on this call
  const body = ""; // GET has no body
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomUUID();

  const signature = buildSignature({ method, pathname, query, body, timestamp, nonce, signingSecret: SIGNING_SECRET });

  const response = await fetch(`${BASE_URL}${pathname}${query}`, {
    method,
    headers: {
      "X-API-Key": API_KEY,
      "X-Signature": signature,
      "X-Timestamp": timestamp,
      "X-Nonce": nonce,
    },
  });

  console.log(response.status, await response.json());
}

main();

Python

import hashlib
import hmac
import os
import time
import uuid

import requests

# Same base URL for staging and production — your API key's environment decides which one you hit.
# Delivery: https://delivery.dawurobo.com  ·  Safe catalog: https://safe.dawurobo.com
BASE_URL = os.environ.get("DAWUROBO_BASE_URL", "https://delivery.dawurobo.com")
API_KEY = os.environ["DAWUROBO_API_KEY"]
SIGNING_SECRET = os.environ["DAWUROBO_SIGNING_SECRET"]


def sha256_hex(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()


def build_signature(method, pathname, query, body, timestamp, nonce, signing_secret):
    canonical = "\n".join([
        method.upper(),
        pathname,
        query or "",
        sha256_hex(body or ""),
        timestamp,
        nonce,
    ])
    return hmac.new(
        signing_secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256
    ).hexdigest()


method = "GET"
pathname = "/api/v1/delivery/health"
query = ""  # no query string on this call
body = ""  # GET has no body
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())

signature = build_signature(method, pathname, query, body, timestamp, nonce, SIGNING_SECRET)

response = requests.get(
    f"{BASE_URL}{pathname}{query}",
    headers={
        "X-API-Key": API_KEY,
        "X-Signature": signature,
        "X-Timestamp": timestamp,
        "X-Nonce": nonce,
    },
)

print(response.status_code, response.json())

cURL

cURL can't compute an HMAC-SHA256 signature inline in a single portable command, so treat this as a template — compute X-Signature, X-Timestamp, and X-Nonce with the Node.js or Python snippet above (or any HMAC-SHA256 tool) and paste the values in:

curl "https://delivery.dawurobo.com/api/v1/delivery/health" \
  -H "X-API-Key: $DAWUROBO_API_KEY" \
  -H "X-Signature: <hex HMAC-SHA256 from the Node.js/Python example above>" \
  -H "X-Timestamp: <unix seconds, matches what you signed>" \
  -H "X-Nonce: <fresh UUID, matches what you signed>"

A successful call returns 200 with a small JSON status body.

Scopes

Each operation requires a scope, returned in the API Reference as x-required-scope. Scopes are namespaced by service:

  • delivery:read, delivery:write, delivery:wallet:topup, delivery:wallet:spend, delivery:*
  • safe:read, safe:write, safe:*

A wildcard scope (delivery:*, or the global *) covers the reads and standard writes under its service — but never the money-moving scopes. delivery:wallet:topup, delivery:wallet:spend, and safe:write are exact-only: they must be granted to your key by name, and no wildcard reaches them. That's deliberate — a broad key can't silently top up, drain a wallet, or charge a buyer.

Replay protection and rate limits

  • Nonces are tracked per key — reusing a nonce returns 409 REPLAY_DETECTED.
  • Rate limits are enforced per key, per endpoint class: 120 req/min for reads, 60 req/min for writes. Exceeding the limit returns 429 RATE_LIMITED.

See Troubleshooting for the full error code reference.