Webhooks

Webhooks allow you to receive verification results asynchronously. When a verification completes — whether synchronously via the API or as part of a batch job — SignalStack sends an HTTP POST request to your configured endpoint.

Event types

EventDescription
verification.completedA verification request has finished processing
verification.failedA verification request encountered an unrecoverable error
batch.completedAll items in a batch verification have finished
batch.partialSome items in a batch succeeded, some failed
credit.thresholdYour account has crossed a usage threshold (75%, 90%, 100%)

Webhook payload format

Code
{
  "id": "wh_9f8e7d6c5b4a",
  "type": "verification.completed",
  "created_at": "2025-01-25T14:30:00Z",
  "data": {
    "request_id": "vrf_a1b2c3d4e5f6",
    "request_type": "claim",
    "trust_score": 0.97,
    "verdict": "true",
    "evidence": [
      {
        "source": "Wikipedia",
        "url": "https://en.wikipedia.org/wiki/Example",
        "supports_claim": true,
        "authority_score": 0.92
      }
    ],
    "latency_ms": 187
  }
}

HMAC signature verification

Every webhook request includes a X-SignalStack-Signature header containing an HMAC-SHA256 signature of the raw request body, keyed with your webhook secret. Always verify this signature before processing the payload.

Python verification

Code
import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

# In your webhook handler:
@app.post("/webhooks/signalstack")
async def handle_webhook(request):
    payload = await request.body()
    signature = request.headers.get("X-SignalStack-Signature")

    if not verify_webhook(payload, signature, WEBHOOK_SECRET):
        return {"error": "Invalid signature"}, 401

    event = json.loads(payload)
    # Process event...

TypeScript verification

Code
import { createHmac, timingSafeEqual } from "node:crypto"

function verifyWebhook(
  payload: Buffer,
  signature: string,
  secret: string,
): boolean {
  const expected = createHmac("sha256", secret)
    .update(payload)
    .digest("hex")
  const expectedSig = `sha256=${expected}`
  try {
    return timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSig),
    )
  } catch {
    return false
  }
}

Retry logic

SignalStack retries failed webhook deliveries up to 5 times with exponential backoff:

AttemptDelay
110 seconds
21 minute
310 minutes
41 hour
56 hours

Your endpoint must respond with a 2xx status within 10 seconds for the delivery to be considered successful. Any other status code, a timeout, or a network error triggers the next retry attempt. After all 5 attempts fail, the event is discarded. Webhook deliveries can be replayed from the dashboard.

Rate of delivery

To protect your server, SignalStack delivers webhooks at a maximum rate of 10 events per second per endpoint. If your endpoint responds slowly, the delivery rate is automatically throttled.

Configuration

Webhooks are configured through the dashboard or via the API:

Code
# Configure webhook via API
curl -X POST https://signal-stack-ten.vercel.app/v1/webhooks   -H "Authorization: Bearer $SIGNALSTACK_API_KEY"   -H "Content-Type: application/json"   -d '{
    "url": "https://api.myapp.com/webhooks/signalstack",
    "events": ["verification.completed", "verification.failed"],
    "secret": "whsec_your_secret_here"
  }'
FieldRequiredDescription
urlYesHTTPS endpoint that receives webhook POST requests
eventsYesArray of event types to subscribe to
secretRecommendedUsed for HMAC signature generation; must be at least 32 characters

Testing webhooks

Use the SignalStack dashboard to send test events to your endpoint. You can also use tools likengrok to expose your local server during development.

Best practices

  • Always verify the HMAC signature before processing
  • Respond with 2xx quickly (within 10s) — process the event asynchronously
  • Idempotent processing: use the id field to deduplicate events
  • Store the raw payload before any transformation for replay/debugging
  • Rotate your webhook secret periodically

Next steps