Webhooks
Webhooks deliver job, quota, and subscription events to your infrastructure in real time. Wire them into dashboards, alerting, or CRM so your systems react the moment MCPP sees a change.
Webhooks are available on the Pro plan and up. Manage endpoints from your dashboard.
Event types
job_returned— fires every time one of your API keys records a tool call against/usage/track.quota_approaching— fires the first time your usage crosses 80% of your monthly quota, at most once per 24 hours.quota_exceeded— fires when your usage blows through the monthly cap. At most one delivery per billing period.subscription_changed— fires when Stripe notifies us of a plan update or cancellation. The payload reports the previous and new plan.
Delivery headers
Every delivery is an HTTP POST with Content-Type: application/json and the following custom headers:
X-Webhook-Signature—sha256=<hex>HMAC of the raw request body using your endpoint's signing secret.X-Webhook-Event— the event name, same as the top-leveleventfield on the JSON body.X-Webhook-Delivery-Id— unique id for this attempt; useful for deduplication.X-Webhook-Timestamp— ISO 8601 timestamp of the attempt. Reject deliveries older than a few minutes to prevent replay.
Example payloads
{
"event": "job_returned",
"job_count": 12,
"tool": "get_jobs",
"api_key_prefix": "gm_abcd",
"timestamp": "2026-04-20T12:34:56.789Z"
}{
"event": "quota_approaching",
"plan": "pro",
"usage": 4000,
"limit": 5000,
"percent": 80,
"timestamp": "2026-04-20T12:34:56.789Z"
}{
"event": "quota_exceeded",
"plan": "pro",
"usage": 5001,
"limit": 5000,
"timestamp": "2026-04-20T12:34:56.789Z"
}{
"event": "subscription_changed",
"old_plan": "free",
"new_plan": "pro",
"stripe_event_id": "evt_1AbCdEfG",
"timestamp": "2026-04-20T12:34:56.789Z"
}Verifying signatures
Always verify X-Webhook-Signature against the signing secret you copied when you created the endpoint. Do the comparison over the raw request body — any reserialization will produce a different digest and fail.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(rawBody, header, secret) {
// header format: "sha256=<hex>"
const expected = "sha256=" + createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
// Express example
app.post("/hooks/getmany", express.raw({ type: "application/json" }), (req, res) => {
const rawBody = req.body.toString("utf8");
const sig = req.get("X-Webhook-Signature");
const ts = req.get("X-Webhook-Timestamp");
if (!verifySignature(rawBody, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("invalid signature");
}
// Replay protection: reject deliveries older than 5 minutes.
if (!ts || Date.now() - new Date(ts).getTime() > 5 * 60_000) {
return res.status(401).send("stale");
}
const payload = JSON.parse(rawBody);
// handle payload.event ...
res.status(200).send("ok");
});import hmac, hashlib, time
from datetime import datetime, timezone
def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header or "")
def handle(request, secret: str):
raw = request.get_data() # Flask: use get_data(), not .json
sig = request.headers.get("X-Webhook-Signature", "")
ts = request.headers.get("X-Webhook-Timestamp", "")
if not verify_signature(raw, sig, secret):
return ("invalid signature", 401)
# Replay protection
try:
when = datetime.fromisoformat(ts.replace("Z", "+00:00"))
if (datetime.now(timezone.utc) - when).total_seconds() > 300:
return ("stale", 401)
except Exception:
return ("bad timestamp", 401)
# process payload (request.get_json()) ...
return ("ok", 200)Retry policy
A delivery is considered successful only when your endpoint returns a 2xx status within 10 seconds. Any other response — or a network error — triggers a retry with exponential backoff:
- Attempt 1: immediate
- Attempt 2: 1 minute later
- Attempt 3: 5 minutes later
- Attempt 4: 15 minutes later
- Attempt 5: 1 hour later
After 5 failed attempts the delivery is marked failed_permanently and you can inspect the last response body in the Deliveries panel on the dashboard.
Security best practices
- Treat your signing secret like a password — never commit it to source control. Use rotate secret from the dashboard if it leaks.
- Always verify the signature using a constant-time comparison (
timingSafeEqual/hmac.compare_digest). - Check
X-Webhook-Timestampand reject deliveries older than 5 minutes to prevent replay attacks. - Deduplicate on
X-Webhook-Delivery-Idif your handler is not naturally idempotent — retries may deliver the same event more than once. - Only https:// URLs on public hostnames can be registered. Private ranges (
127.0.0.0/8,10.0.0.0/8,192.168.0.0/16,172.16.0.0/12,169.254.0.0/16, ::1, fc00::/7, fe80::/10) are rejected.
Endpoint limits
Pro includes up to 5 active webhook endpoints. Enterprise includes 20. Contact sales for higher limits.
Managing endpoints
Create, edit, and test endpoints in your dashboard. From there you can send a test delivery, inspect recent attempts, rotate the signing secret, or pause an endpoint without deleting it.
Recipes that pair with webhooks
These job-search recipes pair well with webhook event pipelines — wire them into dashboards, alerting, or CRM flows.
Ready to wire webhooks?
Webhook delivery is a Pro-plan feature. Upgrade to pipe job, quota, and subscription events into your own systems in real time.