Webhook Integration
Receive push notifications when decks change or cards are due for review.
3 min read
Webhooks let your integration receive real-time events instead of polling. When a deck is updated or cards become due, Chamelingo POSTs a signed JSON payload to your endpoint.
Supported Events#
| Event | Fired when |
|---|---|
review.due | Cards are due for spaced repetition review |
deck.updated | A deck is created or modified |
deck.deleted | A deck is deleted |
Register a Webhook#
bash
curl -X POST https://api.chamelingo.com/api/v1/webhooks \
-H "Authorization: Bearer ck_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://my-bot.example.com/chamelingo-webhook",
"events": ["deck.updated", "review.due"]
}'
Python
import requests
response = requests.post(
"https://api.chamelingo.com/api/v1/webhooks",
headers={"Authorization": "Bearer ck_live_YOUR_KEY_HERE"},
json={
"url": "https://my-bot.example.com/chamelingo-webhook",
"events": ["deck.updated", "review.due"],
},
)
secret = response.json()["secret"] # Save immediately
TypeScript
const response = await fetch("https://api.chamelingo.com/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: "Bearer ck_live_YOUR_KEY_HERE",
"Content-Type": "application/json",
},
body: JSON.stringify({
url: "https://my-bot.example.com/chamelingo-webhook",
events: ["deck.updated", "review.due"],
}),
});
const { secret } = await response.json(); // Save immediately
The response includes a secret — save it immediately, it's shown only once:
JSON
{
"id": "wh_abc...",
"url": "https://my-bot.example.com/chamelingo-webhook",
"events": ["deck.updated", "review.due"],
"secret": "a1b2c3d4e5f6...",
"_note": "Save the secret — it will not be shown again."
}
Payload Format#
Payloads arrive as JSON POST requests:
JSON
{
"event": "deck.updated",
"timestamp": "2026-03-20T12:00:00.000Z",
"data": {
"deck_id": "clxyz123",
"action": "created"
}
}
Headers#
| Header | Value |
|---|---|
Content-Type | application/json |
X-Chamelingo-Signature | sha256=<HMAC-SHA256 hex digest> |
X-Chamelingo-Event | Event name (e.g., deck.updated) |
Verify Signatures#
Always verify the X-Chamelingo-Signature header to ensure payloads are authentic.
TypeScript
import crypto from "crypto";
function verifyWebhook(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
// In your Express handler:
app.post("/chamelingo-webhook", (req, res) => {
const signature = req.headers["x-chamelingo-signature"];
if (!verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
// Process event...
res.status(200).send("OK");
});
Python
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.post("/chamelingo-webhook")
def handle_webhook():
signature = request.headers.get("X-Chamelingo-Signature", "")
if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
abort(401)
# Process event...
return "OK", 200
Test Your Endpoint#
Send a test event to verify connectivity:
bash
curl -X POST https://api.chamelingo.com/api/v1/webhooks/WEBHOOK_ID/test \
-H "Authorization: Bearer ck_live_YOUR_KEY_HERE"
Warning
Webhook URLs must be HTTPS. Private/internal IP addresses (localhost, 10.x, 172.16-31.x, 192.168.x) are blocked for security.
Manage Webhooks#
bash
# List all webhooks
curl https://api.chamelingo.com/api/v1/webhooks \
-H "Authorization: Bearer ck_live_YOUR_KEY_HERE"
# Delete a webhook
curl -X DELETE https://api.chamelingo.com/api/v1/webhooks/WEBHOOK_ID \
-H "Authorization: Bearer ck_live_YOUR_KEY_HERE"
Maximum 10 active webhooks per user account.