Webhook Security

Authentication

Upward’s webhooks are secured using a Hashed Message Authentication Code (HMAC) in the webhook message header. The name of this header value is Upwardli-Signature. The Upwardli-Signature header contains two comma-separated key-value pairs encoding information about the request. The first key-value pair will be in the form t=<unix_timestamp> and represents the unix time that the request was sent. The second key-value pair will be in the form v1=WeNeedSomethingHere, where the signature is a sha256 hash computed from the consumers webhook secret and a dot-separated string composed of the unix timestamp joined with the request body.

Note: computing the signature is sensitive to the exact characters input into the algortihm. The request data should be a json string with no whitespace formatting.

Sample Data

client_id => 'public'
signature => 't=2023-10-12T20:44:58.082694+00:00,v1=263a5f79d899f7d5e04eb9a902b173d5901a9088966b932edca7174aec3d9e12'
message => '{"id":"954935cb-be33-47a4-99af-ec8bbc662ec7","createdAt":"2023-10-05T17:39:21.097794+00:00","eventName":"consumer_created","partnerId":"cb739356-5f69-429c-8157-756876d08d27","resources":["api/v2/consumers/00000000-0000-0000-0000-000000000000"],"lastAttemptedAt":"2023-10-05T17:39:21.097794+00:00"}'

Example decode in Python

1t, v1 = [value.split('=')[1] for value in request.headers['Upwardli-Signature'].split(',')]
2computed_digest = hmac.new(<YOUR_CLIENT_ID>.encode(), (t + '.' + request.data.decode('utf-8')).encode(), 'sha256').hexdigest()
3
4if hmac.compare_digest(v1, computed_digest):
5 # Process webhook

Example decode in Node.JS

1const header = "<Upwardli-Signature>"
2
3const bodyRaw = `{
4 "id": "e1bc276c-1705-431b-98ad-0c63d4502c35",
5 "created_at": "2025-10-29T15:25:33.980841+00:00",
6 "event_name": "consumer.created",
7 "partner_id": "cb739356-5f69-429c-8157-756876d08d27",
8 "resources": [
9 "https://api-sandbox.upwardli.com/v2/consumer/00000000-0000-0000-0000-000000000000"
10 ],
11 "last_attempted_at": "2025-10-29T15:25:33.980841+00:00"
12}`
13const body = JSON.stringify(JSON.parse(bodyRaw))
14
15const CLIENT_ID = "<YOUR_CLIENT_ID>"
16
17const [t, v1] = header.split(",").map((p) => p.split("=")[1])
18
19const computed = crypto
20 .createHmac("sha256", CLIENT_ID)
21 .update(`${t}.${body}`)
22 .digest("hex")
23
24const v1Buffer = Buffer.from(v1, "hex")
25const computedBuffer = Buffer.from(computed, "hex")
26
27if (crypto.timingSafeEqual(v1Buffer, computedBuffer)) {
28 console.log("Valid signature")
29} else {
30 console.log("Invalid signature")
31}