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.

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)
  • signingKey is derived from your plaintext secret (security review #2): the sha256(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 bodyless GET, 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 — path includes 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 (get and GET yield 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 from sha256("sig-v2|" + secret).

Authentication errors

CodeMeaning
401 missing_credentialsX-Api-Key, X-Timestamp or X-Signature header missing
401 invalid_signatureSignature does not match the key, path, method or body
401 stale_timestampTimestamp outside the 5-minute window
429Too many authentication failures from this IP — retry after Retry-After seconds
403 ip_not_allowedSource IP not in the allow list
403 key_revokedKey revoked, or gateway/format not enabled for this key

Error details: Errors.

Was this page helpful?