Webhooks
Webhooks let you receive real-time HTTP notifications when events happen in your MCP Gateway workspace. Instead of polling the API, MCP Gateway pushes event data to your server as it occurs.
How it works
- Create a webhook endpoint -- register a URL and select which events you want to receive
- Receive events -- MCP Gateway sends a
POSTrequest to your URL with event data - Verify the signature -- validate the HMAC signature to ensure the payload is authentic
- Return 2xx -- respond with a 2xx status code to acknowledge receipt
Webhooks require the webhooks entitlement on your plan, and the number of endpoints per workspace is capped by your plan's webhook endpoint limit.
Creating a webhook endpoint
In the app (workspace owners only):
- Click Webhooks in the sidebar under the Integrate section
- Click Add Endpoint
- Enter the HTTPS URL, select the events you want to receive, and save
You can also manage endpoints programmatically via the REST API (/api/v1/webhook_endpoints, webhooks:manage scope) or the MCP server tools.
About the signing secret: every endpoint gets an HMAC signing secret, generated at creation. When you create an endpoint via the API or MCP, the secret is included in the create response only — it is never returned by list or get. In the web UI, workspace owners can reveal and copy the secret from the endpoint's row on the Webhooks page.
Event types
| Event | Trigger |
|---|---|
member.invited |
An invitation to join the workspace is sent |
member.joined |
Someone accepts an invitation and becomes a member |
member.removed |
A member is removed from the workspace |
subscription.updated |
The workspace's subscription changes (plan, status, or renewal) |
These four events cover the chassis. As you build your own features, add your domain's event types to the allowed list and fire them through the same dispatch pipeline.
Payload format
Every delivery is a JSON POST with the same envelope: event, timestamp, data, and a unique webhook_id you can use for deduplication.
member.invited — data describes the invitation:
{
"event": "member.invited",
"timestamp": "2026-02-09T12:00:00Z",
"data": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"email": "jane@example.com",
"role": "editor",
"status": "pending",
"created_at": "2026-02-09T12:00:00Z"
},
"webhook_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
member.joined and member.removed — data describes the membership:
{
"event": "member.joined",
"timestamp": "2026-02-09T12:05:00Z",
"data": {
"id": "11111111-2222-3333-4444-555555555555",
"email": "jane@example.com",
"role": "editor",
"status": "active",
"joined_at": "2026-02-09T12:05:00Z",
"created_at": "2026-02-09T12:05:00Z"
},
"webhook_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
}
subscription.updated — data describes the subscription:
{
"event": "subscription.updated",
"timestamp": "2026-02-09T12:10:00Z",
"data": {
"id": "22222222-3333-4444-5555-666666666666",
"status": "active",
"plan_name": "Pro",
"current_period_end": "2026-03-09T12:10:00Z",
"created_at": "2026-01-26T09:00:00Z",
"updated_at": "2026-02-09T12:10:00Z"
},
"webhook_id": "c3d4e5f6-a7b8-9012-cdef-123456789012"
}
HTTP headers
Each delivery includes these headers:
| Header | Description |
|---|---|
Content-Type |
application/json |
X-Mcp-Gateway-Signature |
HMAC-SHA256 signature of the request body, formatted sha256=<hex digest> |
X-MCP Gateway-Event |
The event type (e.g., member.joined) |
X-MCP Gateway-Delivery |
Unique delivery ID (UUID) |
Verifying signatures
Every payload is signed with HMAC-SHA256 using your endpoint's signing secret: the signature is the hex digest of the raw request body, sent in the X-Mcp-Gateway-Signature header as sha256=<hex_digest>. Always verify it before trusting a payload, and use a constant-time comparison.
Ruby
def verify_webhook(request, secret)
payload = request.body.read
signature = request.headers["X-Mcp-Gateway-Signature"]
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, payload)
unless ActiveSupport::SecurityUtils.secure_compare(signature, expected)
raise "Invalid webhook signature"
end
JSON.parse(payload)
end
Python
import hmac
import hashlib
import json
def verify_webhook(request, secret):
payload = request.body
signature = request.headers.get("X-Mcp-Gateway-Signature", "")
expected = "sha256=" + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("Invalid webhook signature")
return json.loads(payload)
JavaScript (Node.js)
const crypto = require("crypto");
function verifyWebhook(req, secret) {
const payload = req.body; // raw body string
const signature = req.headers["x-mcp-gateway-signature"];
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(payload).digest("hex");
if (
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
throw new Error("Invalid webhook signature");
}
return JSON.parse(payload);
}
Retry policy
A delivery succeeds when your endpoint responds with any 2xx status. Otherwise MCP Gateway retries with exponential backoff, up to 6 attempts in total:
| After failed attempt | Next attempt in |
|---|---|
| 1st | 5 seconds |
| 2nd | 30 seconds |
| 3rd | 2 minutes |
| 4th | 15 minutes |
| 5th | 1 hour |
If the 6th attempt also fails, the delivery is marked permanently failed. You can inspect delivery history — status, response codes, and attempt counts — on the endpoint's page in the app or via GET /api/v1/webhook_endpoints/:id.
Connection timeouts
- Connect timeout: 10 seconds
- Read timeout: 30 seconds
Auto-disable
Endpoints that fail consistently are disabled automatically. Every failed attempt increments the endpoint's failure count (a successful delivery resets it to zero). Once the count reaches 15 and the endpoint has gone at least 3 days without a successful delivery, it is disabled and stops receiving events. Re-enable it from the Webhooks page, or set enabled: true via the API or MCP.
URL requirements
- Webhook URLs must use HTTPS in production
- URLs resolving to private or internal IP ranges (10.x, 172.16.x, 192.168.x, loopback, link-local) are rejected as SSRF protection
Testing webhooks
Click Test on any endpoint on the Webhooks page, or call POST /api/v1/webhook_endpoints/:id/test, to send a ping event and see the result — including the HTTP status your server returned. The ping payload uses the standard envelope with "event": "ping" and "data": {"message": "Test ping from MCP Gateway"}, and is signed like any other delivery.
Best practices
- Always verify signatures -- never trust unverified payloads
- Respond quickly -- return 2xx within 30 seconds and process the event asynchronously
- Be idempotent -- use
webhook_idto deduplicate, since retries resend the same payload - Monitor delivery health -- watch the failure count and recent deliveries so an endpoint doesn't drift into auto-disable
Next steps
- API Overview -- manage endpoints programmatically
- MCP server -- manage endpoints from an AI assistant