Overview

kurnl sends HTTP POST requests to your webhook_url after significant events. Configure your webhook URL in Dashboard → Settings → Integration. All events share a common envelope structure with event-specific fields merged in at the top level.

Event reference

EventWhen firedKey fields
subscription.activatedSubscription becomes ACTIVEsubscription_id, subscriber_id, plan_version_id, location_hash
subscription.pending_installHome-drop signup received — install not yet donesubscription_id, subscriber_id, unit_id
subscription.cancelledSubscription cancelled from any sourcesubscription_id
provisioning.completedSSH port provisioning finished successfullyjob_id, subscription_id
provisioning.failedSSH provisioning failed after all retriesjob_id, subscription_id, error

Payload structure

{
  "event": "provisioning.completed",
  "provider_document_id": "a1b2c3d4-...",
  "timestamp": 1714000000.123,
  "job_id": "3d6e8f12-...",
  "subscription_id": "8c4a1b9e-..."
}
All payloads include event, provider_document_id, and timestamp. Event-specific fields are merged in alongside them.

Verifying signatures

When you have a webhook_secret configured, kurnl signs every request body with HMAC-SHA256 and sends the signature in the X-Webhook-Signature header as sha256=<hex_digest>. Always verify this signature before processing any event.
import hashlib
import hmac

def verify_kurnl_webhook(body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)

# In your handler:
sig = request.headers.get("X-Webhook-Signature", "")
if not verify_kurnl_webhook(request.body, sig, KURNL_WEBHOOK_SECRET):
    return Response(status_code=401)

Delivery behaviour

  • kurnl attempts delivery up to 3 times with exponential back-off (2s, then up to 30s between retries)
  • Network errors and 5xx responses are retried; 4xx responses are not retried
  • Timeout per attempt: 15 seconds
  • Return any 2xx status to acknowledge — kurnl does not inspect the response body

Idempotency

kurnl may deliver the same event more than once after transient failures. Use job_id or subscription_id as an idempotency key to deduplicate in your handler:
def handle_provisioning_completed(event: dict):
    job_id = event["job_id"]
    if already_processed(job_id):
        return  # skip duplicate
    mark_processed(job_id)
    # ... your logic

Testing webhooks locally

During development you can use a tunnelling tool to expose your local server to kurnl:
# Using ngrok:
ngrok http 8000
# → Forwarding: https://abc123.ngrok.io → localhost:8000

# Set this as your sandbox webhook URL in the dashboard:
# https://abc123.ngrok.io/webhooks/kurnl
Alternatively, use webhook.site for a no-setup temporary receiver to inspect payloads without running any code.

Debugging delivery failures

In the dashboard under Documentation → Testing & Sandbox, you can see the last 50 webhook delivery attempts for your sandbox, including the response status and body. Failed events can be replayed from the dashboard. For production webhook issues, check Dashboard → Settings → Integration → Webhook Events.