Guide to Upollo Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Add a webhook URL under Webhooks on the Access & Keys page (app.upollo.ai) |
| Signature header | Upollo-Signature, formatted t:<timestamp>,s0:<hash> |
| Signature scheme | s0 = HMAC-SHA512 over the raw body, keyed with the webhook secret |
| Encoding | Not documented (likely hex given the s0: prefix), verify against a live payload |
| Trigger | Fires on a risk flag (account sharing / multi-accounting), not a typed event |
| Payload | analysis (action e.g. CHALLENGE, flags[]), user/device info, short-form eventType (e.g. LOGIN) |
| SDK | upollo-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 exampleCHALLENGE), - a
flags[]array (withfirstFlagged/mostRecentlyFlagged), - user and device information,
- a short-form
eventType(for exampleLOGIN).
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 theflags[].
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-pythonwhere 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.