Webhooks
Receive real-time notifications when subscribers join, update their preferences, or unsubscribe from your calendar feeds.
On this page
Overview
Webhooks let you receive HTTP POST requests to your server whenever subscriber-related events happen on your EmbedACal feeds. This is useful for syncing subscriber data to your CRM, email marketing tools, or custom systems.
Each webhook delivery is signed with HMAC-SHA256, so you can verify that the request genuinely came from EmbedACal.
Setting up webhooks
- Go to your dashboard and click "Webhooks" in the sidebar
- Click "Add endpoint"
- Enter your HTTPS endpoint URL
- Select which events you want to receive
- Copy the signing secret (shown once — store it securely in your server's environment variables)
Your endpoint must respond with a 2xx status code within 10 seconds to be considered successful.
Event types
| Event | Description |
|---|---|
subscriber.created | A new subscriber confirmed their email and is now active |
subscriber.updated | A subscriber changed their notification frequency or criteria |
subscriber.unsubscribed | A subscriber opted out and was removed |
Payload format
All webhook payloads are JSON with the following structure:
subscriber.created
POST /your-webhook-url
{
"event": "subscriber.created",
"data": {
"email": "[email protected]",
"feedSlug": "community-events",
"criteria": null,
"digestFrequency": "daily",
"timestamp": 1740000000000
},
"timestamp": 1740000000000
}subscriber.updated
POST /your-webhook-url
{
"event": "subscriber.updated",
"data": {
"email": "[email protected]",
"feedSlug": "community-events",
"criteria": "{\"keywords\":\"outdoor\",\"state\":\"California\"}",
"digestFrequency": "weekly",
"timestamp": 1740100000000
},
"timestamp": 1740100000000
}subscriber.unsubscribed
POST /your-webhook-url
{
"event": "subscriber.unsubscribed",
"data": {
"email": "[email protected]",
"feedSlug": "community-events",
"timestamp": 1740200000000
},
"timestamp": 1740200000000
}Verifying signatures
Every webhook request includes an X-EmbedACal-Signature header containing an HMAC-SHA256 hex digest of the request body, signed with your endpoint's secret.
Always verify this signature before processing the webhook to ensure the request is authentic.
Node.js / JavaScript
verify-webhook.js
import crypto from 'crypto';
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhooks/embedacal', (req, res) => {
const signature = req.headers['x-embedacal-signature'];
const isValid = verifyWebhook(
JSON.stringify(req.body),
signature,
process.env.EMBEDACAL_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
// Process the webhook event...
res.status(200).json({ received: true });
});Python
verify_webhook.py
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your webhook handler (Flask example):
@app.route('/webhooks/embedacal', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-EmbedACal-Signature', '')
is_valid = verify_webhook(
request.data,
signature,
os.environ['EMBEDACAL_WEBHOOK_SECRET']
)
if not is_valid:
return {'error': 'Invalid signature'}, 401
payload = request.json
event_type = payload['event']
data = payload['data']
# Process the webhook event...
return {'received': True}, 200Ruby
verify_webhook.rb
require 'openssl'
def verify_webhook(body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
Rack::Utils.secure_compare(signature, expected)
end
# In your webhook handler (Sinatra example):
post '/webhooks/embedacal' do
body = request.body.read
signature = request.env['HTTP_X_EMBEDACAL_SIGNATURE']
unless verify_webhook(body, signature, ENV['EMBEDACAL_WEBHOOK_SECRET'])
halt 401, { error: 'Invalid signature' }.to_json
end
payload = JSON.parse(body)
# Process the webhook event...
{ received: true }.to_json
endcurl (testing)
Test your endpoint
# Generate a test signature
SECRET="whsec_your_secret_here"
BODY='{"event":"subscriber.created","data":{"email":"[email protected]","feedSlug":"my-feed","timestamp":1740000000000},"timestamp":1740000000000}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
# Send test webhook
curl -X POST https://your-app.com/webhooks/embedacal \
-H "Content-Type: application/json" \
-H "X-EmbedACal-Signature: $SIGNATURE" \
-d "$BODY"Retries and failure handling
If your endpoint doesn't return a 2xx status code within 10 seconds, EmbedACal will retry the delivery with exponential backoff:
- First retry: 1 minute after initial failure
- Second retry: 5 minutes after first retry
- Third retry: 30 minutes after second retry
After 3 failed retries, the delivery is marked as failed. You can see delivery status in your dashboard under Webhooks.
Your endpoint should be idempotent — the same event may be delivered more than once in edge cases. Use the timestamp field to deduplicate.
Security best practices
- Always verify the
X-EmbedACal-Signatureheader before processing any webhook - Use HTTPS for your endpoint URL (HTTP endpoints are rejected)
- Store the signing secret in environment variables, never in code
- Respond quickly (within 10 seconds) — do heavy processing asynchronously after acknowledging receipt
- If your secret is compromised, regenerate it immediately from the dashboard (the old secret stops working right away)
- Use a constant-time comparison function when verifying signatures to prevent timing attacks