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:
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your app's API key |
X-Signature | Yes | HMAC-SHA256 signature of the canonical request |
X-Timestamp | Yes | Unix timestamp in seconds |
X-Nonce | Yes | A 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\nNONCEWhere:
METHOD— uppercase HTTP method (GET,POST, …)PATHNAME— the exact request path, e.g./api/v1/delivery/wallet.topup.initiateQUERY— the exact query string including?, or an empty string if there is noneSHA256_BODY— lowercase hex SHA256 of the raw request body (empty string hashed for bodyless requests)TIMESTAMP— the same value sent inX-TimestampNONCE— the same value sent inX-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("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855Signing 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.