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_EXPIRING at most once per 25-day cooldown period
  • 7-day window (1–7 days before expiry) — sends CERTIFICATE_EXPIRING at 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:

  1. Read the raw request body bytes.
  2. Compute HMAC-SHA256(signing_secret, body).
  3. Compare the hex digest against the value after sha256= in the X-Webhook-Signature header.

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_digest in Python, crypto.timingSafeEqual in 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

  1. Respond quickly — Return a 2xx status within 30 seconds. Process the payload asynchronously if needed.
  2. Handle duplicates — Use the certificate.id and event_type to deduplicate. The same event may be delivered more than once in edge cases.
  3. Verify the HMAC signature — Use the X-Webhook-Signature header for strongest payload integrity verification. Fall back to X-API-Key if HMAC is not yet implemented on your side.
  4. Use HTTPS — Webhook URLs should use HTTPS in production to protect payload contents in transit.
  5. Monitor deliveries — Check the delivery log periodically for failed deliveries. Use the manual retry endpoint to recover from transient failures.
  6. Handle all subscribed events — Your endpoint should gracefully handle any event type it's subscribed to, even if you only act on some.
  7. Rotate credentials regularly — Use the key rotation and signing secret rotation endpoints to refresh credentials without losing subscription history.