Webhook Signature Verification
Ping signs all outgoing callbacks with HMAC-SHA256 signatures. This allows you to verify that callbacks are genuinely from Ping and haven't been tampered with.
Signature Header
Every callback includes an X-Ping-Signature header:
X-Ping-Signature: t=1768483312,v1=21c1746916fac3c5e98835f5c00ea78081a122a26843fbff05961bd9cf059ec9
t- Unix timestamp when the signature was generatedv1- HMAC-SHA256 signature of"timestamp.payload"
Getting Your Webhook Secret
Each channel has a unique secret for signature verification:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID/secret"Response:
{
"webhook_secret": "4140db9a5fa04fe3208183ceb1d659cafbfa23ff3f31e7cc3777fd3062e60992",
"algorithm": "HMAC-SHA256",
"header_name": "X-Ping-Signature"
}Verification Steps
- Extract
t(timestamp) andv1(signature) from the header - Check timestamp is within 5 minutes (replay protection)
- Reconstruct:
"${timestamp}.${rawBody}" - Compute HMAC-SHA256 with your webhook secret
- Compare using timing-safe comparison
Quick Verification (Node.js)
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const [tPart, sigPart] = signatureHeader.split(',');
const timestamp = parseInt(tPart.split('=')[1]);
const signature = sigPart.split('=')[1];
// Check timestamp (5 min tolerance)
if (Math.abs(Date.now()/1000 - timestamp) > 300) return false;
// Verify signature
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}Quick Verification (Python)
import hmac
import hashlib
import time
def verify_signature(raw_body: str, signature_header: str, secret: str) -> bool:
parts = dict(part.split('=', 1) for part in signature_header.split(','))
timestamp = int(parts['t'])
signature = parts['v1']
# Check timestamp (5 min tolerance)
if abs(time.time() - timestamp) > 300:
return False
# Verify signature
expected = hmac.new(
secret.encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Rotating Secrets
If your secret is compromised:
curl -X POST -H "Authorization: Bearer YOUR_API_KEY" \
"https://ping-api-production.up.railway.app/v1/channels/YOUR_CHANNEL_ID/secret/rotate"The new secret takes effect immediately. Old signatures will fail verification.
Debugging Signatures
Use the public verification endpoints to debug issues:
Generate a Test Signature
curl -X POST "https://ping-api-production.up.railway.app/v1/verify/generate" \
-H "Content-Type: application/json" \
-d '{"payload": "{\"test\":true}", "secret": "YOUR_SECRET"}'Verify a Signature
curl -X POST "https://ping-api-production.up.railway.app/v1/verify/signature" \
-H "Content-Type: application/json" \
-d '{"payload": "...", "signature": "t=...,v1=...", "secret": "YOUR_SECRET"}'Get Documentation
curl "https://ping-api-production.up.railway.app/v1/verify/docs"Common Issues
Timestamp Drift
If your server clock is off, you may fail the 5-minute check. Use NTP to sync your server time.
Raw Body Encoding
The signature is computed on the raw request body bytes. Make sure you're using the raw body before any JSON parsing.
Secret Handling
Store your webhook secret securely. Don't commit it to version control.