waymark.networkguides → uber-direct-webhook-signature-verification

Verify Uber Direct webhook signatures

Uber Direct pushes delivery-status and courier-location updates to your endpoint as signed POSTs, with the signature in an x-uber-signature header. The HMAC math is standard; what breaks integrations is everything around it — which key to use, which header to trust, what bytes to hash, and a retry policy that silently drops events your handler rejects. These are the documented patterns and failure modes from Waymark's live route corpus, checked against Uber's official webhook documentation.

Sourced from 2 live route records · regenerated 2026-07-12

Part of the webhook verification series — also see: Stripe · Plaid · DocuSign · Typeform · Attio

The key that isn't your OAuth secret

The single most common dead end: signing verification that fails for every event because the wrong secret is in the HMAC. Uber Direct issues a dedicated Webhook Signing Key per webhook when you create it in the dashboard — it lives under the Developer → Webhooks tab (open the webhook entry's menu → Edit), not in the API-credentials section where your OAuth client ID and secret live. Compute the HMAC with your OAuth client secret and every signature check fails, for legitimate and forged requests alike, with nothing in the response to tell you which secret was wrong.

Documented patterns

Verify Uber Direct webhook signature to authenticate delivery status callbacks

developer.uber.comsampled

Verify Uber Direct webhook signatures and handle exponential-backoff retries

developer.uber.comverified

The verification procedure

  1. Create the webhook in the Uber Direct Dashboard — Developer tab → Webhooks → Create Webhook; pick the event types (event.delivery_status, event.courier_update) and give it an HTTPS endpoint (HTTP is not accepted)
  2. Retrieve that webhook's dedicated signing key ( → Edit on the webhook entry)
  3. In the handler, capture the raw unparsed request bytes before any JSON-parsing middleware runs
  4. Compute HMAC-SHA256 over the raw bytes with the signing key and hex-encode the digest — Uber's own example uses Python's hexdigest(), i.e. lowercase hex, not base64
  5. Compare against x-uber-signature with a constant-time comparison; reject on missing or mismatched signature, return 200 fast, process asynchronously, and dedupe on delivery id + event kind
// Express — capture raw bytes with express.raw(), not express.json()
const crypto = require('crypto');
app.use('/uber-webhook', express.raw({ type: 'application/json' }));

app.post('/uber-webhook', (req, res) => {
  const received = req.headers['x-uber-signature']; // headers arrive lowercased
  if (!received) return res.status(401).send('missing signature');

  const expected = crypto.createHmac('sha256', process.env.UBER_WEBHOOK_SIGNING_KEY)
    .update(req.body)   // raw Buffer — NOT a parsed/re-stringified object
    .digest('hex');     // hex, not base64

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  if (!valid) return res.status(401).send('invalid signature');

  const event = JSON.parse(req.body); // safe to parse now that it's verified
  res.status(200).send('ok');
  // hand event off to a queue — dedupe on delivery id + kind (retries + out-of-order both happen)
});

Two headers, one of them legacy

Delivery Status and Courier Update webhooks carry the signature in both x-uber-signature and x-postmates-signature — a leftover from the Postmates acquisition — and Uber's docs say either is valid for verification on those event types. The Refund webhook is the exception: verify it against x-uber-signature only. The safe default is to standardize on x-uber-signature everywhere and never reference the Postmates header in new code; a handler that only reads x-postmates-signature breaks on refund events.

The \u0026 trap: raw bytes or nothing

Uber signs the exact bytes it sends — and its payloads can contain Unicode escape sequences like \u0026 (the escaped form of &). Uber's docs warn about this explicitly: if anything in your stack parses the body and re-serializes it, escape sequences may be converted to their literal characters, the byte sequence changes, and the digest you compute no longer matches the header — for perfectly legitimate events. This is the same raw-body rule as every HMAC webhook (see the cross-provider guide), with an Uber-specific twist: even a "raw" capture that round-trips through a lenient JSON layer can mutate escapes. Hash the bytes as they arrived on the wire; parse a copy afterwards. Test with a payload containing \uXXXX sequences before shipping.

Your 400 is a black hole: the retry budget

Uber retries a webhook only on 500, 502, 503, 504, a timeout, or a network error. Respond with anything else — a 400 from a validation bug, a 401 from a misconfigured signature check — and that event is gone; there is no retry and no replay console. And the retry budget for genuine failures is small: per Uber's docs, the first retry comes 10 seconds after the initial event, subsequent retries back off exponentially (30s → 60s → 120s), and delivery stops once a response is received or after 3 total unanswered sends. The practical consequences: a signature check that's broken for more than a few minutes loses events permanently, so treat webhooks as best-effort and reconcile by polling the Get Delivery endpoint for any in-flight delivery whose status has gone quiet. Retries also mean duplicates and out-of-order arrival — dedupe on delivery id + event kind, and trust the status field in the payload over arrival order.

Courier updates are a firehose

event.courier_update fires with the courier's live location — often every few seconds while a delivery is in transit. A handler that verifies, writes to a database, and calls a downstream API synchronously per event will start timing out exactly when it matters (active deliveries), and timeouts burn through the 3-send retry budget above. Verify the signature, acknowledge with 200 immediately, and push the event to a queue; if you only need delivery milestones, subscribe that endpoint to event.delivery_status only and skip the firehose entirely.

How agents avoid this automatically

Agents wired to Waymark's MCP server pull these routes (and their gotchas — including the signing-key-vs-OAuth-secret trap) at plan time instead of rediscovering them against a live delivery pipeline. Reads are free and keyless.

# Any MCP client — one call, no key:
waymark_query({ "task": "uber direct webhook signature verification" })

# Or plain HTTP:
curl -s "https://mcp.waymark.network/search?q=uber%20direct%20webhook%20signature%20verification"
Wire up waymark_query — free → Need a route for your exact stack? $25, delivered in 24h → For teams: verify your whole stack — Pilot $750/mo →