SuperSig.aiWebhooks

Webhooks

SuperSig.ai sends an HTTPS POST to your endpoint when something happens in your organization. Every request is signed so you can prove it came from us, and failed deliveries are retried for about 39 hours.

1. Register an endpoint

Create an endpoint with POST /api/v2/webhooks and the events you want (leave events out to get all of them). The response contains a secret starting with whsec_. It is shown once: store it with your other secrets.

curl -X POST https://www.supersig.ai/api/v2/webhooks \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://hooks.example.com/supersig","events":["campaign.created","click.tracked"]}'

Endpoint URLs must use https and point to a public host. Private, loopback and link-local addresses are refused, and we never follow redirects: a 3xx response counts as a failed attempt.

To check your receiver, POST /api/v2/webhooks/test with { "url", "secret" } sends one signed test.ping event (not retried).

2. Events

campaign.createdA banner campaign was created in the app or through the API.
campaign.updatedA campaign was edited, activated or paused.
signature.deployedA signature was pushed to a Gmail or Outlook mailbox.
click.trackedA recipient clicked a banner.
user.createdA user was added to the organization by SCIM, Microsoft 365 sync or provisioning.
test.completedAn A/B test ended, either automatically on a winner or by an admin.

3. What a delivery looks like

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: SuperSig-Webhooks/2.0
X-SuperSig-Event: campaign.created
X-SuperSig-Delivery: 6f1c2a9e-4b7d-4e0a-9d51-0c3f8f2b7a10
X-SuperSig-Timestamp: 1790704800
X-SuperSig-Signature: v1=<64 hex characters>
{
  "id": "6f1c2a9e-4b7d-4e0a-9d51-0c3f8f2b7a10",
  "event": "campaign.created",
  "timestamp": "2026-09-29T18:00:00.000Z",
  "data": {
    "campaign": {
      "id": "c2d3e4f5-0000-4000-8000-000000000001",
      "name": "Copilot Launch",
      "isActive": true,
      "priority": 1,
      "ctaUrl": "https://example.com/copilot"
    }
  }
}
  • id in the body equals the X-SuperSig-Delivery header and stays the same across retries. Use it to ignore duplicates.
  • timestamp in the body is when the event happened. X-SuperSig-Timestamp is when this attempt was sent (Unix seconds), and is the one that is signed.

4. Verify the signature

  1. Read the raw request body before any JSON parsing. Re-serialized JSON will not match.
  2. Reject the request if X-SuperSig-Timestamp is more than 5 minutes from your clock. This stops replays.
  3. Compute HMAC-SHA256 with your endpoint secret over <timestamp>.<raw body>, hex encoded.
  4. Compare it, in constant time, with each v1= value in X-SuperSig-Signature. Any match passes; there may be more than one during a secret rotation.

Node.js (Express)

import crypto from 'node:crypto'
import express from 'express'

const app = express()
const SECRET = process.env.SUPERSIG_WEBHOOK_SECRET // whsec_...

// Keep the raw body: the signature covers the exact bytes we sent.
app.post('/supersig', express.raw({ type: 'application/json' }), (req, res) => {
  const body = req.body.toString('utf8')
  const timestamp = req.get('X-SuperSig-Timestamp') ?? ''
  const header = req.get('X-SuperSig-Signature') ?? ''

  // 1. Reject old or future timestamps (replay protection).
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(400).send('stale timestamp')
  }

  // 2. Recompute HMAC-SHA256 over "<timestamp>.<body>" and compare in constant time.
  const expected = crypto.createHmac('sha256', SECRET).update(`${timestamp}.${body}`).digest('hex')
  const ok = header.split(',').some((part) => {
    const given = Buffer.from(part.trim().replace(/^v1=/, ''))
    return part.trim().startsWith('v1=') &&
      given.length === expected.length &&
      crypto.timingSafeEqual(given, Buffer.from(expected))
  })
  if (!ok) return res.status(400).send('bad signature')

  // 3. Drop duplicates: the same delivery id can arrive more than once.
  const event = JSON.parse(body)
  // if (alreadyProcessed(event.id)) return res.sendStatus(200)

  res.sendStatus(200) // respond fast; do slow work after
})

Python

import hashlib, hmac, time

def verify(body: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = timestamp.encode() + b"." + body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return any(
        part.strip().startswith("v1=") and hmac.compare_digest(part.strip()[3:], expected)
        for part in signature_header.split(",")
    )

5. Responses and retries

Reply with any 2xx status within 10 seconds. Anything else (another status, a timeout, a connection error) is a failed attempt, and we try again after:

retry 1: +1 minretry 2: +5 minretry 3: +30 minretry 4: +2 hretry 5: +12 hretry 6: +24 h

Each delay is measured from the previous attempt and is a minimum; retries go out on the next scheduler run after it. After 7 failed attempts the delivery stops and is kept as failed. Deliveries to a disabled endpoint stop right away. Deliveries can arrive out of order.