Skip to content

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:

sha256=HMAC-SHA256(payload, secret)

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

curl https://your-ospobox.com/api/webhooks \
  -H "Authorization: Bearer YOUR_TOKEN"

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

  1. Always verify signatures - Never trust webhook payloads without verification
  2. Respond quickly - Return 200 within 30 seconds, process asynchronously if needed
  3. Handle duplicates - Use the event_id to deduplicate if needed
  4. Use HTTPS - Always use HTTPS endpoints in production
  5. Monitor failures - Check delivery history for failed attempts