TeamGriddeveloper
TeamGrid Developer

Verify webhook signatures

Verify TeamGrid webhook v2 HMAC signatures over the exact raw request body in Node.js.

Capture the request body as raw bytes before any JSON parser runs. The SDK helper verifies the signature and timestamp before parsing JSON, compares the HMAC in constant time, and can claim the delivery ID through an atomic deduplication store.

import { verifyTeamGridWebhook } from '@teamgrid/api-client'

const rawBody = new Uint8Array(await request.arrayBuffer())
const delivery = await verifyTeamGridWebhook<MyEvent>({
  body: rawBody,
  headers: request.headers,
  signingSecret: process.env.TEAMGRID_WEBHOOK_SECRET!,
  deduplicationStore: {
    // This claim must be atomic: true for the first delivery ID, false thereafter.
    claim: (deliveryId, expiresAt) => deliveryIds.claim(deliveryId, expiresAt),
  },
})

await processEvent(delivery.payload)

The default timestamp window is five minutes and can be bounded with maxTimestampSkewSeconds. A duplicate claim, invalid metadata, stale timestamp, malformed JSON, or invalid signature throws TeamGridWebhookVerificationError with a stable code. Store the reveal-once signing secret separately from API credentials.

Express: preserve the raw body

Register the raw-body parser on the webhook route before any application-wide JSON parser. Parse the payload only after verification succeeds.

import express from 'express'
import { verifyTeamGridWebhook } from '@teamgrid/api-client'

const app = express()

app.post(
  '/webhooks/teamgrid',
  express.raw({ type: 'application/json', limit: '1mb' }),
  async (request, response, next) => {
    try {
      const delivery = await verifyTeamGridWebhook({
        body: new Uint8Array(request.body),
        headers: request.headers,
        signingSecret: process.env.TEAMGRID_WEBHOOK_SECRET!,
        deduplicationStore: deliveryIds,
      })

      await events.enqueue(delivery.payload)
      response.sendStatus(204)
    } catch (error) {
      next(error)
    }
  },
)

Do not call express.json() for this route before signature verification. Re-serializing a parsed object can change whitespace or property ordering and therefore changes the signed byte sequence.

Failure handling

Return a non-2xx response for an invalid signature, stale timestamp, or malformed envelope. For a valid delivery, persist or enqueue the event before returning 204; perform slow downstream work asynchronously. The deduplication claim must be atomic and retained for at least the delivery retry window. Log the stable verification error code and delivery identifier when available, but never log the signing secret or an unrestricted payload.

Stable documentation · Security · Reviewed 2026-07-29Edit this page ↗
Documentation feedbackWas this page useful?
Esc

Search TeamGrid Developer

Find guides, concepts and every API operation.