Webhooks
Base URL: https://events.pana.space/api
Configure outbound webhooks to receive real-time notifications when events happen in your workspace.
Requires: workspace admin role.
Base path:
/api/v1/{workspace_id}/developers/webhooks/
List webhooks
GET /api/v1/{workspace_id}/developers/webhooks/
Authorization: Bearer <jwt>
[
{
"id": "uuid",
"target_url": "https://example.com/webhooks/mathiverse",
"events": ["lead.created", "task.assigned"],
"is_active": true,
"created_at": "2026-06-01T09:00:00Z"
}
]
Create webhook
POST /api/v1/{workspace_id}/developers/webhooks/
Authorization: Bearer <jwt>
Content-Type: application/json
{
"target_url": "https://example.com/webhooks/mathiverse",
"events": ["lead.created", "lead.updated", "task.assigned"]
}
At least one event is required. Returns 201 with webhook object.
Update webhook
PATCH /api/v1/{workspace_id}/developers/webhooks/{webhook_id}/
Authorization: Bearer <jwt>
Content-Type: application/json
{
"target_url": "https://example.com/new-url",
"events": ["expense.submitted"]
}
Delete webhook
DELETE /api/v1/{workspace_id}/developers/webhooks/{webhook_id}/
Soft-deactivates the webhook (204 No Content).
List available event types
GET /api/v1/{workspace_id}/developers/webhook-events/
Authorization: Bearer <jwt>
[
{ "id": "lead.created", "label": "Lead created" },
{ "id": "lead.updated", "label": "Lead updated" },
{ "id": "event.updated", "label": "Event updated" },
{ "id": "demo_booking.updated", "label": "Demo booking updated" },
{ "id": "expense.submitted", "label": "Expense submitted" },
{ "id": "task.assigned", "label": "Task assigned" }
]
Supported event types
| Event | Description |
|---|---|
lead.created | New lead captured |
lead.assigned | Lead assigned (also matches lead.updated subscriptions) |
lead.status_changed | Lead status changed |
lead.updated | Lead updated |
event.updated | Event updated |
event.cancelled | Event cancelled |
demo_booking.created | Demo booking created |
demo_booking.status_changed | Demo status changed |
demo_booking.updated | Demo booking updated |
expense.created | Expense created |
expense.status_changed | Expense status changed |
expense.submitted | Expense submitted |
task.assigned | Task assigned |
task.status_changed | Task status changed |
admin.member_joined | Member joined workspace |
admin.role_changed | Member role changed |
Aliases: subscribing to lead.updated also receives lead.status_changed and lead.assigned. Similar aliases exist for demo bookings and expenses.
Delivery payload
POST to your target_url:
{
"id": "delivery-uuid",
"type": "lead.created",
"created_at": "2026-06-20T10:00:00.000000+00:00",
"data": {
"lead_id": "…",
"workspace_id": "…"
}
}
Headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Mathiverse-Webhooks/1.0 |
X-Webhook-Event | Event type (e.g. lead.created) |
X-Webhook-Signature | sha256=<hmac_hex> |
Verify signature
import hashlib
import hmac
def verify(secret: str, body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)
The signing secret is generated server-side when the webhook is created. Retrieve it from Django admin or database if needed — it is not currently returned in the API response.
Retries
Deliveries run asynchronously via Celery. Failed deliveries (non-2xx or network error) retry up to 3 times with exponential backoff.
When webhooks fire
Webhooks are dispatched from the same notification pipeline as in-app notifications — when app code calls emit_notification_event() (lead create, task assign, expense submit, etc.).
Requires: Celery worker + Redis for async delivery.