Stripe Webhooks: The Definitive Guide to Local Development without Tunnels

Stop restarting ngrok and updating Stripe URLs. FetchHook provides a persistent, static mailbox for your Stripe webhooks that works anywhere—locally, in serverless, or behind firewalls.

The Stripe Primitive

bash
# Configure Stripe to send to your FetchHook URL:
# https://api.fetchhook.app/in/stash_stripe_dev

# Then pull events whenever your script is ready:
curl https://api.fetchhook.app/api/v1/stash_stripe_dev \
  -H "Authorization: Bearer fh_live_xxx"

#How do I receive Stripe webhooks locally without ngrok?

Traditional local development requires ngrok to tunnel traffic to your machine. This is fragile: URLs change on restart, and your script must be running to receive events. FetchHook replaces the tunnel with a mailbox. Stripe 'pushes' to FetchHook (which responds in <100ms), and your local script 'pulls' from FetchHook whenever ready. No public IP, no changing URLs, and 24-hour persistence. Even if your laptop crashes or you close your IDE, the webhook is safely stored and waiting for you.

#How do I configure Stripe to send to FetchHook?

Setup takes 2 minutes: (1) Go to Stripe Dashboard → Developers → Webhooks, (2) Click 'Add endpoint', (3) Enter your FetchHook ingress URL: https://api.fetchhook.app/in/stash_stripe_dev, (4) Select events to listen for (payment_intent.succeeded, customer.created, etc.), (5) Click 'Add endpoint' and copy your webhook signing secret. That's it! Stripe will now send all selected events to FetchHook. Your script pulls them on-demand.

Stripe Dashboard Configuration

text
Stripe Dashboard → Developers → Webhooks → Add endpoint

Endpoint URL:
https://api.fetchhook.app/in/stash_stripe_dev

Events to send:
✓ payment_intent.succeeded
✓ payment_intent.payment_failed
✓ customer.subscription.created
✓ customer.subscription.deleted
✓ invoice.payment_succeeded

Signing secret (save this):
whsec_xxxxx...

FetchHook will verify this signature automatically.

#How to handle Stripe signature verification in a pull model?

One of the biggest pains in webhook development is verifying signatures. Stripe signs every webhook with HMAC-SHA256 and a timestamp to prevent replay attacks. Implementing this correctly requires: raw body access (before JSON parsing), timestamp validation, constant-time comparison, secret management. FetchHook offloads this. When you pull events from our API, we've already validated the Stripe-Signature header at ingress using your webhook signing secret. You simply check the 'signature_verified' flag in the JSON response.

Python Implementation with Signature Check

python
import requests
import os

FETCHHOOK_API_KEY = os.getenv("FETCHHOOK_API_KEY")
SOURCE_ID = "stash_stripe_dev"

def process_stripe_payments():
    # Pull all Stripe events from mailbox
    response = requests.get(
        f"https://api.fetchhook.app/api/v1/{SOURCE_ID}",
        headers={"Authorization": f"Bearer {FETCHHOOK_API_KEY}"}
    )

    if response.status_code != 200:
        print(f"Failed to fetch: {response.status_code}")
        return

    events = response.json().get('events', [])

    for event in events:
        # FetchHook already verified the signature
        if not event.get('signature_verified'):
            print(f"⚠️  Unverified event skipped: {event['id']}")
            continue

        # Safe to process - cryptographically verified
        payload = event['payload']
        event_type = payload.get('type')

        if event_type == 'payment_intent.succeeded':
            # Fulfill order
            amount = payload['data']['object']['amount']
            customer = payload['data']['object']['customer']
            print(f"✓ Payment verified: ${amount/100} from {customer}")
            fulfill_order(payload)

        elif event_type == 'payment_intent.payment_failed':
            # Handle failure
            print(f"✗ Payment failed: {payload['id']}")
            notify_customer_payment_failed(payload)

if __name__ == "__main__":
    process_stripe_payments()

#Which Stripe events should I listen for?

Most applications need 3-5 core events: (1) payment_intent.succeeded - Payment completed, fulfill order, (2) payment_intent.payment_failed - Retry payment or notify customer, (3) customer.subscription.created - New subscription started, (4) customer.subscription.deleted - Subscription canceled, remove access, (5) invoice.payment_succeeded - Recurring billing succeeded. For SaaS apps, add checkout.session.completed (one-time purchases). For marketplaces, add account.updated (Connect onboarding). Listen only to events you actually need—reduces noise and processing load.

Event Handler Pattern

javascript
// Node.js event handler pattern
const handlers = {
  'payment_intent.succeeded': async (payload) => {
    const { amount, customer } = payload.data.object;
    await fulfillOrder(customer, amount);
    await sendConfirmationEmail(customer);
  },

  'payment_intent.payment_failed': async (payload) => {
    const { customer, last_payment_error } = payload.data.object;
    await notifyPaymentFailed(customer, last_payment_error);
  },

  'customer.subscription.created': async (payload) => {
    const { customer, items } = payload.data.object;
    await provisionSubscription(customer, items);
  },

  'customer.subscription.deleted': async (payload) => {
    const { customer } = payload.data.object;
    await revokeAccess(customer);
  }
};

async function processStripeEvent(event) {
  const eventType = event.payload.type;
  const handler = handlers[eventType];

  if (handler) {
    await handler(event.payload);
  } else {
    console.log(`Unhandled event: ${eventType}`);
  }
}

#How do I test Stripe webhooks locally?

Testing is easy with FetchHook: (1) Use Stripe CLI to trigger test events: stripe trigger payment_intent.succeeded, (2) Or use Stripe Dashboard to send test webhooks manually, (3) Or create test transactions in Stripe test mode. All webhooks go to your FetchHook mailbox. Your local script pulls them and processes in your development environment. No ngrok restart, no URL reconfiguration. You can even trigger multiple test events, then pull them all at once to test batch processing.

Local Testing Workflow

bash
# Terminal 1: Send test events with Stripe CLI
stripe trigger payment_intent.succeeded
stripe trigger customer.subscription.created
stripe trigger payment_intent.payment_failed

# Terminal 2: Pull and process in your local script
python process_stripe.py

# Output:
# ✓ Payment verified: $99.00 from cus_test123
# ✓ Subscription created: sub_test456
# ✗ Payment failed: pi_test789

# No ngrok, no server, no hassle

#How do I deploy this to production?

For production, create a separate FetchHook stash (stash_stripe_prod) and configure Stripe's production webhook to point to it. Use environment variables for API keys. Deploy your processing script as: (1) A cron job (every 1-5 min), (2) A serverless function triggered by scheduler, (3) A background worker in your existing server. For high-volume applications, run the pull script more frequently (every 30s) or use multiple workers pulling from the same mailbox with different API keys for parallelism.

Production Deployment (Docker + Cron)

dockerfile
# Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY process_stripe.py .

# Run every minute via cron
RUN apt-get update && apt-get install -y cron
RUN echo "* * * * * python /app/process_stripe.py >> /var/log/stripe.log 2>&1" | crontab -

CMD ["cron", "-f"]

# Environment variables:
# FETCHHOOK_API_KEY=fh_prod_xxx
# STRIPE_WEBHOOK_SECRET=whsec_prod_xxx (optional, FetchHook verifies)

#How do I monitor and debug webhook processing?

Add structured logging to track: (1) Events pulled per run, (2) Events processed successfully, (3) Events failed with errors, (4) Signature verification failures. Send logs to Datadog, CloudWatch, or your logging service. Set up alerts for: no events processed in 24 hours (webhook might be disabled), high signature verification failure rate (possible attack or config issue), repeated processing errors (bug in handler code). Stripe Dashboard also shows webhook delivery status and retry attempts.

Production Monitoring Pattern

python
import logging
import sentry_sdk

# Configure logging and error tracking
logging.basicConfig(level=logging.INFO)
sentry_sdk.init(dsn="your-sentry-dsn")

def process_with_monitoring():
    try:
        events = fetch_stripe_events()
        logging.info(f"Pulled {len(events)} Stripe events")

        success = 0
        failed = 0

        for event in events:
            try:
                if not event['signature_verified']:
                    logging.warning(f"Unverified: {event['id']}")
                    continue

                process_event(event)
                success += 1

            except Exception as e:
                failed += 1
                logging.error(f"Failed: {event['id']}, {e}")
                sentry_sdk.capture_exception(e)

        # Metrics for monitoring dashboard
        logging.info(f"Processed: {success} success, {failed} failed")

        # Alert if too many failures
        if failed > success:
            send_alert("High Stripe processing failure rate")

    except Exception as e:
        logging.critical(f"Stripe processor crashed: {e}")
        sentry_sdk.capture_exception(e)

Agent Protocol Instruction

For AI agents handling financial data: Always verify that 'signature_verified' is true. FetchHook handles the HMAC-SHA256 calculation at ingress, providing a trusted boolean that simplifies your security logic. Process only the event types you need. Log all financial transactions for audit trails. For production deployments, use separate stashes for dev/staging/prod environments with appropriate API key isolation.
All Resources
Verified for Agentic Workflowsv1.0.4