# Webhooks

Upvera can call your server when your startup's Upvera Value moves by at least your threshold. Webhooks are part of the Scale plan. Add up to three HTTPS endpoints in Upvera under **Connections › Value API**.

## Events

| Event | When |
|---|---|
| `value.threshold` | The value moved by at least your threshold (0.1% to 50%) since the last notification, in the direction you chose: `up`, `down` or `both`. Checked every 5 minutes. |
| `ping` | You added an endpoint or pressed **Send test**. |

The anchor moves on every threshold crossing in either direction, so you get one event per move, not a stream.

## Payload

```json
{
  "v": "upvera/webhook/v1",
  "id": "evt_…",
  "type": "value.threshold",
  "created_at": "2026-10-19T00:05:02Z",
  "data": {
    "startup": { "slug": "upvera-mthgaif7", "name": "Upvera", "url": "https://app.upvera.co/value/upvera-mthgaif7" },
    "value": 344.762011,
    "from": 341.213023,
    "change_pct": 1.04,
    "direction": "up",
    "threshold_pct": 1.0,
    "as_of": "2026-10-19T00:00:00Z",
    "from_at": "2026-10-18T21:00:00Z",
    "rule": "value@1"
  }
}
```

`change_pct` is computed by Upvera. Show it as it is.

## Headers

```text
Upvera-Signature: t=1760834702,v1=5f2b…
Upvera-Event: value.threshold
Upvera-Event-Id: evt_…
Upvera-Delivery-Attempt: 1
User-Agent: Upvera-Webhooks/1.0
```

## Verify the signature

`v1` is the hex HMAC-SHA256 of `t + "." + raw body`, keyed with your endpoint's secret (`whsec_…`). While you rotate a secret, the header carries two `v1` values: accept the request if either matches. Reject timestamps older than 5 minutes. Verify before you parse the body.

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyUpvera(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.groupBy(header.split(','), (p) => p.split('=')[0]);
  const t = Number(parts.t?.[0]?.split('=')[1]);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const want = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return (parts.v1 ?? []).some((p) => {
    const got = Buffer.from(p.slice(3), 'hex');
    return got.length === want.length && timingSafeEqual(got, want);
  });
}
```

## Delivery

- At least once. The same event can arrive twice: deduplicate on `id`, which is also the `Upvera-Event-Id` header.
- Any `2xx` acknowledges it. Anything else is retried with back-off from 1 minute to 6 hours, 12 attempts in about a day.
- HTTPS only, redirects are not followed, the response body is never read, and a delivery times out after 8 seconds.
- `GET /api/v1/me/deliveries` shows recent deliveries with their status, without bodies.

---
Source: https://upvera.dev/api/webhooks/ · Markdown: https://upvera.dev/api/webhooks.md
