Webhook Setup
Create webhooks in the dashboard and verify webhook signatures securely.
Create a Webhook
- Navigate to Settings > Webhooks.
- Click
Create Webhook. - Select the team and fill out the webhook URL.
- Toggle the status to
Activated. - Save Changes.
Only one webhook is allowed per team.

Vision Dashboard. Configuring webhooks in the interface.
You can also create webhooks with the API. This is useful when you create teams programmatically and want to configure each team's webhook as part of your provisioning flow. See the webhook creation API reference.
Secure Webhooks
If you provide a secret for your webhook, Truepic will include a truepic-signature header in each webhook activity that can be verified to ensure it is coming from Truepic. While this is completely optional, we recommend it for the increased security.
The truepic-signature looks like this:
truepic-signature: t=1634066973,s=6FBEiVZ8EO79dk5XllfnG18b83ZvLt2kdxcE8FJ/BwUThere is a t value that is the timestamp of when the request was sent, and an s value that is the signature of the request. The signature can be either auto-generated by Truepic, or provided by you.
Verify webhooks with the official Node.js library
If you're using Node.js, install the officially supported @truepic/webhook-verifier package and call it at the beginning of your webhook route. It verifies the integrity of the payload, the authenticity of the sender, the authenticity of the receiver, and the request timestamp to help prevent replay attacks.
See the @truepic/webhook-verifier package on npm for package details and additional documentation.
npm install @truepic/webhook-verifierimport verifyTruepicWebhook from '@truepic/webhook-verifier'
import express from 'express'
import { env } from 'node:process'
const app = express()
app.post(
'/truepic/webhook',
// Use the raw body so the verifier can compare the exact payload that was signed.
express.raw({
type: 'application/json',
}),
(req, res) => {
try {
verifyTruepicWebhook({
url: env.TRUEPIC_WEBHOOK_URL,
secret: env.TRUEPIC_WEBHOOK_SECRET,
header: req.header('truepic-signature'),
body: req.body.toString(),
})
} catch (error) {
console.warn(error)
return res.sendStatus(200)
}
const body = JSON.parse(req.body.toString())
console.log(`Processing webhook: ${body.type}`)
return res.sendStatus(200)
}
)If the webhook is verified, verifyTruepicWebhook returns true. If verification fails, it throws a TruepicWebhookVerifierError describing what failed.
If you're not using Node.js, use the manual verification flow below as a reference implementation.
Parse the Header Value
To start, split the header value on the comma (,). This should leave you with two parts:
t=1634066973s=6FBEiVZ8EO79dk5XllfnG18b83ZvLt2kdxcE8FJ/BwU
Next, split each part on the equals (=). This should leave you with two values for each part:
t1634066973s6FBEiVZ8EO79dk5XllfnG18b83ZvLt2kdxcE8FJ/BwU
Verify the Timestamp
The timestamp (t) can be verified to ensure it is a recent request and not a potentially delayed replay attack. Some leeway should be allowed in case the clocks on either end of the request are not quite in sync.
Here is our service with 5 minutes of leeway:
/* Services */
function verifyTimestamp({ timestamp, leewayMinutes = 5 }) {
const diff = Math.abs(Date.now() - timestamp * 1000)
const diffMinutes = Math.ceil(diff / (1000 * 60))
return leewayMinutes >= diffMinutes
}Verify the Signature
The signature (s) can be verified to ensure the recipient and payload have not been tampered with. This is done by rebuilding the signature – an HMAC digest using SHA-256 that is Base64-encoded – with a secret that only Vision and you are privy to.
To create the message to sign, join your webhook URL together with the timestamp (t) and the raw request body using a comma (,). It is important to use the raw request body (a string) before it is parsed as JSON, as different languages/frameworks can parse/stringify JSON in subtly different ways, which could result in different signatures.
Next, sign the {{url}},{{timestamp}},{{body}} message with the secret and compare the Base64-encoded signatures with a constant-time algorithm to prevent timing attacks.
Here is how that looks in our service that relies on Node.js’s crypto module for all of the heavy lifting:
/* Dependencies */
import { createHmac, timingSafeEqual } from 'crypto'
/* Services */
function verifySignature({ url, timestamp, body, secret, signature }) {
const comparisonSignature = createHmac('sha256', secret)
comparisonSignature.update([url, timestamp, body].join(','))
return timingSafeEqual(
Buffer.from(comparisonSignature.digest('base64'), 'base64'),
Buffer.from(signature, 'base64')
)
}Putting It All Together
Now that we have services to parse and verify the truepic-signature header, update the webhook route to put it all together:
/* Constants */
const WEBHOOK_URL = `http://localhost:${PORT}/webhook`
const WEBHOOK_SECRET = 'secret'
/* Routes */
app.post(
'/webhook',
express.raw({
type: 'application/json',
}),
(req, res, next) => {
const { timestamp, signature } = parseSignatureHeader(
req.header('truepic-signature')
)
const isTimestampVerified = verifyTimestamp({ timestamp })
if (!isTimestampVerified) {
console.warn('Invalid timestamp')
return res.sendStatus(200)
}
const isSignatureVerified = verifySignature({
url: WEBHOOK_URL,
timestamp,
body: req.body.toString(),
secret: WEBHOOK_SECRET,
signature,
})
if (!isSignatureVerified) {
console.warn('Invalid signature')
return res.sendStatus(200)
}
next()
},
(req, res) => {
const body = JSON.parse(req.body.toString())
console.log(`Processing webhook: ${body.type}`)
console.dir(body, { depth: null })
res.sendStatus(200)
}
)Updated 17 days ago
