Claude
Skills
Sign in
Back

bknd-webhooks

Included with Lifetime
$97 forever

Use when configuring webhook integrations in Bknd. Covers receiving incoming webhooks via HTTP triggers, sending outgoing webhooks with FetchTask, event-triggered webhooks on data changes, signature verification, retry patterns, and async processing.

General

What this skill does


# Webhooks

Configure webhook integrations for receiving external events and sending notifications on data changes.

## Prerequisites

- Running Bknd instance
- Understanding of HTTP and webhooks concept
- Familiarity with Bknd Flows (see `bknd-custom-endpoint`)

## When to Use UI Mode

Webhook configuration requires code. No UI approach available.

## When to Use Code Mode

- Receiving webhooks from external services (Stripe, GitHub, etc.)
- Sending notifications when data changes
- Integrating with third-party services
- Building event-driven architectures

## Webhook Types

| Type | Description | Approach |
|------|-------------|----------|
| **Incoming** | Receive webhooks from external services | HTTP Trigger + Flow |
| **Outgoing** | Send webhooks when events occur | EventTrigger + FetchTask |

---

## Receiving Incoming Webhooks

### Step 1: Basic Webhook Receiver

```typescript
import { App, Flow, HttpTrigger, Task } from "bknd";
import { s } from "bknd/utils";

class WebhookReceiverTask extends Task<typeof WebhookReceiverTask.schema> {
  override type = "webhook-receiver";
  static override schema = s.strictObject({});

  override async execute(input: Request) {
    const body = await input.json();
    const eventType = input.headers.get("x-event-type");

    console.log(`Received webhook: ${eventType}`, body);

    return { received: true, event: eventType };
  }
}

const receiverTask = new WebhookReceiverTask("receive", {});

const webhookFlow = new Flow("incoming-webhook", [receiverTask]);
webhookFlow.setRespondingTask(receiverTask);

webhookFlow.setTrigger(
  new HttpTrigger({
    path: "/webhooks/external",
    method: "POST",
    mode: "async",  // Return 200 immediately
  })
);

const app = new App({
  flows: { flows: [webhookFlow] },
});
```

### Step 2: Webhook with Signature Verification

```typescript
import { createHmac, timingSafeEqual } from "crypto";

class SecureWebhookTask extends Task<typeof SecureWebhookTask.schema> {
  override type = "secure-webhook";

  static override schema = s.strictObject({
    secret: s.string(),
  });

  override async execute(input: Request) {
    const signature = input.headers.get("x-webhook-signature");
    const body = await input.text();

    // Verify signature
    if (!this.verifySignature(body, signature)) {
      throw this.error("Invalid signature", { signature });
    }

    const data = JSON.parse(body);
    return { verified: true, data };
  }

  private verifySignature(payload: string, signature: string | null): boolean {
    if (!signature) return false;

    const expected = createHmac("sha256", this.params.secret)
      .update(payload)
      .digest("hex");

    const sig = Buffer.from(signature);
    const exp = Buffer.from(`sha256=${expected}`);

    return sig.length === exp.length && timingSafeEqual(sig, exp);
  }
}

const secureTask = new SecureWebhookTask("verify", {
  secret: process.env.WEBHOOK_SECRET!,
});

const secureFlow = new Flow("secure-webhook", [secureTask]);
secureFlow.setRespondingTask(secureTask);
secureFlow.setTrigger(
  new HttpTrigger({
    path: "/webhooks/secure",
    method: "POST",
  })
);
```

### Step 3: Stripe Webhook Receiver

```typescript
import Stripe from "stripe";

class StripeWebhookTask extends Task<typeof StripeWebhookTask.schema> {
  override type = "stripe-webhook";

  static override schema = s.strictObject({
    webhookSecret: s.string(),
  });

  override async execute(input: Request) {
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
    const sig = input.headers.get("stripe-signature")!;
    const body = await input.text();

    let event: Stripe.Event;

    try {
      event = stripe.webhooks.constructEvent(
        body,
        sig,
        this.params.webhookSecret
      );
    } catch (err) {
      throw this.error("Webhook verification failed", { err });
    }

    // Handle event types
    switch (event.type) {
      case "checkout.session.completed":
        const session = event.data.object;
        // Process successful payment...
        break;

      case "customer.subscription.deleted":
        // Handle subscription cancellation...
        break;
    }

    return { received: true, type: event.type };
  }
}
```

### Step 4: GitHub Webhook Receiver

```typescript
class GitHubWebhookTask extends Task<typeof GitHubWebhookTask.schema> {
  override type = "github-webhook";

  static override schema = s.strictObject({
    secret: s.string(),
  });

  override async execute(input: Request) {
    const event = input.headers.get("x-github-event");
    const delivery = input.headers.get("x-github-delivery");
    const signature = input.headers.get("x-hub-signature-256");
    const body = await input.text();

    // Verify GitHub signature
    const expected = createHmac("sha256", this.params.secret)
      .update(body)
      .digest("hex");

    if (signature !== `sha256=${expected}`) {
      throw this.error("Invalid GitHub signature");
    }

    const payload = JSON.parse(body);

    switch (event) {
      case "push":
        console.log(`Push to ${payload.ref} by ${payload.pusher.name}`);
        break;

      case "pull_request":
        console.log(`PR ${payload.action}: ${payload.pull_request.title}`);
        break;

      case "issues":
        console.log(`Issue ${payload.action}: ${payload.issue.title}`);
        break;
    }

    return { event, delivery };
  }
}
```

### Step 5: Plugin-Based Webhook Receiver

For simpler cases, use plugin routes:

```typescript
import { createPlugin } from "bknd";
import { Hono } from "hono";

const webhooksPlugin = createPlugin({
  name: "webhooks",

  onServerInit: (server) => {
    const webhooks = new Hono();

    // Stripe
    webhooks.post("/stripe", async (c) => {
      const sig = c.req.header("stripe-signature");
      const body = await c.req.text();
      // Verify and process...
      return c.json({ received: true });
    });

    // GitHub
    webhooks.post("/github", async (c) => {
      const event = c.req.header("x-github-event");
      const body = await c.req.json();
      // Process...
      return c.json({ received: true });
    });

    // Generic
    webhooks.post("/:source", async (c) => {
      const source = c.req.param("source");
      const body = await c.req.json();
      console.log(`Webhook from ${source}:`, body);
      return c.json({ received: true });
    });

    server.route("/webhooks", webhooks);
  },
});
```

---

## Sending Outgoing Webhooks

Use Flows with EventTrigger to send webhooks when data changes.

### Step 1: Basic Outgoing Webhook

```typescript
import { App, Flow, FetchTask, EventTrigger } from "bknd";

// Task to send webhook
const sendWebhook = new FetchTask("send-webhook", {
  url: "https://example.com/webhook",
  method: "POST",
  headers: [
    { key: "Content-Type", value: "application/json" },
    { key: "X-Webhook-Source", value: "my-app" },
  ],
  body: "{{JSON.stringify(input)}}",  // Forward event data
});

const webhookFlow = new Flow("outgoing-webhook", [sendWebhook]);

// Trigger on data event
webhookFlow.setTrigger(
  new EventTrigger({
    event: "mutator-insert-after",  // After record created
    mode: "async",
  })
);

const app = new App({
  flows: { flows: [webhookFlow] },
});
```

### Step 2: Entity-Specific Webhook

```typescript
import { App, Flow, FetchTask, Task, EventTrigger } from "bknd";
import { s } from "bknd/utils";

// Filter task to check entity
class EntityFilterTask extends Task<typeof EntityFilterTask.schema> {
  override type = "entity-filter";

  static override schema = s.strictObject({
    targetEntity: s.string(),
  });

  override async execute(input: any) {
    if (input.entity?.name !== this.params.targetEntity) {
      throw this.error("Skip - wrong entity");
    }
    return input;
  }
}

const filterTask = new EntityFilterTask("filter", {
  targetEntity: "orders",
});

const sendWebhook = new FetchTask("send", {
  url: "https://api.example.com/orders/webhook",
  metho
Files: 1
Size: 18.5 KB
Complexity: 23/100
Category: General

Related in General