Webhooks
Webhooks allow your application to receive real-time notifications when events occur in CODITECT.
Overview
When an event occurs, CODITECT sends an HTTP POST request to your configured endpoint with details about the event.
CODITECT Event → Webhook Endpoint → Your Server
Setting Up Webhooks
Create Webhook Endpoint
POST /v1/webhooks
curl -X POST https://api.coditect.ai/v1/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/coditect",
"events": ["license.seat.acquired", "license.seat.released"],
"description": "Production webhook"
}'
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS endpoint URL |
events | array | Yes | Events to subscribe to |
description | string | No | Webhook description |
secret | string | No | Custom signing secret |
Response:
{
"data": {
"id": "wh_abc123",
"url": "https://yourapp.com/webhooks/coditect",
"events": ["license.seat.acquired", "license.seat.released"],
"secret": "whsec_xxx...",
"status": "active",
"created_at": "2026-01-09T10:00:00Z"
}
}
Save Your Secret
The webhook signing secret is only shown once. Save it securely to verify webhook signatures.
List Webhooks
GET /v1/webhooks
curl https://api.coditect.ai/v1/webhooks \
-H "Authorization: Bearer YOUR_TOKEN"
Delete Webhook
DELETE /v1/webhooks/{id}
curl -X DELETE https://api.coditect.ai/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer YOUR_TOKEN"
Event Types
License Events
| Event | Description |
|---|---|
license.seat.acquired | User acquired a license seat |
license.seat.released | User released a license seat |
license.seat.timeout | Seat released due to inactivity |
license.seats.exhausted | All seats are in use |
license.expiring | License expires within 7 days |
license.expired | License has expired |
Subscription Events
| Event | Description |
|---|---|
subscription.created | New subscription started |
subscription.updated | Subscription changed |
subscription.canceled | Subscription canceled |
subscription.expired | Subscription has ended |
Invoice Events
| Event | Description |
|---|---|
invoice.created | Invoice generated |
invoice.paid | Invoice payment successful |
invoice.payment_failed | Payment attempt failed |
Organization Events
| Event | Description |
|---|---|
org.member.invited | Member invitation sent |
org.member.joined | Member accepted invitation |
org.member.removed | Member was removed |
org.member.role_changed | Member role updated |
Webhook Payload
All webhook payloads follow this structure:
{
"id": "evt_abc123",
"type": "license.seat.acquired",
"created": "2026-01-09T10:30:00Z",
"data": {
"object": {
// Event-specific data
}
},
"organization_id": "org_xyz789"
}
Example: license.seat.acquired
{
"id": "evt_abc123",
"type": "license.seat.acquired",
"created": "2026-01-09T10:30:00Z",
"data": {
"object": {
"session_id": "sess_xyz789",
"license_id": "lic_abc123",
"user_id": "usr_def456",
"hardware_id": "hw_ghi789",
"active_seats": 4,
"max_seats": 10
}
}
}
Example: subscription.canceled
{
"id": "evt_def456",
"type": "subscription.canceled",
"created": "2026-01-09T11:00:00Z",
"data": {
"object": {
"subscription_id": "sub_abc123",
"plan_id": "plan_pro",
"cancel_at_period_end": true,
"current_period_end": "2026-02-01T00:00:00Z",
"canceled_by": "usr_abc123",
"reason": "Switching to annual plan"
}
}
}
Verifying Signatures
Always verify webhook signatures to ensure requests are from CODITECT.
Signature Header
X-Coditect-Signature: t=1704794400,v1=abc123...
Verification Process
- Extract timestamp and signature from header
- Construct signed payload:
{timestamp}.{request_body} - Compute HMAC-SHA256 with your webhook secret
- Compare with provided signature
- Verify timestamp is within 5 minutes
Python Example
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
"""Verify CODITECT webhook signature."""
# Parse header
parts = dict(item.split("=") for item in signature_header.split(","))
timestamp = parts.get("t")
signature = parts.get("v1")
# Check timestamp (prevent replay attacks)
if abs(time.time() - int(timestamp)) > 300: # 5 minutes
return False
# Compute expected signature
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
# Compare signatures (constant-time)
return hmac.compare_digest(expected, signature)
# Usage in Flask
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_xxx..."
@app.route("/webhooks/coditect", methods=["POST"])
def handle_webhook():
payload = request.get_data()
signature = request.headers.get("X-Coditect-Signature")
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
return "Invalid signature", 400
event = request.get_json()
event_type = event["type"]
if event_type == "license.seat.acquired":
handle_seat_acquired(event["data"]["object"])
elif event_type == "subscription.canceled":
handle_subscription_canceled(event["data"]["object"])
return "OK", 200
JavaScript Example
const crypto = require('crypto');
const express = require('express');
const WEBHOOK_SECRET = 'whsec_xxx...';
function verifyWebhook(payload, signatureHeader) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parts.t;
const signature = parts.v1;
// Check timestamp
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return false;
}
// Compute expected
const signedPayload = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/webhooks/coditect', (req, res) => {
const signature = req.headers['x-coditect-signature'];
if (!verifyWebhook(req.body.toString(), signature)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
Best Practices
1. Return Quickly
Respond with 200 OK immediately, then process asynchronously:
@app.route("/webhooks/coditect", methods=["POST"])
def handle_webhook():
event = request.get_json()
# Queue for async processing
queue.enqueue(process_webhook_event, event)
return "OK", 200 # Return immediately
2. Handle Retries
CODITECT retries failed webhooks:
- 1st retry: 1 minute
- 2nd retry: 5 minutes
- 3rd retry: 30 minutes
- 4th retry: 2 hours
- 5th retry: 24 hours
Use idempotency keys (event.id) to handle duplicates.
3. Monitor Deliveries
Check delivery status in the dashboard:
- Navigate to Settings → Webhooks → Delivery History
- View recent events, response codes, and retry attempts
4. Test with CLI
coditect webhooks test --event license.seat.acquired
This sends a test event to your endpoint.
Troubleshooting
Webhook Not Receiving Events
- Verify URL is publicly accessible
- Check firewall allows CODITECT IPs
- Ensure SSL certificate is valid
- Verify endpoint returns 2xx status
Signature Verification Failing
- Use raw request body (not parsed JSON)
- Ensure secret matches dashboard value
- Check server clock is synchronized
- Verify HMAC algorithm is SHA-256
Events Arriving Late
- Events are sent within seconds
- Check for processing delays on your end
- Consider async processing for reliability