Webhooks
Verifying deliveries
Deliveries for a plain URL webhook (one with no target) carry a Terros-Signature header, so you can check that a request came from Terros and was not altered. Webhooks with no signing secret, including those added before signing existed, are not signed. Webhooks with a built-in target (CONNECT, ENERFLO, SERVICETITAN, SALESFORCE or JOBNIMBUS) are never signed.
Terros-Signature: t=1700000000,v1=38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789
tis the Unix time, in seconds, when the delivery was signed.v1is the hex HMAC-SHA256 of<t>.<raw request body>, keyed with the webhook's signing secret.
Retries and redrives are signed again, so each attempt has a fresh t.
To verify a delivery:
- Read the raw request body, before any JSON parsing.
- Compute the HMAC-SHA256 of
<t>.<raw body>with your signing secret. - Compare it to
v1in constant time. - Reject the request if
tis more than a few minutes old, to stop replays.
import { createHmac, timingSafeEqual } from 'node:crypto'
function isFromTerros(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')))
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
const sent = Buffer.from(parts.v1 ?? '')
const isSame = sent.length === expected.length && timingSafeEqual(sent, Buffer.from(expected))
return isSame && Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds
}
Signing secret
You choose each webhook's signing secret: a string of 16 to 256 characters, such as a long random value from a password manager. The secret is write-only. Terros stores it to sign deliveries and never returns it, so keep your own copy to verify with.
- Send
signingSecretto/webhook/addto sign a new webhook. Without one, the webhook is not signed. - Send
signingSecretto/webhook/updateto set or replace the secret, which is also how to start signing a webhook added without one. Omit it to keep the current secret. - In the Terros web app, enter the secret in the Signing secret field when you add or edit a plain URL webhook. Leave it blank when editing to keep the current secret. The field never shows the stored value.
- After you replace a secret, deliveries are signed with the new one as soon as the change takes effect, which can take up to 5 minutes, so accept both secrets during the switch.