Webhooks
Webhooks send real-time HTTP POST notifications to your server when events occur in Envizion AI. Use them to trigger downstream workflows, update dashboards, or send alerts without polling the API.
How Webhooks Work
Register a webhook
Create a webhook with a URL and list of events you want to receive.
Events are triggered
When a subscribed event occurs (e.g., a run completes), we prepare a JSON payload.
Signed delivery
We POST the payload to your URL with an HMAC-SHA256 signature header for verification.
Your server processes the event
Verify the signature, process the event, and respond with a 2xx status code within 10 seconds.
Available Events
There are 8 event types you can subscribe to. Click any event to see its payload format.
Setting Up Webhooks
Via API
curl -s -X POST https://api.envizion.ai/v1/webhooks \
-H "X-API-Key: vk_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/vision",
"events": ["run.completed", "run.failed", "render.completed"]
}'Save your signing secret
The response includes a secret field (e.g., whsec_...). This is only shown once. Store it securely -- you will need it to verify webhook signatures.
Signature Verification
Every webhook delivery includes an X-Vision-Signature header containing an HMAC-SHA256 hash of the request body, signed with your webhook secret. Always verify this signature before processing the event.
How it works
- We compute
HMAC-SHA256(webhook_secret, request_body) - The result is sent as
X-Vision-Signature: sha256=<hex_digest> - On your server, compute the same HMAC and compare using a constant-time comparison
Delivery Headers
| Header | Description |
|---|---|
X-Vision-Signature | HMAC-SHA256 signature: sha256=<hex> |
X-Vision-Event | Event type (e.g., run.completed) |
Content-Type | application/json |
Python Verification
import hashlib
import hmac
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""Verify an Envizion AI webhook signature."""
if not signature.startswith("sha256="):
return False
expected = signature[7:] # Strip "sha256=" prefix
computed = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, expected)
# In your Flask/FastAPI handler:
@app.post("/webhooks/vision")
async def handle_webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Vision-Signature", "")
if not verify_webhook(body, signature, WEBHOOK_SECRET):
return JSONResponse(status_code=401, content={"error": "Invalid signature"})
data = json.loads(body)
event = data["event"]
if event == "run.completed":
# Process completed run
pass
return {"ok": True}TypeScript Verification
import crypto from "crypto";
import type { Request, Response } from "express";
function verifyWebhook(
payload: string,
signature: string,
secret: string
): boolean {
if (!signature.startsWith("sha256=")) return false;
const expected = signature.slice(7);
const computed = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(expected)
);
}
// Express handler
app.post("/webhooks/vision", (req: Request, res: Response) => {
const payload = JSON.stringify(req.body);
const signature = req.headers["x-vision-signature"] as string;
if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const { event, data } = req.body;
if (event === "run.completed") {
// Process completed run
}
res.json({ ok: true });
});Using the Python SDK
from envizion.webhooks import verify_webhook_signature
# Returns True if valid, raises ValueError if format is wrong
is_valid = verify_webhook_signature(
payload=request_body,
signature=request.headers["X-Vision-Signature"],
secret="whsec_your_secret_here",
tolerance=300 # Max age in seconds (5 min)
)Testing with cURL
# Send a test event to your webhook
curl -s -X POST https://api.envizion.ai/v1/webhooks/{webhook_id}/test \
-H "X-API-Key: vk_your_key"
# Response:
# {"status": "delivered", "response_status": 200, "response_body": "OK"}Retry Policy
If your endpoint does not respond with a 2xx status within 10 seconds, we retry delivery with exponential backoff.
| Attempt | Delay | Cumulative |
|---|---|---|
| 1st attempt | Immediate | 0s |
| 2nd attempt | 30 seconds | 30s |
| 3rd attempt | 2 minutes | 2m 30s |
| 4th attempt (final) | 10 minutes | 12m 30s |
If all 4 attempts fail, the delivery is marked as failed and the webhook failure counter is incremented.
Auto-Disable Behavior
After 10 consecutive delivery failures, the webhook is automatically disabled to prevent further failed deliveries. The failure counter resets to 0 after any successful delivery.
To re-enable a disabled webhook, delete it and create a new one. Check the failure_count field in the webhook list response to monitor health.
Delivery Log
Every delivery attempt is logged. Use the deliveries endpoint to inspect payloads, response codes, and retry attempts:
curl -s "https://api.envizion.ai/v1/webhooks/{webhook_id}/deliveries?limit=10" \
-H "X-API-Key: vk_your_key"
# Response:
{
"deliveries": [
{
"id": "del_1...",
"event_type": "run.completed",
"payload": {"event": "run.completed", "data": {...}},
"response_status": 200,
"response_body": "OK",
"attempt": 1,
"created_at": "2026-02-12T15:33:00Z"
}
]
}Best Practices
Respond with 2xx quickly
Return a 200 or 202 status as fast as possible (within 10 seconds). Process the event asynchronously in a background job if needed.
Always verify signatures
Never skip signature verification, even in development. This prevents replay attacks and ensures the payload has not been tampered with.
Handle duplicate deliveries
Due to retries, you may receive the same event more than once. Use the run_id or render_id as an idempotency key to deduplicate.
Use HTTPS endpoints
Always use HTTPS URLs for webhook endpoints. While HTTP is accepted for development, it provides no transport security.
Monitor failure counts
Periodically check the failure_count on your webhooks. If it is climbing, investigate your endpoint's availability.
Subscribe only to events you need
Do not subscribe to all events. Only receive the events your integration actually processes to reduce noise and load.