Overview
Xenia Webhooks provide real-time HTTP notifications when events occur in your workspace. Instead of polling for changes, your application receives instant updates when tasks change status, submissions are completed, or users are activated. Key Capabilities:- Real-time event notifications via HTTP POST
- Configurable event subscriptions (subscribe only to events you need)
- Secure signature verification using HMAC-SHA256
- Automatic retries with exponential backoff
- Dead letter queue for failed deliveries with replay capability
- Secret rotation with 24-hour grace period
Prerequisites
Before using webhooks:- Feature Flag:
WEBHOOKSmust be enabled for your workspace - Authentication: Valid API credentials (
x-client-keyandx-client-secret) - HTTPS Endpoint: Your webhook receiver must use HTTPS (required for production)
Quick Start
Step 1: Create a Webhook Subscription
Step 2: Store the Secret Securely
Store the webhook secret in your environment variables or secrets manager:Step 3: Implement Signature Verification
See the Signature Verification section below for implementation details.Step 4: Test Your Endpoint
Event Types
Tier 1 - Critical (Active)
These events are currently available:| Event Type | Description |
|---|---|
task.status_changed | Task status transitions (e.g., Open → In Progress → Completed) |
submission.submitted | Checklist submission completed |
submission.approved | Submission approved in approval workflow |
submission.rejected | Submission rejected in approval workflow |
user.invited | User invited to workspace |
user.activated | User completed onboarding and became active |
user.deactivated | User deactivated in workspace |
Tier 2 - Standard (Coming Soon)
| Event Type | Description |
|---|---|
task.created | New task created |
task.assigned | Task assignment changed |
recurring_task.instance_created | New instance of recurring task generated |
submission.approval_required | Submission requires approval |
template.published | Checklist template published |
template.archived | Checklist template archived |
Tier 3 - Extended (Future)
| Event Type | Description |
|---|---|
bulk_operation.completed | Bulk operation finished |
export.completed | Data export ready for download |
location.member_added | User added to location |
location.member_removed | User removed from location |
team.member_added | User added to team |
team.member_removed | User removed from team |
To get the current list of available event types, use the List Event Types endpoint.
Webhook Payload Format
Payload Structure
Every webhook delivery includes a JSON payload with this structure:| Field | Type | Description |
|---|---|---|
eventId | string | Unique identifier for this event (use for idempotency) |
eventType | string | The type of event that occurred |
timestamp | string | ISO-8601 timestamp when the event occurred |
workspaceId | string | Workspace where the event occurred |
data | object | Event-specific payload data |
metadata | object | Additional context (e.g., who triggered the event) |
HTTP Headers
Every webhook request includes these headers:| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Xenia-Signature | HMAC-SHA256 signature of the payload |
X-Xenia-Timestamp | Unix timestamp (seconds) when the webhook was sent |
X-Xenia-Event-Id | Unique event ID (same as eventId in payload) |
X-Xenia-Event-Type | Event type (same as eventType in payload) |
X-Xenia-Previous-Signature | Previous signature (only during secret rotation) |
User-Agent | Xenia-Webhooks/1.0 |
Receiving Webhooks
Event Payload Examples
task.status_changed
task.status_changed
submission.submitted
submission.submitted
user.invited
user.invited
submission.approved
submission.approved
user.activated
user.activated
Signature Verification
Always verify webhook signatures to ensure requests are genuinely from Xenia and haven’t been tampered with.Algorithm
The signature is computed as:Verification Steps
- Extract the
X-Xenia-TimestampandX-Xenia-Signatureheaders - Check that the timestamp is within 5 minutes of current time (prevents replay attacks)
- Compute the expected signature using your webhook secret
- Compare signatures using a timing-safe comparison
Implementation Examples
Handling Secret Rotation
During secret rotation, webhooks include both the current and previous signature:Retry Logic & Delivery
Delivery Behavior
| Setting | Value |
|---|---|
| Timeout | 30 seconds |
| Max Attempts | 5 |
| Backoff Schedule | 1s, 2s, 4s, 8s, 16s (exponential) |
| Success Codes | 200-299 |
Response Handling
| Response | Behavior |
|---|---|
2xx | Success - no retry |
429 (Rate Limited) | Retry with backoff |
5xx (Server Error) | Retry with backoff |
| Timeout | Retry with backoff |
| Connection Error | Retry with backoff |
4xx (except 429) | Non-retryable - moves to dead letter queue immediately |
Auto-Disable
Subscriptions are automatically disabled after too many consecutive failures. To re-enable:Dead Letter Queue
Failed webhooks (after 5 retry attempts) are moved to a dead letter queue with 28-day retention.Viewing Dead Letters
Replaying Failed Webhooks
Single Replay:Bulk replay requires at least one filter (
subscriptionId or eventType) and processes a maximum of 100 items per request.Secret Rotation
Rotate your webhook secret periodically for security. During rotation, both old and new secrets are valid for 24 hours.Rotation Workflow
- Initiate Rotation:
- Update Your Application: Deploy the new secret to your webhook handler
- Verify Both Secrets: During the 24-hour grace period, verify against both signatures
- Complete Migration: After 24 hours, only the new secret is valid
During rotation, all webhook deliveries include both
X-Xenia-Signature (new secret) and X-Xenia-Previous-Signature (old secret) headers.Subscription Configuration
Create/Update Options
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Subscription name (max 255 chars) |
url | string | Yes | HTTPS URL to receive webhooks (max 2048 chars) |
events | array | Yes | Event types to subscribe to (1-50 events) |
description | string | No | Description (max 2000 chars) |
rateLimit | integer | No | Requests per minute (1-1000, default: 100) |
metadata | object | No | Custom key-value pairs |
isActive | boolean | No | Enable/disable subscription |
Example: Update Subscription
Monitoring & Analytics
Workspace Statistics
Subscription Logs
Best Practices
Return 200 immediately
Return 200 immediately
Process webhooks asynchronously to avoid timeouts. Your endpoint should acknowledge receipt within a few seconds, then process the data in the background.
Always verify signatures
Always verify signatures
Never skip signature verification in production. It protects against:
- Spoofed requests from attackers
- Replay attacks using captured webhooks
- Man-in-the-middle modifications
Implement idempotency
Implement idempotency
Use the
eventId to detect and handle duplicate deliveries. Store processed event IDs and skip duplicates:Rotate secrets regularly
Rotate secrets regularly
Rotate your webhook secrets periodically (e.g., every 90 days) to limit exposure if a secret is compromised. The 24-hour grace period allows seamless rotation without downtime.
Monitor dead letters
Monitor dead letters
Regularly check the dead letter queue for failed webhooks. Investigate persistent failures - they often indicate endpoint issues or payload handling problems.
Use HTTPS only
Use HTTPS only
All production webhook URLs must use HTTPS. HTTP URLs are rejected to ensure payload confidentiality and integrity.
Troubleshooting
| Issue | Solution |
|---|---|
| WEBHOOKS feature not enabled | Contact your admin to activate the WEBHOOKS feature flag |
| Invalid signature errors | Verify you’re using the correct secret and JSON stringification matches |
| Webhook timeouts | Return 200 within 30 seconds; process asynchronously |
| Events not arriving | Check subscription status (isActive) and URL accessibility |
| Subscription auto-disabled | Fix endpoint issues, then set isActive: true |
| Missing events | Verify the event types are in your subscription’s events array |
| Duplicate events | Implement idempotency using eventId |
Common Signature Issues
- Wrong secret: Ensure you’re using the secret from subscription creation (or the rotated secret)
- JSON serialization mismatch: Use
JSON.stringify()without pretty-printing or extra spaces - Encoding issues: Ensure UTF-8 encoding throughout
- Timestamp format: Use the raw header value, not parsed date object