CertWatch — Webhooks Integration Guide
CertWatch sends real-time HTTP notifications (webhooks) when certificate events occur. This guide explains how to integrate your system as a webhook subscriber.
Overview
CertWatch ──── certificate event ────▶ Your System
POST to your URL
with JSON payload
and API key header
When a certificate is created, updated, or approaching expiry, CertWatch delivers a JSON payload to every matching subscriber's webhook URL.
Quick Start
1. Create a Subscriber
POST /api/webhooks/subscribers/
Authorization: Token <your-token>
{
"name": "My ERP System",
"webhook_url": "https://erp.example.com/webhooks/certwatch"
}
Response:
{
"id": "subscriber-uuid",
"name": "My ERP System",
"webhook_url": "https://erp.example.com/webhooks/certwatch",
"api_key": "auto-generated-api-key",
"is_active": true,
"subscriptions_count": 0,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
Save the api_key — your endpoint will receive it in every webhook request for authentication.
2. Create a Subscription
POST /api/webhooks/subscriptions/
Authorization: Token <your-token>
{
"subscriber_id": "subscriber-uuid",
"event_types": ["NEW_CERTIFICATE", "CERTIFICATE_EXPIRING", "CERTIFICATE_EXPIRED"],
"organization_id": null,
"certificate_type_id": null
}
Setting organization_id and certificate_type_id to null subscribes to events for all organizations and all certificate types. Provide specific UUIDs to narrow the scope.
3. Receive Webhooks
CertWatch sends an HTTP POST to your webhook_url for each matching event.
Event Types
| Event | Trigger |
|---|---|
NEW_CERTIFICATE |
A new certificate record is created (via scraping, bulk upload, manual creation, or import) |
CERTIFICATE_UPDATED |
A watched field on an existing certificate changes (status, expiry_date, issuing_body, scope, document_url) |
CERTIFICATE_EXPIRING |
A certificate is approaching its expiry date (sent at 30 days and 7 days before expiry) |
CERTIFICATE_EXPIRED |
A certificate has passed its expiry date |
Expiry Notifications
CertWatch runs a periodic check (via Celery Beat) that scans for certificates nearing expiry. Notifications use windowed deduplication to prevent duplicate alerts:
- 30-day window (8–30 days before expiry) — sends
CERTIFICATE_EXPIRINGat most once per 25-day cooldown period - 7-day window (1–7 days before expiry) — sends
CERTIFICATE_EXPIRINGat most once per 6-day cooldown period - After expiry date — sends
CERTIFICATE_EXPIRED
Each certificate tracks a last_expiry_notification_at timestamp. When the Expiry Checker runs, it skips dispatching if the certificate was already notified within the cooldown period for its current window. After a successful dispatch, last_expiry_notification_at is updated to the current time.
If a certificate's expiry_date is updated (e.g., after renewal), last_expiry_notification_at is automatically reset to allow fresh notifications for the new expiry date.
Webhook Delivery
HTTP Request
CertWatch sends a POST request to your webhook URL with:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-API-Key |
The subscriber's auto-generated API key |
X-Webhook-Signature |
sha256=<hex> — HMAC-SHA256 of the payload body using the subscriber's signing_secret |
Payload Format
{
"event_type": "NEW_CERTIFICATE",
"timestamp": "2025-01-15T10:30:00.000000+00:00",
"certificate": {
"id": "cert-uuid",
"certificate_number": "ISO-9001-2024-12345",
"certificate_type": {
"code": "ISO_9001",
"name": "ISO 9001:2015"
},
"organization": {
"id": "org-uuid",
"external_id": "ORG-001",
"name": "Acme Corp"
},
"issuing_body": "TÜV SÜD",
"scope": "Design and manufacturing of electronic components",
"issue_date": "2024-01-15",
"expiry_date": "2027-01-14",
"status": "ACTIVE",
"document_url": "https://example.com/cert.pdf",
"source_url": "https://acme.com/certificates",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
Authenticating Incoming Webhooks
CertWatch sends two authentication headers with every webhook delivery:
| Header | Purpose |
|---|---|
X-API-Key |
Matches the subscriber's api_key — simple shared-secret authentication |
X-Webhook-Signature |
sha256=<hex> — HMAC-SHA256 signature of the request body using the subscriber's signing_secret |
We recommend verifying the HMAC signature for strongest security. The X-API-Key header is retained for backward compatibility.
Verifying the HMAC Signature
The signature is computed over the raw JSON request body using the subscriber's signing_secret as the HMAC key. To verify:
- Read the raw request body bytes.
- Compute
HMAC-SHA256(signing_secret, body). - Compare the hex digest against the value after
sha256=in theX-Webhook-Signatureheader.
Python example (Django):
import hmac
import hashlib
import json
def webhook_receiver(request):
# Verify HMAC signature
signing_secret = "your-subscriber-signing-secret"
signature_header = request.headers.get("X-Webhook-Signature", "")
expected_sig = "sha256=" + hmac.new(
signing_secret.encode(),
request.body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature_header, expected_sig):
return HttpResponse(status=401)
# Optionally also verify API key
expected_key = "your-subscriber-api-key"
if request.headers.get("X-API-Key") != expected_key:
return HttpResponse(status=401)
payload = json.loads(request.body)
event_type = payload["event_type"]
certificate = payload["certificate"]
# Process the event...
return HttpResponse(status=200)
JavaScript example (Express.js):
const crypto = require('crypto');
app.post('/webhooks/certwatch', (req, res) => {
// Verify HMAC signature
const signingSecret = process.env.CERTWATCH_SIGNING_SECRET;
const signatureHeader = req.headers['x-webhook-signature'] || '';
const expectedSig = 'sha256=' + crypto
.createHmac('sha256', signingSecret)
.update(JSON.stringify(req.body))
.digest('hex');
if (signatureHeader !== expectedSig) {
return res.sendStatus(401);
}
// Optionally also verify API key
const expectedKey = process.env.CERTWATCH_API_KEY;
if (req.headers['x-api-key'] !== expectedKey) {
return res.sendStatus(401);
}
const { event_type, certificate } = req.body;
// Process the event...
res.sendStatus(200);
});
Note: Use a constant-time comparison (e.g.,
hmac.compare_digestin Python,crypto.timingSafeEqualin Node.js) to prevent timing attacks when comparing signatures.
Retry Policy
If your endpoint returns a non-2xx status code or the request times out (30 seconds), CertWatch retries delivery:
| Attempt | Timing |
|---|---|
| 1st attempt | Immediate |
| 2nd attempt | ~2 seconds later (with jitter) |
| 3rd attempt | ~4 seconds later (exponential backoff, max 60s) |
After 3 failed attempts, the delivery is marked as FAILED. Failed deliveries are visible in the delivery log.
Subscription Filtering
Subscriptions support fine-grained filtering:
| Field | Behavior |
|---|---|
organization_id |
null = all organizations; UUID = specific organization only |
certificate_type_id |
null = all certificate types; UUID = specific type only |
event_types |
Array of event types to receive (must include at least one) |
Examples
Subscribe to all events for a specific organization:
{
"subscriber_id": "uuid",
"organization_id": "org-uuid",
"certificate_type_id": null,
"event_types": ["NEW_CERTIFICATE", "CERTIFICATE_UPDATED", "CERTIFICATE_EXPIRING", "CERTIFICATE_EXPIRED"]
}
Subscribe only to new ISO 9001 certificates across all organizations:
{
"subscriber_id": "uuid",
"organization_id": null,
"certificate_type_id": "iso9001-type-uuid",
"event_types": ["NEW_CERTIFICATE"]
}
Managing Subscribers
Disable a Subscriber
PATCH /api/webhooks/subscribers/{id}/
{
"is_active": false
}
When a subscriber is inactive, no deliveries are attempted. Existing pending deliveries for inactive subscribers are skipped.
Rotate API Key
POST /api/webhooks/subscribers/{id}/rotate-key/
Generates a new api_key for the subscriber. The previous key is immediately invalidated. All existing subscriptions, delivery logs, and subscriber metadata are preserved.
Response (200):
{
"api_key": "new-auto-generated-api-key"
}
Rotate Signing Secret
POST /api/webhooks/subscribers/{id}/rotate-signing-secret/
Generates a new signing_secret for HMAC signature verification. The previous secret is immediately invalidated. Update your webhook receiver to use the new secret.
Response (200):
{
"signing_secret": "new-auto-generated-signing-secret"
}
Manual Delivery Retry
Failed webhook deliveries can be retried manually:
POST /api/webhooks/deliveries/{id}/retry/
This resets the delivery status from FAILED to PENDING and enqueues a new delivery task. Only deliveries with status FAILED can be retried — attempting to retry a PENDING or SUCCESS delivery returns HTTP 400.
Response (200):
{
"status": "retry_queued",
"delivery_id": "delivery-uuid"
}
Error (400):
{
"error": "Only FAILED deliveries can be retried. Current status: SUCCESS"
}
Delivery Log
All delivery attempts are logged and queryable:
GET /api/webhooks/deliveries/
GET /api/webhooks/deliveries/{id}/
Query Parameters:
| Parameter | Description |
|---|---|
subscriber |
Filter by subscriber UUID |
event_type |
Filter by event type |
status |
Filter by delivery status: PENDING, SUCCESS, FAILED |
Delivery record:
{
"id": "delivery-uuid",
"subscriber": {
"id": "uuid",
"name": "My ERP System",
"is_active": true
},
"event_type": "NEW_CERTIFICATE",
"payload": { "...full payload..." },
"status": "SUCCESS",
"response_status": 200,
"error_message": null,
"attempts": 1,
"created_at": "2025-01-15T10:30:00Z",
"delivered_at": "2025-01-15T10:30:01Z"
}
Best Practices
- Respond quickly — Return a 2xx status within 30 seconds. Process the payload asynchronously if needed.
- Handle duplicates — Use the
certificate.idandevent_typeto deduplicate. The same event may be delivered more than once in edge cases. - Verify the HMAC signature — Use the
X-Webhook-Signatureheader for strongest payload integrity verification. Fall back toX-API-Keyif HMAC is not yet implemented on your side. - Use HTTPS — Webhook URLs should use HTTPS in production to protect payload contents in transit.
- Monitor deliveries — Check the delivery log periodically for failed deliveries. Use the manual retry endpoint to recover from transient failures.
- Handle all subscribed events — Your endpoint should gracefully handle any event type it's subscribed to, even if you only act on some.
- Rotate credentials regularly — Use the key rotation and signing secret rotation endpoints to refresh credentials without losing subscription history.