Back to list
git-tao

webhook-safety

by git-tao

Personal portfolio website for taotang.io - AI Systems Engineer consulting

0🍴 0📅 Jan 16, 2026

SKILL.md


name: webhook-safety description: "Knowledge for safe webhook handling. Apply when working on webhook handlers, payment processing, or subscription events."

Webhook Safety Skill

Safe webhook handling patterns for Stripe and other payment/event webhooks.

When to Apply

  • Modifying webhook handlers
  • Processing Stripe events
  • Credit allocation from payments
  • Subscription status updates
  • Any async event processing

Core Invariants

1. Idempotency (CRITICAL)

Webhooks may be delivered multiple times. Every handler MUST be idempotent.

// CORRECT: Idempotent handling
async function handleCheckoutCompleted(event) {
    // Check if already processed
    const existing = await db.billingEvents.findFirst({
        where: { stripeEventId: event.id }
    });

    if (existing) {
        return { status: 'already_processed' };
    }

    // Process and record
    await processCheckout(event.data.object);
    await db.billingEvents.create({
        data: { stripeEventId: event.id, eventType: event.type }
    });
}

// WRONG: Will double-credit on retry
async function handleCheckoutCompleted(event) {
    user.credits += 100; // DOUBLES ON RETRY!
}

2. Signature Verification

app.post('/webhooks/stripe', async (req, res) => {
    const sig = req.headers['stripe-signature'];
    let event;

    try {
        event = stripe.webhooks.constructEvent(
            req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
        );
    } catch (err) {
        return res.status(400).send('Invalid signature');
    }

    // Process event...
});

3. Fail-Fast for Critical Events

// CORRECT: Re-throw to trigger retry
async function handlePaymentSucceeded(event) {
    try {
        await allocateCredits(event);
    } catch (error) {
        console.error('Credit allocation failed:', error);
        throw error; // Re-throw for retry
    }
}

// WRONG: Silent failure = lost payment
async function handlePaymentSucceeded(event) {
    try {
        await allocateCredits(event);
    } catch {
        // Silent catch = LOST PAYMENT
    }
}

Event Types to Handle

EventAction
checkout.session.completedAllocate credits, create order
customer.subscription.createdCreate subscription record
customer.subscription.updatedUpdate subscription
customer.subscription.deletedMark inactive
invoice.paidAllocate monthly credits
invoice.payment_failedNotify user, pause service

Race Condition Prevention

// Webhooks can arrive out of order!
// invoice.paid may arrive before subscription.created

async function handleInvoicePaid(event) {
    const subscriptionId = event.data.object.subscription;

    // Wait for subscription with timeout
    let subscription;
    for (let i = 0; i < 5; i++) {
        subscription = await db.subscriptions.findFirst({
            where: { stripeSubscriptionId: subscriptionId }
        });
        if (subscription) break;
        await sleep(1000);
    }

    if (!subscription) {
        throw new Error('Subscription not yet created - retry');
    }

    await allocateCredits(subscription);
}

Billing Events Table

CREATE TABLE billing_events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    stripe_event_id VARCHAR(255) UNIQUE NOT NULL,
    event_type VARCHAR(100) NOT NULL,
    processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX ON billing_events(stripe_event_id);

Common Bugs

BugFix
Double creditsCheck billing_events before processing
Lost paymentsRe-throw errors to trigger retry
Race conditionsWait for prerequisites with timeout
Wrong customerUse customer field, not customer_email

Score

Total Score

50/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon