BillKit/Docs Console
Webhook endpoints

Create a webhook endpoint

Registers a URL to deliver events to and returns the signing secret you verify deliveries against.

POST/v1/webhook_endpoints
Operation create_webhook_endpoint · Scope webhook_endpoints:write · Spec snapshot 2026-09-13

Parameters

ParameterDescription
urlstring (uri)requiredMust be an absolute URL. At most 2083 characters.
descriptionstring | nulloptionalAt most 255 characters.
enabled_eventsarray of stringoptionalNo description in the spec.

Returns

The endpoint, with secret populated. This is the only response that ever carries it: every later read returns secret_fingerprint instead. Store the secret when you get it. If you lose it, rotate.

Leave enabled_events off and the endpoint receives everything. Rotation keeps the previous secret valid for 24 hours, so you can deploy the new one without dropping deliveries signed with the old.

Verify every payload. Compare the BillKit-Signature header against this endpoint’s secret before trusting the body.

Test it

Point the endpoint at a tunnel, or skip registration entirely and forward deliveries straight to localhost with billkit listen.

Errors

Status Cause
401 Missing or invalid API key.
403 Key is missing the webhook_endpoints:write scope.
422 Validation Error
429 Rate limited. Back off and retry.
Request · Node.js
import { verifyWebhookSignature, WebhookVerificationError } from "@billkit-eu/sdk";

// Express, Hono, Bun.serve, Workers: anything that hands you the raw body.
app.post("/webhooks/billkit", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = await verifyWebhookSignature({
      payload: req.body,
      signatureHeader: req.headers["billkit-signature"],
      secret: process.env.BILLKIT_WEBHOOK_SECRET,
    });
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.status(400).send("invalid signature");
    throw err;
  }

  if (event.type === "invoice.payment_failed") {
    await dunning.start(event.data.customer_id);
  }
  res.json({ received: true });
});
Request · Python
import os

from billkit import WebhookSignature, WebhookVerificationError

@app.post("/webhooks/billkit")
def billkit_webhook():
    try:
        event = WebhookSignature.verify(
            payload=request.data,
            signature_header=request.headers.get("BillKit-Signature"),
            secret=os.environ["BILLKIT_WEBHOOK_SECRET"],
        )
    except WebhookVerificationError:
        return "invalid signature", 400

    if event["type"] == "invoice.payment_failed":
        dunning.start(event["data"]["customer_id"])

    return {"received": True}
Request · PHP
<?php

use BillKit\Webhooks;
use BillKit\Exception\WebhookVerificationException;

try {
    $event = Webhooks::verifySignature(
        payload: file_get_contents('php://input'),
        signatureHeader: $_SERVER['HTTP_BILLKIT_SIGNATURE'] ?? null,
        secret: getenv('BILLKIT_WEBHOOK_SECRET'),
    );
} catch (WebhookVerificationException $e) {
    http_response_code(400);
    exit('invalid signature');
}

if ($event['type'] === 'invoice.payment_failed') {
    Dunning::start($event['data']['customer_id']);
}
Request · Laravel
<?php

namespace App\Listeners;

use BillKit\Laravel\Events\WebhookReceived;

// The package registers POST /billkit/webhook and verifies the
// signature before dispatching this event.
class HandleBillKitWebhook
{
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['type'] === 'invoice.payment_failed') {
            Dunning::start($event->payload['data']['customer_id']);
        }
    }
}
Request · React
// Webhooks are server to server, and the signing secret must never
// reach a browser. Handle the event on your backend and let the page
// read the state your own API exposes afterwards.
Request · curl
curl -X POST https://api.billkit.eu/v1/webhook_endpoints \
  -H "Authorization: Bearer $BILLKIT_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://acme.example/welcome"}'
Request body · every field
{
  "description": "string",
  "enabled_events": [
    "string"
  ],
  "url": "https://acme.example/welcome"
}
Response · 200 OK
{
  "created": 1789392000,
  "description": "string",
  "enabled_events": [
    "string"
  ],
  "id": "we_9XKp2vQ1",
  "livemode": false,
  "object": "webhook_endpoint",
  "previous_secret_fingerprint": "string",
  "previous_secret_retired_at": 1789392000,
  "secret": "string",
  "secret_fingerprint": "string",
  "status": "string",
  "url": "https://acme.example/welcome"
}