Skip to content

Signature Verification

Webhook endpoints must verify the authenticity of incoming requests to prevent spoofing. Synapse verifies signatures for all webhook providers: Resend (Svix), Lemon Squeezy (HMAC-SHA256), and pyrx.payment (shared secret).


Verification Methods by Provider

ProviderEndpointMethodHeader(s)
ResendPOST /v1/webhooks/resendSvix (HMAC-SHA256)svix-id, svix-timestamp, svix-signature
Lemon SqueezyPOST /v1/webhooks/lemonsqueezyHMAC-SHA256x-signature
pyrx.payment (deprecated)POST /v1/webhooks/paymentShared secretX-Webhook-Secret
Note

Synapse uses Resend as the primary email provider. All email delivery webhooks are verified via Svix.


pyrx.payment (Deprecated)

Warning

pyrx.payment is deprecated. New workspaces use Lemon Squeezy. This section is preserved for legacy integrations.

Every webhook request from pyrx.payment includes an X-Webhook-Secret header containing the shared secret. Your handler should compare this against your configured webhook secret using a constant-time comparison.

POST /v1/webhooks/payment
X-Webhook-Secret: whsec_abc123def456...
Content-Type: application/json

Verification Logic

Python

python
import hmac
 
def verify_webhook_secret(provided: str, configured: str) -> bool:
"""
Verify the webhook secret. Supports secret rotation
via comma-separated configured secrets.
"""
for secret in configured.split(","):
if hmac.compare_digest(provided.strip(), secret.strip()):
return True
return False
 
# Usage in a webhook handler
webhook_secret = request.headers.get("X-Webhook-Secret", "")
if not verify_webhook_secret(webhook_secret, settings.PYRX_PAYMENT_WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid webhook secret")

JavaScript

javascript
const crypto = require("crypto");
 
function verifyWebhookSecret(provided, configured) {
const secrets = configured.split(",").map((s) => s.trim());
for (const secret of secrets) {
if (
provided.length === secret.length &&
crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(secret))
) {
return true;
}
}
return false;
}
 
// Usage in an Express handler
app.post("/webhooks/payment", (req, res) => {
const provided = req.headers["x-webhook-secret"] || "";
if (!verifyWebhookSecret(provided, process.env.PYRX_PAYMENT_WEBHOOK_SECRET)) {
return res.status(401).json({ detail: "Invalid webhook secret" });
}
// Process the event...
res.json({ received: true });
});
Warning

Always use constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) to prevent timing attacks. Standard string comparison (==) leaks information about the secret through response timing.


Secret Rotation

To rotate your webhook secret without downtime, Synapse supports comma-separated secrets:

bash
# Environment variable during rotation
PYRX_PAYMENT_WEBHOOK_SECRET=whsec_new_secret_here,whsec_old_secret_here

Rotation Steps

  1. Generate a new secret in the pyrx.payment dashboard
  2. Update your configuration to accept both old and new secrets (comma-separated)
  3. Deploy the configuration change
  4. Update pyrx.payment to use the new secret for outgoing webhooks
  5. Remove the old secret from your configuration after confirming all webhooks arrive with the new secret
Tip

Rotate webhook secrets every 90 days or immediately if you suspect a compromise. The comma-separated format ensures zero downtime during rotation.


Lemon Squeezy Webhook Verification

Lemon Squeezy signs webhook requests with HMAC-SHA256. The signature is in the x-signature header, computed over the raw request body.

Python

python
import hashlib
import hmac
 
def verify_ls_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
 
# Usage
signature = request.headers.get("x-signature", "")
if not verify_ls_signature(request.body, signature, settings.LS_WEBHOOK_SECRET):
raise HTTPException(status_code=400, detail="Invalid signature")

JavaScript

javascript
const crypto = require("crypto");
 
function verifyLsSignature(payload, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
 
// Usage in Express
app.post("/webhooks/lemonsqueezy", (req, res) => {
const signature = req.headers["x-signature"] || "";
if (!verifyLsSignature(req.rawBody, signature, process.env.LS_WEBHOOK_SECRET)) {
return res.status(400).json({ detail: "Invalid signature" });
}
// Process the event...
res.json({ received: true });
});

Resend Webhook Verification

Resend uses Svix for webhook signing. Three headers are included in every request:

HeaderDescription
svix-idUnique message identifier
svix-timestampTimestamp used in signature generation
svix-signatureHMAC signature of the payload

Python

python
from svix.webhooks import Webhook, WebhookVerificationError
 
def verify_resend_webhook(payload: bytes, headers: dict, secret: str) -> dict:
wh = Webhook(secret)
return wh.verify(payload, headers)
 
# Usage
svix_headers = {
"svix-id": request.headers.get("svix-id", ""),
"svix-timestamp": request.headers.get("svix-timestamp", ""),
"svix-signature": request.headers.get("svix-signature", ""),
}
try:
event = verify_resend_webhook(request.body, svix_headers, settings.RESEND_WEBHOOK_SECRET)
except WebhookVerificationError:
raise HTTPException(status_code=401, detail="Invalid webhook signature")

JavaScript

javascript
const { Webhook } = require("svix");
 
function verifyResendWebhook(payload, headers, secret) {
const wh = new Webhook(secret);
return wh.verify(payload, headers);
}
 
// Usage in Express
app.post("/webhooks/resend", (req, res) => {
try {
const event = verifyResendWebhook(req.rawBody, {
"svix-id": req.headers["svix-id"],
"svix-timestamp": req.headers["svix-timestamp"],
"svix-signature": req.headers["svix-signature"],
}, process.env.RESEND_WEBHOOK_SECRET);
// Process the event...
res.json({ status: "ok", processed: 1 });
} catch {
res.status(401).json({ detail: "Invalid webhook signature" });
}
});
Tip

Install the Svix SDK: pip install svix (Python) or npm install svix (Node.js). Resend uses Svix for webhook signing.


Best Practices

  1. Always verify before processing -- Never process a webhook payload without verifying the signature first.
  2. Return 200 quickly -- Verify the signature, enqueue the work, and return 200 OK. Do not perform slow processing synchronously in the webhook handler.
  3. Handle duplicates -- Webhook providers may retry delivery. Design your handler to be idempotent.
  4. Log all webhook requests -- Store the raw payload and headers for debugging delivery issues.
  5. Monitor for failures -- Alert on repeated verification failures, which may indicate an attack or a misconfigured secret.