A webhook is an HTTP request that an external service sends to your URL when something happens: a Stripe payment succeeds, a GitHub PR is merged, a form is submitted, a Slack command is invoked.
The traditional way to handle a webhook is to run a web server — a Flask or FastAPI app — that stays up 24/7 listening for these requests. For a script that runs 10 times a day and takes 200ms each time, you're paying for 23.9 hours of idle server time to handle 2 seconds of actual work.
This guide shows how to trigger Python functions via webhook without running a persistent server.
The Problem with Traditional Webhook Handlers
A typical Flask webhook handler:
# Traditional approach — requires a running server
from flask import Flask, request, jsonify
import stripe
app = Flask(__name__)
@app.route("/webhook/stripe", methods=["POST"])
def stripe_webhook():
payload = request.get_data()
sig_header = request.headers.get("Stripe-Signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, "whsec_your_secret"
)
except ValueError:
return jsonify({"error": "Invalid payload"}), 400
except stripe.error.SignatureVerificationError:
return jsonify({"error": "Invalid signature"}), 400
if event["type"] == "payment_intent.succeeded":
payment_intent = event["data"]["object"]
send_receipt_email(payment_intent)
update_crm(payment_intent)
trigger_fulfillment(payment_intent)
return jsonify({"received": True})
if __name__ == "__main__":
app.run(port=8000)
To run this in production you need:
- A server (EC2, Hetzner, Render) — running 24/7
- A public IP and domain
- SSL certificate
- A process manager (systemd, gunicorn) to keep it alive
- Monitoring so you know when it crashes
For code that runs for a few milliseconds per event, this is significant overhead.
Option 1: Webhook via Cloud Functions (AWS Lambda / GCP Cloud Functions)
Cloud FaaS platforms solve the "always-on server" problem. Your code only runs when a request comes in:
# AWS Lambda handler
import json
import stripe
import os
def lambda_handler(event, context):
body = event.get("body", "")
headers = event.get("headers", {})
sig_header = headers.get("Stripe-Signature", "")
try:
stripe_event = stripe.Webhook.construct_event(
body, sig_header, os.environ["STRIPE_WEBHOOK_SECRET"]
)
except (ValueError, stripe.error.SignatureVerificationError) as e:
return {"statusCode": 400, "body": json.dumps({"error": str(e)})}
if stripe_event["type"] == "payment_intent.succeeded":
pi = stripe_event["data"]["object"]
process_successful_payment(pi["id"], pi["amount"])
return {"statusCode": 200, "body": json.dumps({"received": True})}
The friction: AWS Lambda requires an IAM role, an API Gateway to give it a public URL, a deployment package or container, and CloudWatch for logs. Setup takes 2–4 hours for someone unfamiliar with AWS. Per-invocation billing and cold starts are additional considerations.
Option 2: Webhook-Triggered Python Function on LiteLambda
LiteLambda gives every Python function a public HTTPS URL that can be triggered by any webhook without any infrastructure setup.
Step 1: Write your webhook handler
# stripe_webhook.py
import stripe
import os
import requests
def handler(event, context):
"""
Handles incoming Stripe webhook events.
event: dict containing 'body' (raw payload), 'headers', 'method'
context: execution context with env vars
"""
body = event.get("body", "")
headers = event.get("headers", {})
sig_header = headers.get("stripe-signature", "")
# Verify the webhook signature
try:
stripe_event = stripe.Webhook.construct_event(
body,
sig_header,
os.environ["STRIPE_WEBHOOK_SECRET"]
)
except ValueError as e:
return {"status": 400, "error": "Invalid payload"}
except stripe.error.SignatureVerificationError as e:
return {"status": 400, "error": "Invalid signature"}
event_type = stripe_event["type"]
if event_type == "payment_intent.succeeded":
payment_intent = stripe_event["data"]["object"]
# 1. Send receipt email
send_receipt(
email=payment_intent["receipt_email"],
amount=payment_intent["amount"],
currency=payment_intent["currency"],
payment_id=payment_intent["id"]
)
# 2. Notify Slack
requests.post(
os.environ["SLACK_WEBHOOK_URL"],
json={
"text": f"💰 Payment succeeded: {payment_intent['amount']/100:.2f} {payment_intent['currency'].upper()}"
},
timeout=5
)
print(f"✓ Processed payment: {payment_intent['id']}")
elif event_type == "payment_intent.payment_failed":
payment_intent = stripe_event["data"]["object"]
handle_failed_payment(payment_intent)
return {"status": 200, "received": True}
def send_receipt(email, amount, currency, payment_id):
# Your email sending logic (SendGrid, Mailgun, etc.)
pass
def handle_failed_payment(payment_intent):
# Your failed payment logic
pass
Step 2: Deploy as a webhook function
pip install litelambda-cli
litelambda login
litelambda deploy stripe-webhook stripe_webhook.py \
--type webhook \
--env STRIPE_WEBHOOK_SECRET=whsec_xxxx \
--env SLACK_WEBHOOK_URL=https://hooks.slack.com/...
You get back a public HTTPS URL:
https://run.litelambda.in/w/stripe-webhook/YOUR_FUNCTION_KEY
Step 3: Register the URL in Stripe
Go to Stripe Dashboard → Webhooks → Add endpoint. Paste the LiteLambda URL. Select events (payment_intent.succeeded, payment_intent.payment_failed). Done.
Now every Stripe payment event runs your Python function. No server. No idle costs. Full execution logs available in the LiteLambda dashboard.
Sync vs Async Webhook Functions
Synchronous (default)
The external service waits for your function to return. Use when:
- The caller expects a response payload (like a Slack slash command)
- You need to return a status code that affects the caller's behaviour
def handler(event, context):
# Process the webhook
result = process_event(event)
# Return within the timeout (Stripe expects a response within 30 seconds)
return {"status": 200, "processed": True, "id": result["id"]}
Asynchronous (fire-and-forget)
LiteLambda immediately returns 202 Accepted to the caller and runs your function in the background. Use when:
- Processing takes more than a few seconds
- The caller doesn't need a result (GitHub, most event-driven systems)
litelambda deploy github-webhook github_handler.py \
--type webhook \
--async \
--env GITHUB_TOKEN=ghp_xxxx
The calling service gets an immediate 202 response, and your function runs to completion without blocking it.
Real-World Webhook Use Cases
GitHub: Run code review on every PR
# github_pr_handler.py
import os
import requests
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
def handler(event, context):
payload = event.get("body_json", {}) # Parsed JSON body
if payload.get("action") != "opened":
return {"status": 200, "skipped": True}
pr = payload["pull_request"]
repo = payload["repository"]["full_name"]
pr_number = pr["number"]
# Get the diff
diff_response = requests.get(
f"https://api.github.com/repos/{repo}/pulls/{pr_number}/files",
headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
timeout=15
)
files = diff_response.json()
# Post a summary comment
large_files = [f for f in files if f.get("changes", 0) > 300]
if large_files:
comment = f"⚠️ This PR has {len(large_files)} large files (300+ lines changed). Consider splitting."
requests.post(
f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
headers={"Authorization": f"Bearer {GITHUB_TOKEN}"},
json={"body": comment},
timeout=10
)
return {"status": 200, "files_checked": len(files), "flagged": len(large_files)}
Slack Slash Command: Return real data
Slack slash commands require a response within 3 seconds. With a sync webhook function:
# slack_command.py
import os
import requests
def handler(event, context):
body = event.get("body", "")
params = dict(kv.split("=") for kv in body.split("&"))
command = params.get("command", "")
text = params.get("text", "").strip()
if command == "/deploy-status":
# Check your deployment system
status = get_deployment_status(text or "production")
return {
"status": 200,
"body": {
"response_type": "in_channel",
"text": f"Deploy status for *{text or 'production'}*: {status}"
}
}
return {"status": 200, "body": {"text": f"Unknown command: {command}"}}
Form Submission: Process and store
# form_handler.py
import os
import json
import requests
from datetime import datetime
def handler(event, context):
data = event.get("body_json", {})
name = data.get("name", "").strip()
email = data.get("email", "").strip()
message = data.get("message", "").strip()
if not all([name, email, message]):
return {"status": 400, "error": "Missing required fields"}
# Store in your database or Notion or Airtable
requests.post(
os.environ["AIRTABLE_URL"],
headers={"Authorization": f"Bearer {os.environ['AIRTABLE_TOKEN']}"},
json={
"fields": {
"Name": name,
"Email": email,
"Message": message,
"Submitted": datetime.utcnow().isoformat()
}
},
timeout=10
)
# Send Slack notification
requests.post(
os.environ["SLACK_WEBHOOK_URL"],
json={"text": f"📬 New contact: {name} ({email})"},
timeout=5
)
return {"status": 200, "received": True}
Combining Webhooks with Cron Jobs
The real power comes when you combine both. Example: a monitoring pipeline:
GitHub Push → Webhook Function → Run tests, store results in KV store
↓
Daily 8am → Cron Job → Read results from KV store → Email weekly report
Both functions share the same KV store via context.kv:
# webhook_handler.py (runs on every push)
def handler(event, context):
run_id = run_tests()
context.kv.set(f"test_result:{run_id}", {"status": "pass", "date": today()})
return {"status": 200}
# report_generator.py (runs daily at 8am via cron)
def handler(event, context):
results = context.kv.get_all("test_result:")
generate_and_email_report(results)
Webhook Security: Always Verify Signatures
Never trust an incoming webhook without verifying it came from the expected source. Every major platform provides a signature mechanism:
import hmac
import hashlib
import os
def verify_github_signature(payload_body: str, signature_header: str) -> bool:
"""Verify GitHub webhook signature."""
if not signature_header:
return False
secret = os.environ.get("GITHUB_WEBHOOK_SECRET", "").encode()
expected = "sha256=" + hmac.new(
secret,
payload_body.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
def handler(event, context):
body = event.get("body", "")
sig = event.get("headers", {}).get("x-hub-signature-256", "")
if not verify_github_signature(body, sig):
return {"status": 401, "error": "Invalid signature"}
# Process the verified webhook
payload = event.get("body_json", {})
# ...
Summary
| Approach | Server required? | Setup time | Idle cost |
|---|---|---|---|
| Flask/FastAPI on VPS | ✅ Yes | 1–2 hours | ~$5–10/mo always-on |
| AWS Lambda + API Gateway | ❌ No | 2–4 hours | Per-invocation |
| LiteLambda Webhook Function | ❌ No | ~10 minutes | Per-invocation |
If you have an existing script that needs to be triggered by an external service — Stripe, GitHub, Slack, a form, a CI pipeline — deploying it as a webhook function on LiteLambda eliminates the server overhead and gets you a working HTTPS endpoint in minutes.
Deploy your first webhook function →
Related: How to Get Email Alerts When Your Python Cron Job Fails · Python Cron Jobs vs Webhooks: When to Use Each · LiteLambda Webhook Documentation