Contents
Overview
A webhook endpoint is an HTTPS URL you own. When an event occurs, TitanVX sends an HTTP POST with a JSON
body to your endpoint. Your server should respond with a 2xx status code as quickly as possible.
X-TitanVX-Event: event type stringX-TitanVX-Delivery: unique delivery idX-TitanVX-Signature: HMAC signature for verification
Event types
These are the event type values you can subscribe to:
Billing
billing.plan.changed
Quota
quota.threshold.reached(thresholds: 80%, 100%)quota.request.rejected
Deployments
agent.deployment.createdagent.deployment.updatedagent.deployment.deleted
Webhook management
webhook.endpoint.createdwebhook.endpoint.updatedwebhook.endpoint.deleted
You can also fetch the live event catalog from the gateway:
GET /api/webhooks/events
Payload format
The webhook body is a JSON object. The data shape depends on the event type.
{
"id": "0c3b1d5b-1d66-4a16-b2b5-7aa3c2c9b5ab",
"type": "quota.threshold.reached",
"created_at": "2026-02-21T22:14:02Z",
"data": {
"scope": "daily",
"threshold": 80,
"limit": 10000,
"count": 8123,
"plan": "builder",
"window": "2026-02-21"
}
}
- Always parse JSON as UTF‑8.
- Use
idfor idempotency (dedupe repeated deliveries). - Do not trust the payload unless signature verification passes.
Signature verification
Each webhook endpoint has a secret (shown once at creation, and when rotated). TitanVX signs each delivery using:
HMAC_SHA256(secret, "{timestamp}.{raw_body}").
The header looks like: X-TitanVX-Signature: t=1700000000,v1=<hex>
Python (Flask example)
import hmac, hashlib
from flask import Flask, request, abort
WEBHOOK_SECRET = "YOUR_ENDPOINT_SECRET"
def verify(sig_header: str, body: bytes) -> bool:
# sig_header: "t=...,v1=..."
parts = dict(p.split("=", 1) for p in sig_header.split(",") if "=" in p)
ts = parts.get("t", "")
v1 = parts.get("v1", "")
msg = (ts + ".").encode("utf-8") + body
expected = hmac.new(WEBHOOK_SECRET.encode("utf-8"), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
app = Flask(__name__)
@app.post("/titanvx/webhooks")
def handler():
sig = request.headers.get("X-TitanVX-Signature", "")
body = request.get_data() # raw bytes
if not verify(sig, body):
abort(400)
event = request.get_json()
# process event["type"], event["data"]
return {"ok": True}
Node.js (Express example)
import crypto from "crypto";
import express from "express";
const WEBHOOK_SECRET = process.env.TITANVX_WEBHOOK_SECRET;
const app = express();
app.post("/titanvx/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const sig = String(req.header("X-TitanVX-Signature") || "");
const parts = Object.fromEntries(sig.split(",").map(p => p.split("=", 2)));
const ts = parts.t || "";
const v1 = parts.v1 || "";
const msg = Buffer.concat([Buffer.from(ts + ".", "utf8"), req.body]);
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(msg).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
return res.json({ ok: true });
});
In production, reject requests with old timestamps (e.g. older than 5 minutes) and dedupe on event.id.
Event filters
When you create or update an endpoint you can set events to control delivery.
*— deliver all events- Exact match — deliver only specific types (e.g.
billing.plan.changed) - Note: prefix patterns like
quota.*may be supported by UI conventions, but exact types are always safe.
Retries & idempotency
Deliveries retry when your endpoint returns a non-2xx response or times out. You should treat webhook handlers as at-least-once delivery and implement idempotency.
- Any
2xxresponse marks the delivery succeeded. - Non-2xx responses are retried with backoff.
- Use
X-TitanVX-Deliveryandevent.idto dedupe.
Managing endpoints
Use the Webhooks API (or the UI) to manage endpoints and view deliveries.
POST /api/webhooks/endpoints # create (returns secret once)
GET /api/webhooks/endpoints # list
PATCH /api/webhooks/endpoints/{id} # update events/url/active, rotate secret
DELETE /api/webhooks/endpoints/{id} # disable (or hard delete with ?hard=true)
GET /api/webhooks/deliveries # recent deliveries
POST /api/webhooks/test # queue test event
Use the management UI at /developer/webhooks.html.
Testing
Send a test event
POST /api/webhooks/test
{
"event_type": "webhook.test",
"data": { "hello": "world" }
}
Dev-only mock deployment event
When enabled on the server (DEV_MOCKS_ENABLED=true), you can emit a mock deployment event:
POST /api/dev/mock-agent-deployment
Headers:
X-Internal-Secret: <TPS_KEY>
Body:
{
"user_id": "<supabase_user_id>",
"action": "updated",
"deployment_id": "dep_123",
"data": { "status": "active" }
}
Troubleshooting
- No events arriving: confirm your endpoint is active and subscribed to the event type.
- Signature failures: ensure you use the raw request body bytes and the correct endpoint secret.
- Timeouts: respond fast (2xx) and process asynchronously.
- Retries: check
GET /api/webhooks/deliveriesfor last error and HTTP status.
Download as PDF: click Download PDF and choose “Save as PDF” in your browser print dialog.