Request signing
Every Brave gateway request is authenticated by three headers. The secret never travels over the network: only a signed HMAC digest is sent.
v2 scheme (security review #2, 2026-11-13): the signing key is derived from the plaintext secret (
sha256("sig-v2|" + secret)) — the secret hash stored in the database no longer suffices to sign. The signature covers the method, path, timestamp and body. The v1 scheme (flat digest of key + secret hash + timestamp) is rejected (401), and so is the legacy hash-based derivation for keys created since review #2.The three headers
- Name
X-Api-Key- Type
- string
- Description
Your public key (e.g.
VOTRE_CLE_API).
- Name
X-Timestamp- Type
- integer
- Description
Unix timestamp of the request, in seconds. Tolerance: ± 5 minutes.
- Name
X-Signature- Type
- string
- Description
Lowercase hexadecimal HMAC-SHA256 of the payload
METHOD \n PATH \n TIMESTAMP \n SHA256(BODY).
Computing the signature
signingKey = sha256("sig-v2|" + secret) # the PLAINTEXT secret, never sent
payload = UPPERCASE-method + "\n"
+ path + "\n" # as received, query included
+ timestamp + "\n"
+ sha256(raw body) # empty = empty string
signature = HMAC-SHA256(payload, signingKey)
signingKeyis derived from your plaintext secret (security review #2): thesha256(secret)fingerprint stored in the database cannot be used to recompute the signature. Keep the secret confidential.path: the path as the request is sent, query string included when present. Adding, removing or altering a query parameter after signing invalidates the request.raw body: the digest covers the body exactly as it goes over the wire. For a bodylessGET, hash the empty string. If you modify the JSON after signing (even one space), the signature no longer holds.- The timestamp is in seconds (not milliseconds), base 10, no padding.
- The signature is lowercase hexadecimal (header case does not matter on verification, but a signature that was already used is rejected: anti-replay).
- A signature cannot be replayed: every request must carry a different timestamp or a different body.
Calculation examples
API_KEY="VOTRE_CLE_API" # your public key
SECRET="VOTRE_SECRET" # your secret — never sent over the network
METHOD="GET"
PATH_="/v1/1000/ping" # path as sent, query included
BODY="" # exact raw body (empty for a bodyless GET)
SIGNING_KEY=$(printf '%s' "sig-v2|${SECRET}" | shasum -a 256 | cut -d' ' -f1)
BODY_HASH=$(printf '%s' "$BODY" | shasum -a 256 | cut -d' ' -f1)
TIMESTAMP=$(date +%s)
PAYLOAD=$(printf '%s\n%s\n%s\n%s' \
"$(printf '%s' "$METHOD" | tr '[:lower:]' '[:upper:]')" \
"$PATH_" "$TIMESTAMP" "$BODY_HASH")
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SIGNING_KEY" | sed 's/^.*= //')
curl -s "https://partners.bravebooking.net${PATH_}" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TIMESTAMP" \
-H "X-Signature: $SIGNATURE" \
-H "Accept: application/json"
Common pitfalls
- Body modified after signing — any change to the JSON (even a space) changes
sha256(body)→ 401. Sign the body exactly as it will be sent. - Query string forgotten —
pathincludes the query as sent: sign/v1/1000/bookings?from=…&to=…, not just/v1/1000/bookings. - Lowercase method — the method is normalized to uppercase in the payload (
getandGETyield the same signature). - Milliseconds instead of seconds — the timestamp is rejected as “too old” (
401). - Desynchronized clock — sync your server (NTP): the tolerance is 5 minutes.
- Replaying a request — each signature is accepted only once: in a script, space calls by 1 s (
sleep 1) or vary the body. - Legacy v1 scheme —
sha256(apiKey + secretHash + timestamp)is permanently rejected (401). - Legacy hash derivation —
sha256(sha256(secret) + "|brave-gateway-signing-v1")is no longer accepted for keys created since review #2: derive the key fromsha256("sig-v2|" + secret).
Authentication errors
| Code | Meaning |
|---|---|
401 missing_credentials | X-Api-Key, X-Timestamp or X-Signature header missing |
401 invalid_signature | Signature does not match the key, path, method or body |
401 stale_timestamp | Timestamp outside the 5-minute window |
429 | Too many authentication failures from this IP — retry after Retry-After seconds |
403 ip_not_allowed | Source IP not in the allow list |
403 key_revoked | Key revoked, or gateway/format not enabled for this key |
Error details: Errors.