Skip to main content
Every webhook subscription in Raise carries a securityToken — a shared secret between Raise and the partner endpoint. Raise uses this token to sign outgoing webhook deliveries; the partner endpoint verifies the signature on every incoming request to confirm the request actually came from Raise rather than from a forger. Signature verification is not optional. Without it, any party that learns the partner’s webhook URL can deliver forged events to the integration — and the partner endpoint has no way to know the difference. This page covers the verification pattern, the practical defenses, and what’s known about the exact algorithm.
Verify the signature on every incoming webhook request — before processing, before logging the payload, before any side effects. An endpoint that processes unverified requests is open to forged events that could create duplicate gifts, send unwanted emails, sync bad data, or trigger any other side effect the integration takes on event arrival.

The signing model

The mechanism follows the standard webhook signature pattern used by Stripe, GitHub, and most other major API platforms: The two sides hold the same shared secret. Raise computes a signature over the request body using the secret; the partner computes the same signature using its copy of the secret. If both signatures match, the partner knows the request came from Raise. If the partner’s recomputed signature doesn’t match what Raise sent, the request didn’t come from Raise — or the request body was tampered with in transit. Either way, reject it.

What the spec confirms

The Raise OpenAPI spec is explicit about one thing: the WebhookRequest carries a securityToken field that the partner sets when creating or updating a subscription. This is the shared secret.
{
  "name": "Partner integration",
  "notificationUrl": "https://partner.example.com/raise-webhooks",
  "eventTypesList": [10, 11],
  "format": 1,
  "status": 1,
  "securityToken": "your-randomly-generated-secret-here"
}
What the spec does not document:
ConcernDocumented?
Where the signature is sent in the HTTP request (header name)No
The hashing algorithm (HMAC-SHA256? SHA1?)No
The exact bytes that are signed (body? body + timestamp? canonicalized body?)No
The signature encoding (hex? base64?)No
Whether a timestamp is included for replay protectionNo
⚠️ Spec gap: The Raise OpenAPI spec doesn’t document the exact signature algorithm or the header name that carries the signature. The patterns on this page describe the general HMAC-based approach that most webhook systems use and that the securityToken field strongly suggests Raise uses; partners building production integrations should confirm the exact algorithm and header name against the live API or with the platform team before deployment.Until the spec is updated, the practical approach is to inspect a few real webhook deliveries (via the webhook log endpoints, or by capturing a request at the partner endpoint) to identify the signature header and confirm the algorithm.

Generating a strong security token

The strength of signature verification is bounded by the strength of the secret. A predictable or short secret can be guessed; a strong secret cannot. Generate the securityToken with a cryptographically secure random source — at least 32 bytes of entropy:
JavaScript
import crypto from 'crypto';

function generateSecurityToken() {
  // 32 bytes = 256 bits of entropy
  return crypto.randomBytes(32).toString('base64');
}

const token = generateSecurityToken();
// Example: 'dHi2bH...P3oP4=' (base64-encoded random bytes)
The format (base64, hex, or any other encoding) doesn’t matter as long as the underlying entropy is strong. What matters:
Don’t useUse instead
A human-chosen password (MyWebhookSecret123!)Cryptographically random bytes
A short string (less than 16 bytes of entropy)At least 32 bytes
The same token across all customersOne token per customer subscription
The same token across the partner’s API keyA separate token specific to the webhook
Store the token in your secrets manager. The partner side will read it on every incoming request to verify signatures.

The verification pattern

The general verification pattern, in pseudocode:
On receiving a webhook request:
  1. Read the signature header from the request
  2. Read the raw request body bytes (don't parse JSON yet)
  3. Compute HMAC over the body using the stored securityToken
  4. Compare the computed signature to the received signature using a constant-time comparison
  5. If they match, parse and process the request
  6. If they don't match, return 401 Unauthorized
Each step matters:

Step 1: read the signature header

The exact header name isn’t documented in the spec. Partner integrations need to identify it by inspecting real deliveries. Likely candidates based on common webhook conventions:
  • X-Raise-Signature
  • X-Signature
  • Signature
  • X-Webhook-Signature
Inspect a real delivery via the serverResponse field on a webhook log entry, or capture an incoming request at the partner endpoint and read the headers.
JavaScript
// Example — read the signature header (confirm the exact name against live data)
const signature = req.headers['x-raise-signature'];

if (!signature) {
  return res.status(400).send('Missing signature header');
}

Step 2: read the raw body

Don’t parse the JSON yet. The signature is typically computed over the exact bytes of the request body. If your HTTP framework parses the body to JSON before you can access the raw bytes, the signature comparison will silently fail because re-serializing the parsed JSON produces different bytes than the original. For Express:
JavaScript
import express from 'express';

const app = express();

// Capture the raw body for signature verification
app.use('/raise-webhooks', express.raw({ type: 'application/json' }));

app.post('/raise-webhooks', (req, res) => {
  const rawBody = req.body; // Buffer of raw bytes
  // ...
});
For Next.js API routes:
JavaScript
export const config = {
  api: { bodyParser: false }, // Disable automatic body parsing
};

export default async function handler(req, res) {
  const rawBody = await getRawBody(req); // Use a library like 'raw-body'
  // ...
}
The exact mechanism for capturing the raw body depends on your framework. The principle is the same: bytes as Raise sent them, before any deserialization.

Step 3: compute the HMAC

The standard webhook signature algorithm is HMAC-SHA256. Until the Raise spec confirms the exact algorithm, start with this assumption:
JavaScript
import crypto from 'crypto';

function computeSignature(rawBody, secret) {
  return crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
}
If the comparison consistently fails despite known-good test deliveries, try variants:
  • digest('base64') instead of 'hex'
  • 'sha1' instead of 'sha256'
  • Including a timestamp prefix (${timestamp}.${rawBody}) if Raise includes a timestamp header
  • A different secret encoding (base64-decoded vs. raw string)
The signature algorithm is consistent across deliveries — once you’ve identified the correct combination by inspecting real deliveries, code it directly.

Step 4: constant-time comparison

A naive string-equality comparison can leak information about the secret through timing attacks. Use a constant-time comparison:
JavaScript
function safeCompare(a, b) {
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));
}

const expectedSignature = computeSignature(rawBody, secret);
if (!safeCompare(signature, expectedSignature)) {
  return res.status(401).send('Invalid signature');
}
crypto.timingSafeEqual (in Node.js) and equivalent functions in other languages produce a comparison that takes the same amount of time regardless of how many characters match. This prevents an attacker from learning the signature byte-by-byte through timing observation.

Step 5: parse and process

Only after verification passes should the body be parsed and processed:
JavaScript
app.post('/raise-webhooks', (req, res) => {
  const signature = req.headers['x-raise-signature'];
  const rawBody = req.body;

  if (!signature || !verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(rawBody.toString('utf8'));

  // Acknowledge quickly, process asynchronously
  res.status(200).send('OK');
  eventQueue.publish(event);
});
The order matters: verify, acknowledge, queue. Don’t process inside the request handler.

A complete verification function

JavaScript
import crypto from 'crypto';

function verifyRaiseWebhook(req, secret) {
  // 1. Read the signature header
  const signature = req.headers['x-raise-signature'];
  if (!signature) return false;

  // 2. Read the raw body
  const rawBody = req.body; // Assumes express.raw() middleware

  // 3. Compute the expected signature
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // 4. Constant-time comparison
  if (signature.length !== expected.length) return false;
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'utf8'),
    Buffer.from(expected, 'utf8')
  );
}

// Usage in the webhook handler
app.post('/raise-webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyRaiseWebhook(req, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge quickly
  res.status(200).send('OK');

  // Parse and queue for async processing
  const event = JSON.parse(req.body.toString('utf8'));
  eventQueue.publish(event);
});
Confirm the algorithm (sha256), the digest encoding (hex), and the header name (x-raise-signature) against actual deliveries before relying on this in production. The function structure stays the same; only the specifics may need adjustment.

Rotating the secret

Webhook secrets should be rotated periodically — typically every 90 days for high-sensitivity integrations, every 6–12 months for lower-sensitivity ones. The challenge: rotation must be coordinated so both sides hold the same secret at all times, without dropping deliveries during the cutover.

The dual-secret rotation pattern

The standard pattern: the partner accepts either the old or the new secret during the rotation window.
1

Generate a new secret

On the partner side, generate a fresh securityToken. Store it alongside the existing one in your secrets manager, both marked as valid.
2

Update the verification to accept either secret

The verification function tries the new secret first, then falls back to the old one. Both pass during the rotation window.
JavaScript
function verifyRaiseWebhook(req, primarySecret, fallbackSecret) {
  if (verifySignature(req, primarySecret)) return true;
  if (fallbackSecret && verifySignature(req, fallbackSecret)) return true;
  return false;
}
3

Update the webhook subscription with the new secret

PUT /api/Webhook/{id} with securityToken set to the new value. Raise begins signing with the new secret immediately on the next delivery.
4

Confirm new deliveries verify with the new secret

Check webhook logs for successful deliveries after the update. Confirm signatures verify with the new secret.
5

Remove the old secret

After a brief window (long enough to confirm no stragglers signed with the old secret), remove the old secret from your secrets manager. The verification now only accepts the new secret.
The dual-secret window can be brief — Raise’s signing changes happen on the very next delivery after PUT /api/Webhook/{id} completes — but the window is what prevents any in-flight requests from being rejected during the cutover.

When to force a rotation

In addition to scheduled rotation:
TriggerWhen
Suspected compromiseIf the secret may have leaked (log exposure, debugging session, etc.) — rotate immediately, don’t wait for the schedule.
Personnel changesIf someone with access to the secret leaves the partner organization.
Repository exposureIf the secret was accidentally committed to source control.
Cutover to a new partner endpointWhen migrating to a new notificationUrl, also rotate the secret.

Handling verification failures

A signature verification failure typically indicates one of:
CauseLikelihoodAction
Genuine forged requestLow for most integrationsReject. Don’t process. Log for review.
Misconfigured secretHigh during initial setupRe-check the secrets manager — partner copy must match Raise’s.
Body parsing before verificationCommon implementation bugMake sure raw body is read before any JSON parsing.
Mid-rotation timingPossible during rotation windowsDual-secret pattern (above) prevents this.
Wrong algorithm or headerPossible during initial integrationInspect real deliveries to confirm.
For each failure:
  • Always reject the request with 401 Unauthorized. Don’t try to “recover” by processing anyway — a forged request that’s processed is worse than a real request that’s rejected.
  • Log the failure (without logging the secret or the raw signature) for diagnostic review.
  • Alert on patterns — sustained verification failures over time indicate either a configuration issue or an active attack worth investigating.

Metrics worth tracking

MetricThreshold for alert
Verification failure rate>1% sustained over 10 minutes
Verification failures per minuteSpike >10x baseline
Time since last successful verification>1 hour during active delivery window
These metrics help distinguish “the secret was rotated incorrectly” from “an attacker is probing the endpoint.”

Layered defenses

Signature verification is the primary defense against forged events. A few layered defenses add resilience:

HTTPS only

The webhook URL must use HTTPS. Plain HTTP would expose the signature in transit and make man-in-the-middle attacks easier. Most modern HTTP frameworks enforce HTTPS for webhook receivers by default; confirm yours does.

IP allowlisting (optional)

For partner integrations with very high security requirements, allowlisting Raise’s delivery IPs adds a network-layer defense before requests even reach the application. Coordinate with the platform team for the canonical IP ranges, and update the allowlist whenever they change. This is belt-and-suspenders — signature verification alone is sufficient for most integrations. IP allowlisting helps but doesn’t replace signature verification.

Rate limiting on the webhook endpoint

Even with signature verification, an attacker can flood the endpoint with forged requests forcing the application to compute HMAC for each. Apply rate limiting at the edge (load balancer, API gateway) to prevent this.

No echoing of received content

Don’t include the received request body in error responses or logs without sanitization. A misconfigured endpoint that echoes unverified content can leak information about the integration’s processing logic.

Where to go next

Retry Behavior

What happens when the partner endpoint returns a non-success response — including signature failures.

Idempotency and Safe Reprocessing

Handle duplicate deliveries from retries.

Webhooks Overview

The full webhook subscription lifecycle.

Security and Credential Management

Storing and rotating webhook secrets alongside other integration credentials.
Last modified on May 20, 2026