# 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 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.

```javascript
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:

```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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Test with the email suffixes

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

## 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle Upollo webhooks reliably in minutes.