Workspace & advanced setup

Verify and handle incoming webhooks

Authenticate signed dashboard events and shared-secret collection requests without mixing their formats.

Updated

On this page
  1. Choose the correct verification method
  2. Verify a signed request
  3. Node.js HMAC example
  4. Verify a collection request
  5. Accept quickly and avoid duplicate work

Choose the correct verification method

The current implementation has two delivery formats. Signed dashboard events include Socialjuice-Signature and Socialjuice-Timestamp. Collection-form requests use X-Webhook-Secret instead. Keep your space’s Signing secret only on the receiving server.

If a signature header is present, require valid HMAC verification. Do not fall back to accepting an invalid signed request as a legacy request. If you support the collection format, require its exact shared-secret header. Reject missing authentication.

Verify a signed request

Read the raw request body before JSON middleware changes it. The signature is HMAC SHA-256 of <timestamp>.<raw_request_body> with the Signing secret. Socialjuice-Signature contains t=<timestamp>,v1=<hex hmac>. Its timestamp must agree with Socialjuice-Timestamp.

Use a constant-time digest comparison. Reject malformed headers and timestamps more than five minutes in the past or future. Keep your server clock synchronized. After verification, parse JSON and distinguish webhook.test from testimonial.created.

Node.js HMAC example

This example verifies the signed format only. Supply the raw body as a Buffer before JSON parsing. It does not authenticate X-Webhook-Secret collection requests.

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

export function verifySocialjuice(rawBody, headers, secret) {
  if (!secret || !Buffer.isBuffer(rawBody)) return false;
  const timestamp = headers.get('socialjuice-timestamp') || '';
  const signature = headers.get('socialjuice-signature') || '';
  const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signature);
  if (!match || match[1] !== timestamp) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) ||
      Math.abs(Date.now() / 1000 - seconds) > 300) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(rawBody)
    .digest();
  const received = Buffer.from(match[2], 'hex');
  return expected.length === received.length &&
    timingSafeEqual(expected, received);
}

Verify a collection request

Read X-Webhook-Secret and compare it with the configured Signing secret on your server. Reject an empty header or a mismatch. Keep the endpoint on HTTPS and never log this header: its value is the secret itself.

This format does not include a signed timestamp or guaranteed delivery ID. It cannot use the HMAC example above for replay protection. Inspect the actual body, find the testimonial uuid where available, and deduplicate within the intended space. Payloads may be flat or contain a nested testimonial object.

Accept quickly and avoid duplicate work

Only process authenticated requests. For signed events, deduplicate with Socialjuice-Delivery or the envelope id. For collection requests, use a stable testimonial identifier when available and keep the workspace context. Store deduplication records persistently if several server instances receive events.

Return a 2xx response promptly after safely accepting a request and queue slower work. Treat webhook.test as a connection test, not a real testimonial. A test passing does not prove your collection payload mapping is correct.

Still need a hand?

Tell us what you are trying to do. We are here to help.

Contact support