Webhooks¶
Webhooks allow you to receive real-time notifications when events occur in Ospobox. Instead of polling the API, you can register an endpoint to receive HTTP POST requests when things happen.
Overview¶
When you create a webhook endpoint, Ospobox will send HTTP POST requests to your URL whenever subscribed events occur. Each request includes:
- A JSON payload describing the event
- An HMAC-SHA256 signature for verification
- Headers identifying the event type and delivery
Event Types¶
| Event | Description |
|---|---|
sync.completed |
Organization sync completed successfully |
sync.failed |
Organization sync failed |
alert.created |
New alert was created |
health.changed |
Repository health score changed significantly |
report.generated |
Report was generated |
All five are produced. health.changed fires when a repository's score
moves by 5 points or more between passes.
Event names are validated on create and update: an unknown name is a
400, and so is an empty list. An endpoint subscribed to nothing would
never fire, which is never what the caller meant.
Creating a Webhook¶
Via API¶
curl -X POST https://your-ospobox.com/api/webhooks \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My Webhook",
"url": "https://your-server.com/webhook",
"events": ["sync.completed", "alert.created"]
}'
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Webhook",
"url": "https://your-server.com/webhook",
"events": ["sync.completed", "alert.created"],
"is_active": true,
"created_at": "2026-03-01T12:00:00Z"
}
Payload Format¶
All webhook payloads follow this structure:
{
"event_type": "sync.completed",
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-03-01T12:00:00.000000Z",
"data": {
// Event-specific data
}
}
Example: sync.completed¶
{
"event_type": "sync.completed",
"event_id": "abc123",
"timestamp": "2026-03-01T12:00:00Z",
"data": {
"organization_id": "org-123",
"organization_name": "my-org",
"platform": "github",
"repositories_synced": 42,
"duration_seconds": 15.5
}
}
Example: alert.created¶
{
"event_type": "alert.created",
"event_id": "def456",
"timestamp": "2026-03-01T12:00:00Z",
"data": {
"alert_id": "alert-789",
"alert_type": "stale_repository",
"severity": "warning",
"repository_id": "repo-123",
"repository_name": "my-repo",
"message": "No commits in 45 days"
}
}
Verifying Signatures¶
Every webhook request includes a signature in the X-Webhook-Signature header. You should verify this signature to ensure the request came from Ospobox.
The signature is computed as:
Python Example¶
import hmac
import hashlib
def verify_signature(payload: bytes, secret: str, signature: str) -> bool:
"""Verify webhook signature."""
expected = "sha256=" + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your webhook handler:
@app.post("/webhook")
async def handle_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("X-Webhook-Signature")
if not verify_signature(payload, WEBHOOK_SECRET, signature):
raise HTTPException(401, "Invalid signature")
data = json.loads(payload)
# Process the event...
Node.js Example¶
const crypto = require('crypto');
function verifySignature(payload, secret, signature) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Request Headers¶
Each webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type |
Always application/json |
User-Agent |
Ospobox-Webhook/1.0 |
X-Webhook-Signature |
HMAC-SHA256 signature |
X-Webhook-Event |
Event type (e.g., sync.completed) |
X-Webhook-Delivery |
Unique delivery ID |
Delivery¶
Deliveries are queued when the event happens and sent by a pass that runs every minute, plus one kicked immediately after each sync, so a signed callback normally lands within seconds of the event.
Each delivery is claimed before it is sent, so two passes can never send the same event twice. A worker that dies mid-delivery simply lets its claim expire and the next pass picks the delivery up.
If the endpoint's signing secret cannot be decrypted (usually a rotated
SECRET_KEY), the delivery fails with webhook secret could not be
decrypted. Signing it with a value your receiver cannot verify would
be worse. Regenerate the secret to recover.
Deliveries queued for an endpoint you then disable are marked failed, so nothing sits pending forever.
Retry Logic¶
If your endpoint returns a non-2xx status code or times out, Ospobox will retry the delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 4 hours (final) |
After 5 failed attempts, the delivery is marked as failed.
Testing Webhooks¶
You can send a test event to verify your endpoint is configured correctly:
curl -X POST https://your-ospobox.com/api/webhooks/{webhook_id}/test \
-H "Authorization: Bearer YOUR_TOKEN"
This sends a test sync.completed event and returns the delivery result.
Testing a disabled endpoint returns 409: the delivery pass skips
inactive endpoints, so queuing one would strand the delivery. Enable
the endpoint first.
Managing Webhooks¶
List Webhooks¶
Update Webhook¶
curl -X PUT https://your-ospobox.com/api/webhooks/{webhook_id} \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"events": ["sync.completed", "sync.failed", "alert.created"],
"is_active": true
}'
Delete Webhook¶
curl -X DELETE https://your-ospobox.com/api/webhooks/{webhook_id} \
-H "Authorization: Bearer YOUR_TOKEN"
View Delivery History¶
curl https://your-ospobox.com/api/webhooks/{webhook_id}/deliveries \
-H "Authorization: Bearer YOUR_TOKEN"
Regenerate Secret¶
curl -X POST https://your-ospobox.com/api/webhooks/{webhook_id}/regenerate-secret \
-H "Authorization: Bearer YOUR_TOKEN"
Best Practices¶
- Always verify signatures - Never trust webhook payloads without verification
- Respond quickly - Return 200 within 30 seconds, process asynchronously if needed
- Handle duplicates - Use the
event_idto deduplicate if needed - Use HTTPS - Always use HTTPS endpoints in production
- Monitor failures - Check delivery history for failed attempts