Gareth Wilson Gareth Wilson

Guide to Upollo Webhooks: Features and Best Practices

Published


Upollo webhooks notify your systems when a user is flagged for risk: account sharing, multi-accounting, or other fraud signals. If you're building fraud/abuse detection on Upollo, webhooks are how you react to risk flags without polling.

This guide covers how Upollo webhooks work, the risk-flag model, how to verify the Upollo-Signature (it's SHA-512), and the best practices for production.

What are Upollo webhooks?

Upollo webhooks are HTTP POSTs delivered to a URL you configure. They fire when a user is flagged for risk (account sharing, multi-accounting), this is a risk signal, not a discrete event-name subscription. Upollo signs deliveries with an Upollo-Signature header formatted t:<unix_timestamp>,s0:<hash>, where s0 is an HMAC computed with SHA-512 (not SHA-256) over the raw body.

Upollo webhook features

FeatureDetails
ConfigurationAdd a webhook URL under Webhooks on the Access & Keys page (app.upollo.ai)
Signature headerUpollo-Signature, formatted t:<timestamp>,s0:<hash>
Signature schemes0 = HMAC-SHA512 over the raw body, keyed with the webhook secret
EncodingNot documented (likely hex given the s0: prefix), verify against a live payload
TriggerFires on a risk flag (account sharing / multi-accounting), not a typed event
Payloadanalysis (action e.g. CHALLENGE, flags[]), user/device info, short-form eventType (e.g. LOGIN)
SDKupollo-python (pip) is live; npm @upollo/* do not currently resolve

The risk-flag model

Upollo doesn't have a discrete event catalog. A webhook fires when a user is flagged, and the payload carries an analysis object with:

  • an action (for example CHALLENGE),
  • a flags[] array (with firstFlagged / mostRecentlyFlagged),
  • user and device information,
  • a short-form eventType (for example LOGIN).

Branch on the analysis.action and the flags, not an event name. You can test with email suffixes like +account_sharing and +multiple_accounts.

Setting up Upollo webhooks

Add a webhook URL under Webhooks on the Access & Keys page at app.upollo.ai. A signing secret is created when you add the URL, store it for verification.

Securing Upollo webhooks

The Upollo-Signature header is t:<timestamp>,s0:<hash> (note the colon separators). To verify, compute an HMAC-SHA512 over the raw body with your webhook secret and compare against the s0 value. Verify against the raw body.

const crypto = require("crypto");

const SECRET = process.env.UPOLLO_WEBHOOK_SECRET;

function verify(rawBody, signatureHeader) {
  // Header format: "t:<timestamp>,s0:<hash>" (colon-separated pairs)
  const params = Object.fromEntries(
    (signatureHeader || "").split(",").map((p) => {
      const i = p.indexOf(":");
      return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    })
  );

  // s0 is HMAC-SHA512 over the raw body. Encoding isn't documented; default to
  // hex and confirm against a live payload (swap to "base64" if that matches).
  const expected = crypto.createHmac("sha512", SECRET).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(params.s0 || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.headers["upollo-signature"])) {
    return res.sendStatus(401);
  }

  res.sendStatus(200); // acknowledge fast
  processQueue.add(JSON.parse(req.body)); // branch on analysis.action, async
});

The same check in Python:

import hashlib
import hmac
import os

SECRET = os.environ["UPOLLO_WEBHOOK_SECRET"].encode()


def verify(raw_body: bytes, signature_header: str) -> bool:
    params = {}
    for part in (signature_header or "").split(","):
        if ":" in part:
            k, v = part.split(":", 1)
            params[k.strip()] = v.strip()
    expected = hmac.new(SECRET, raw_body, hashlib.sha512).hexdigest()  # confirm hex vs base64
    return hmac.compare_digest(expected, params.get("s0", ""))

Upollo webhook limitations and pain points

It's SHA-512, and the encoding isn't documented

The Problem: The s0 HMAC is SHA-512 (not SHA-256), and Upollo doesn't document whether the digest is hex or base64. A SHA-256 verifier, or the wrong encoding, fails.

Why It Happens: Upollo uses SHA-512 and doesn't pin the encoding.

Workarounds:

  • Use SHA-512, default to hex (given the s0: prefix), and confirm against a real payload, swapping to base64 if needed.

How Hookdeck Can Help: Hookdeck verifies the signature at the edge, so your app doesn't guess the algorithm or encoding.

It fires on a flag, not a typed event

The Problem: There's no event catalog; a webhook fires when a user is flagged. Handlers built to switch on event names have nothing to switch on.

Why It Happens: Upollo models risk as flags, not typed events.

Workarounds:

  • Branch on analysis.action (e.g. CHALLENGE) and the flags[].

How Hookdeck Can Help: Hookdeck's filters can route on the analysis fields, giving you action-based routing.

The colon-separated header format

The Problem: The header uses t:<ts>,s0:<hash> (colons, not =), which trips parsers written for Stripe-style t=,v1=.

Why It Happens: Upollo uses colon-separated pairs.

Workarounds:

  • Split on commas, then on the first colon per pair.

How Hookdeck Can Help: Hookdeck parses and verifies the header at the edge.

SDK availability

The Problem: The pip upollo-python is live, but the npm @upollo/web and @upollo/node don't currently resolve on the registry, so Node integrations roll their own verification.

Why It Happens: The npm packages appear unpublished.

Workarounds:

  • Use upollo-python where you can, and the manual HMAC-SHA512 check in Node.

How Hookdeck Can Help: Hookdeck verifies at the edge, so a missing SDK isn't a blocker.

Best practices

Verify the s0 HMAC-SHA512 over the raw body

Compute HMAC-SHA512 over the raw body with your webhook secret and compare against s0 in constant time; confirm hex vs base64 against a live payload.

Branch on the analysis, not an event name

Read analysis.action and flags[]; there's no typed-event system.

Acknowledge fast, process asynchronously

Return 200 quickly and defer work to a queue. See why to process webhooks asynchronously.

Test with the email suffixes

Use +account_sharing and +multiple_accounts email suffixes to trigger test flags.

Make Upollo webhooks production-ready

Hookdeck verifies Upollo-Signature, deduplicates, and durably queues every risk flag

Conclusion

Upollo webhooks fire on a risk flag (account sharing, multi-accounting), verified with an Upollo-Signature where s0 is an HMAC-SHA512 over the raw body (confirm hex vs base64 against a live payload). Parse the colon-separated header, verify SHA-512 over the raw body, branch on the analysis action, and acknowledge fast.

Hookdeck verifies the signature, deduplicates, and durably queues every event at the edge, so your app only ever processes verified risk flags.

Get started with Hookdeck for free and handle Upollo webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.