agentsclimarketplace

Webhook subscriptions

Skill furkangonel/cowrangler/bundled_skills/devops/webhook-subscriptions

Autonomous terminal AI agent for workflows and feasible project procedures. Co-Worker Co-Wrangler πŸ™

Install
npx -y skills add furkangonel/cowrangler --skill webhook-subscriptions

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Design, implement, and debug webhook integrations with security and reliability.

SKILL.md

9.1 KB, as published. Nobody here has run it

Webhook Subscriptions SOP

When to Use

  • User wants to receive events from an external service (Stripe, GitHub, Shopify, Twilio, etc.)
  • User wants to send webhooks from their own service to subscribers
  • User is debugging why webhooks aren't being received or processed
  • User wants to validate webhook security or handle retries

Part 1 β€” Receiving Webhooks

Minimal Express.js Handler

import express from "express";
import crypto from "crypto";

const app = express();

// IMPORTANT: use raw body for signature validation, not parsed JSON
app.post(
  "/webhooks/stripe",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    // 1. Validate signature first β€” reject early if invalid
    const signature = req.headers["stripe-signature"] as string;
    const isValid = validateStripeSignature(req.body, signature);

    if (!isValid) {
      console.warn("Invalid webhook signature", { signature });
      return res.status(400).json({ error: "Invalid signature" });
    }

    // 2. Parse the payload
    const event = JSON.parse(req.body.toString());

    // 3. Respond 200 immediately β€” do not wait for processing
    res.status(200).json({ received: true });

    // 4. Process asynchronously (after responding)
    await processEvent(event).catch((err) => {
      console.error("Webhook processing failed", { eventId: event.id, err });
    });
  }
);

Why respond 200 immediately?

Most webhook providers retry on any non-2xx response or on timeout (typically 10–30 seconds). Long-running processing will cause unnecessary retries. Always acknowledge first, process after.


Part 2 β€” Signature Validation (HMAC)

Generic HMAC-SHA256 Validation

function validateWebhookSignature(
  payload: Buffer,
  receivedSig: string,
  secret: string
): boolean {
  const expectedSig = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  // Constant-time comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  );
}

Provider-Specific Signature Patterns

ProviderHeaderFormat
Stripestripe-signaturet=timestamp,v1=signature
GitHubx-hub-signature-256sha256=<hex>
Shopifyx-shopify-hmac-sha256Base64 encoded
Twiliox-twilio-signatureBase64 HMAC-SHA1 of URL + sorted params
Slackx-slack-signaturev0=<hex>, includes timestamp

Stripe Signature Example

function validateStripeSignature(
  payload: Buffer,
  sigHeader: string,
  secret: string = process.env.STRIPE_WEBHOOK_SECRET!
): boolean {
  const parts = sigHeader.split(",").reduce((acc, part) => {
    const [key, value] = part.split("=");
    acc[key] = value;
    return acc;
  }, {} as Record<string, string>);

  const timestamp = parts["t"];
  const receivedSig = parts["v1"];

  // Reject events older than 5 minutes (replay attack prevention)
  const tolerance = 300; // seconds
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > tolerance) {
    return false;
  }

  const signedPayload = `${timestamp}.${payload.toString()}`;
  const expectedSig = crypto
    .createHmac("sha256", secret)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  );
}

Part 3 β€” Idempotency

Webhook providers may deliver the same event more than once. Always handle duplicates.

Strategy 1 β€” Event ID Deduplication (Redis)

import { Redis } from "ioredis";
const redis = new Redis(process.env.REDIS_URL);

async function processEvent(event: WebhookEvent): Promise<void> {
  const lockKey = `webhook:processed:${event.id}`;
  const ttl = 60 * 60 * 24; // 24 hours

  // SET NX (only set if not exists) β€” atomic deduplication
  const acquired = await redis.set(lockKey, "1", "EX", ttl, "NX");
  if (!acquired) {
    console.log("Duplicate webhook, skipping", { eventId: event.id });
    return;
  }

  await handleEvent(event);
}

Strategy 2 β€” Database Upsert

INSERT INTO webhook_events (id, type, payload, processed_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (id) DO NOTHING;
-- Returns 0 rows affected if duplicate β€” check in application code
const result = await db.query(
  `INSERT INTO webhook_events (id, type, payload, processed_at)
   VALUES ($1, $2, $3, NOW())
   ON CONFLICT (id) DO NOTHING`,
  [event.id, event.type, JSON.stringify(event)]
);

if (result.rowCount === 0) {
  return; // already processed
}

Part 4 β€” Retry Handling

Exponential Backoff (for sending webhooks)

async function deliverWebhook(
  url: string,
  payload: object,
  attempt = 1
): Promise<void> {
  const maxAttempts = 5;
  const baseDelay = 1000; // 1 second

  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Webhook-Id": generateEventId(),
        "X-Webhook-Signature": signPayload(payload),
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(10_000), // 10s timeout
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
  } catch (err) {
    if (attempt >= maxAttempts) {
      console.error("Webhook delivery failed after max attempts", { url, attempt });
      await saveFailedWebhook(url, payload, err); // dead-letter queue
      return;
    }

    // Exponential backoff with jitter
    const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
    console.warn(`Webhook attempt ${attempt} failed, retrying in ${delay}ms`);
    await sleep(delay);
    return deliverWebhook(url, payload, attempt + 1);
  }
}

Retry Schedule Reference

AttemptDelay (typical)
1st retry5 seconds
2nd retry30 seconds
3rd retry5 minutes
4th retry30 minutes
5th retry2 hours

Part 5 β€” Local Testing

Option A β€” ngrok

# Install ngrok, then:
ngrok http 3000

# Your local port 3000 is now accessible at:
# https://abc123.ngrok.io

# Use that URL in the Stripe / GitHub webhook settings
# ngrok dashboard: http://localhost:4040 β€” inspect all requests/responses

Option B β€” smee.io (GitHub-native)

npm install --global smee-client
smee --url https://smee.io/your-channel-id --target http://localhost:3000/webhooks/github

Option C β€” Webhook.site

  1. Go to webhook.site β€” get a unique URL
  2. Point the external service at that URL
  3. Inspect the raw payload format (headers, body)
  4. Copy the exact payload for local testing with curl:
curl -X POST http://localhost:3000/webhooks/stripe \
  -H "Content-Type: application/json" \
  -H "stripe-signature: t=1234567890,v1=your_test_sig" \
  -d @payload.json

Part 6 β€” Error Response Codes

ScenarioResponseWhy
Success200 OKAlways β€” even if you'll process async
Invalid signature400 Bad RequestSignals misconfiguration to sender
Unsupported event type200 OKDon't cause retries for events you ignore
Duplicate (already processed)200 OKIdempotent success
Your server error500 Internal Server ErrorTriggers provider retry
Rate limited (you're overloaded)429 Too Many Requests + Retry-After headerTells sender to back off

Part 7 β€” Outgoing Webhook Server Checklist

When building a service that sends webhooks to subscribers:

  • Validate the subscriber URL is reachable before saving it
  • Store the subscription with: URL, events list, secret, created_at, status
  • Sign every outgoing payload with HMAC-SHA256 using a per-subscriber secret
  • Include a unique event ID in headers (X-Webhook-Id)
  • Implement retry with exponential backoff (max 5 attempts)
  • Log all delivery attempts (status code, latency, attempt number)
  • Disable subscriptions after N consecutive failures (e.g., 3 days of failures)
  • Provide a UI for subscribers to view delivery logs and manually retry
  • Implement a test delivery endpoint (POST /webhooks/test)

Agent Instructions

  1. When helping with incoming webhooks, always ask which provider (Stripe, GitHub, etc.) β€” each has a different signature scheme
  2. Warn immediately if the user parses the body before signature validation β€” this invalidates the signature check
  3. Always include idempotency handling β€” assume delivery-at-least-once
  4. For local testing, recommend ngrok for quick sessions and smee for persistent dev setups
  5. When writing webhook handlers, put the 200 response before async processing β€” it prevents retries
  6. If a user's webhooks aren't arriving, check: URL accessibility, signature mismatch, SSL certificate, firewall rules, and request timeout

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.