Verifying PulseSignal webhook signatures
PulseSignal signs every outbound webhook with HMAC-SHA256 so your server can confirm the request came from us and was not modified in transit. The signing scheme is Standard Webhooks v1, the same shape used by Stripe, Shopify, and Polar.
Headers
Every POST includes the following headers. The signature is over the exact request body bytes, joined to the timestamp with a period.
X-PulseSignal-Timestampthe Unix timestamp at the moment of signing.X-PulseSignal-Signatureof the formv1=<hex>, where hex = HMAC-SHA256(secret, "{timestamp}.{body}").X-PulseSignal-Event-Ida stable event identifier; use it to dedupe retries on your side.X-PulseSignal-Event-Typeone ofchange_event,narrative,alert_rule_trigger, orwebhook.test.X-PulseSignal-Attemptthe 1-indexed delivery attempt. PulseSignal retries on 5xx responses, transport errors, and 429 throttles.
Verification in Python
import hmac, hashlib
def verify(secret: str, timestamp: str, body: bytes, sig_header: str) -> bool:
expected = "v1=" + hmac.new(
secret.encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig_header)
Verification in Node.js
const crypto = require('crypto')
function verify(secret, timestamp, body, sigHeader) {
const expected =
'v1=' +
crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(body)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sigHeader),
)
}
Replay protection
Retries are byte-identical to the first attempt: PulseSignal signs each event once and reuses the original timestamp and signature on every retry, so a delivery retried hours later (see the schedule below) carries a timestamp hours in the past. Do not reject deliveries on timestamp age alone, or you will drop legitimate retries.
Instead, deduplicate on X-PulseSignal-Event-Id: after verifying the signature, record each event id you have processed (covering at least the 24-hour retry window) and ignore any request whose id you have already seen. A replayed capture is then harmless, because it can only re-deliver an event id you have already consumed exactly once.
Retry schedule
A delivery that returns 5xx, 429, or fails to connect is retried with exponential backoff:
- Attempt 2: 1 minute later
- Attempt 3: 5 minutes later
- Attempt 4: 30 minutes later
- Attempt 5: 2 hours later
- Attempt 6: 8 hours later
- Attempt 7: 24 hours later
After 10 consecutive failures across all events, the endpoint is automatically disabled and you will need to re-enable it from the dashboard once your receiver is healthy.
Response contract
Return any 2xx status code within 10 seconds to acknowledge delivery. 3xx redirects are not followed (cloud-provider metadata services bait this) and are treated as failures. 4xx responses other than 429 stop the retry chain since they indicate a permanent configuration problem on the receiving side.
Secret rotation
Rotate the signing secret from the dashboard at any time. The new secret is shown exactly once; the previous secret stops verifying signatures the instant the rotate request returns. Update your receiver before rotating to avoid a window of rejected deliveries.