Memorial Day Sale: 25% OFF! View Plans
Tutorial

Webhook Integration Guide: GitHub, Stripe, and Real-Time Events

May 26, 2026

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

What Are Webhooks and Why Do They Matter?

Traditional API integrations work on a pull model: your application repeatedly asks another service "has anything changed?" This polling approach wastes bandwidth, introduces latency, and puts unnecessary load on both systems. If you poll every 60 seconds, events can be delayed by up to a minute. If you poll every second, you are burning API rate limits on empty responses 99% of the time.

Webhooks flip the model. Instead of asking, you listen. You give a service a URL, and when something happens, that service sends an HTTP request to your URL with the event data. The event arrives in real time — typically within seconds — with zero wasted requests.

Polling
"Any new events?"
x 1000/hour
Empty Response
999 times

vs.

Event Occurs
Service Sends
POST to your URL
Your App
Processes instantly

This guide walks through building webhook integrations with GitHub and Stripe, the two most common webhook providers. You will learn how to set up endpoints, verify signatures, handle retries, and process events reliably in production.

Webhook Architecture

A webhook integration has four components: the provider (GitHub, Stripe), the transport (HTTPS), your endpoint (a URL that receives POST requests), and your processing logic (what to do with the event).

Provider Responsibilities

  • Detect events (push, payment, etc.)
  • Serialize event data as JSON
  • Sign the payload with a secret
  • Send HTTP POST to your URL
  • Retry on failure (usually 3-5 times)

Your Responsibilities

  • Expose a public HTTPS endpoint
  • Verify the signature on every request
  • Respond quickly (under 5-10 seconds)
  • Handle duplicate events (idempotency)
  • Process events asynchronously if needed
Critical security rule: Never trust a webhook request without verifying its signature. Anyone who discovers your webhook URL can send fake events. Signature verification proves the request genuinely came from the provider.

Setting Up a Webhook Endpoint

Basic PHP Endpoint

// webhook.php — Basic webhook receiver
<?php
// Only accept POST requests
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method Not Allowed');
}

// Read the raw request body
$payload = file_get_contents('php://input');
if (empty($payload)) {
http_response_code(400);
exit('Empty payload');
}

// Parse JSON
$event = json_decode($payload, true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
exit('Invalid JSON');
}

// Log the event (for debugging)
file_put_contents(
'/var/log/webhooks/events.log',
date('c') . ' ' . json_encode($event) . "\n",
FILE_APPEND
);

// Respond immediately with 200
http_response_code(200);
echo 'OK';

Basic Node.js Endpoint

// webhook-server.js — Express webhook receiver
const express = require('express');
const app = express();

// IMPORTANT: Use raw body for signature verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const payload = JSON.parse(req.body);

console.log('Received webhook:', payload.action || payload.type);

// Respond immediately — process async if needed
res.status(200).send('OK');

// Process event asynchronously
processEvent(payload).catch(err => {
console.error('Event processing failed:', err);
});
});

app.listen(3000, () => console.log('Webhook server on port 3000'));
Key principle: Respond to webhooks quickly. Most providers expect a response within 5-30 seconds. If your processing takes longer, respond with 200 immediately and process the event asynchronously using a job queue (BullMQ, Sidekiq, Laravel Queues, etc.).

Signature Verification: HMAC-SHA256

Most webhook providers use HMAC-SHA256 to sign payloads. When you register a webhook, you provide (or receive) a shared secret. The provider computes HMAC-SHA256(secret, body) and includes it in a header. Your endpoint must compute the same HMAC and compare.

How HMAC Verification Works

Provider
HMAC(secret, body)
HTTP Header
X-Hub-Signature-256
Your Server
HMAC(secret, body)
Compare
Match = Authentic

PHP HMAC Verification

function verifyWebhookSignature($payload, $signatureHeader, $secret) {
$computed = 'sha256=' . hash_hmac('sha256', $payload, $secret);

// Use timing-safe comparison to prevent timing attacks
return hash_equals($computed, $signatureHeader);
}

// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$secret = getenv('WEBHOOK_SECRET');

if (!verifyWebhookSignature($payload, $signature, $secret)) {
http_response_code(401);
exit('Invalid signature');
}

Node.js HMAC Verification

const crypto = require('crypto');

function verifySignature(payload, signatureHeader, secret) {
const computed = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signatureHeader)
);
}
Always use timing-safe comparison: Regular string comparison (===) is vulnerable to timing attacks. An attacker can determine the correct signature character by character by measuring response times. Use hash_equals() in PHP or crypto.timingSafeEqual() in Node.js.

GitHub Webhook Integration

GitHub webhooks notify your server when events occur in your repositories: pushes, pull requests, issues, releases, and dozens more.

Setting Up GitHub Webhooks

1
Go to your repository Settings > Webhooks > Add webhook
2
Configure:
  • Payload URL: https://yourdomain.com/webhook/github
  • Content type: application/json
  • Secret: A strong random string (store in env variable)
  • Events: Select specific events you need

Common GitHub Webhook Events

EventTriggerUse Case
pushCode pushed to branchAuto-deploy
pull_requestPR opened/merged/closedCI/CD trigger
issuesIssue created/updatedNotification
releaseRelease publishedDeploy new version
createBranch/tag createdBuild trigger
deleteBranch/tag deletedCleanup trigger
starRepository starredAnalytics

Handling a GitHub Push Event

// Handle GitHub push webhook
app.post('/webhook/github', express.raw({ type: 'application/json' }), (req, res) => {
// Verify signature
const signature = req.headers['x-hub-signature-256'];
if (!verifySignature(req.body, signature, process.env.GITHUB_SECRET)) {
return res.status(401).send('Invalid signature');
}

const event = req.headers['x-github-event'];
const payload = JSON.parse(req.body);

// Respond immediately
res.status(200).send('OK');

// Route by event type
switch (event) {
case 'push':
const branch = payload.ref.replace('refs/heads/', '');
console.log(`Push to ${branch} by ${payload.pusher.name}`);
console.log(`Commits: ${payload.commits.length}`);
if (branch === 'main') {
triggerDeployment(payload);
}
break;
case 'pull_request':
console.log(`PR #${payload.number}: ${payload.action}`);
break;
case 'issues':
console.log(`Issue #${payload.issue.number}: ${payload.action}`);
break;
}
});

Stripe Webhook Integration

Stripe uses webhooks for payment events: successful charges, failed payments, subscription changes, refunds, and disputes. Stripe webhook handling requires extra care because financial data is involved.

Key Stripe Events

EventWhen It FiresRequired Action
checkout.session.completedCustomer completes checkoutFulfill order
payment_intent.succeededPayment capturedConfirm payment
payment_intent.payment_failedPayment declinedNotify customer
customer.subscription.createdNew subscriptionGrant access
customer.subscription.deletedSubscription canceledRevoke access
invoice.payment_failedRecurring payment failedRetry or notify
charge.dispute.createdChargeback initiatedRespond to dispute

Stripe Webhook Handler (Node.js)

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

app.post('/webhook/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;

try {
// Stripe SDK handles signature verification
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook signature failed:', err.message);
return res.status(400).send('Invalid signature');
}

// Handle specific events
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
await fulfillOrder(session);
break;
case 'customer.subscription.deleted':
const subscription = event.data.object;
await revokeAccess(subscription.customer);
break;
case 'invoice.payment_failed':
const invoice = event.data.object;
await notifyPaymentFailed(invoice);
break;
}

res.status(200).json({ received: true });
}
);

Idempotency and Duplicate Handling

Webhook providers retry failed deliveries, which means your endpoint may receive the same event multiple times. Your processing logic must be idempotent — processing an event twice should produce the same result as processing it once.

// Idempotency with event ID tracking
async function processEvent(event) {
// Check if we have already processed this event
const existing = await db.query(
'SELECT id FROM processed_events WHERE event_id = ?',
[event.id]
);

if (existing.length > 0) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}

// Process the event
await handleEvent(event);

// Record that we processed it
await db.query(
'INSERT INTO processed_events (event_id, processed_at) VALUES (?, NOW())',
[event.id]
);
}
Tip: Periodically clean up your processed_events table. Events older than 30 days are unlikely to be retried, so you can safely remove them to keep the table manageable.

Retry Policies

Different providers have different retry behaviors. Understanding these helps you design your error handling:

ProviderRetry CountRetry ScheduleTimeout
GitHubUp to 3Immediate, then increasing delays10 seconds
StripeUp to 3 (or more)Exponential backoff over 72 hours20 seconds
SlackUp to 3Immediately, 1 min, 5 min3 seconds
GitLab0 (no retries)N/A10 seconds
TwilioUp to 2At request intervals15 seconds

Webhook Security Best Practices

Authentication

  • Always verify HMAC signatures
  • Use timing-safe string comparison
  • Store secrets in environment variables
  • Rotate secrets periodically

Transport Security

  • Always use HTTPS endpoints
  • Use valid SSL certificates
  • Consider IP whitelisting for critical webhooks
  • Reject non-POST requests

Processing Safety

  • Validate event data before processing
  • Handle duplicates (idempotency)
  • Set processing timeouts
  • Log all events for debugging

Error Handling

  • Return 200 even if async processing fails
  • Implement dead letter queues for failures
  • Alert on sustained delivery failures
  • Monitor webhook processing latency

Testing Webhooks During Development

Testing webhooks locally is challenging because your development machine is not publicly accessible. These tools solve that problem:

ngrok for Local Testing

# Expose local port 3000 to the internet
ngrok http 3000

Forwarding: https://abc123.ngrok.io -> http://localhost:3000

# Use the ngrok URL as your webhook URL in GitHub/Stripe
# https://abc123.ngrok.io/webhook/github

Stripe CLI for Local Testing

# Forward Stripe events to your local server
stripe listen --forward-to localhost:3000/webhook/stripe

# Trigger a test event
stripe trigger checkout.session.completed

# Trigger with specific data
stripe trigger payment_intent.succeeded

Manual Testing with curl

# Send a test webhook payload
PAYLOAD='{"action":"push","ref":"refs/heads/main"}'
SECRET="your-webhook-secret"
SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -X POST http://localhost:3000/webhook/github \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: $SIGNATURE" \
-H "X-GitHub-Event: push" \
-d "$PAYLOAD"

Scaling Webhook Processing

As your webhook volume grows, you need to decouple receiving from processing. The pattern is: receive the webhook, validate the signature, store the event in a queue, and respond with 200. A separate worker process then reads from the queue and handles each event at its own pace.

Webhook
Receive + Verify
Queue
Store event
Worker
Process async
Dead Letter
Failed events

A dead letter queue captures events that fail processing after multiple retries. This ensures no event is lost permanently — you can inspect failed events, fix bugs, and reprocess them.

Webhooks with Panelica

Panelica includes built-in webhook support — send notifications to HTTP endpoints, Telegram, Slack, Discord, and email when events occur on your server. Domain created, backup completed, SSL renewed, security alert triggered — all of these events can fire webhooks to your external services. Configure webhook URLs through the panel settings, and Panelica handles signature generation, retry logic, and delivery tracking automatically.

Summary

Webhooks are the backbone of modern service integrations. They replace wasteful polling with real-time push notifications, and when implemented correctly, they are reliable, secure, and efficient. The critical requirements are simple: always verify signatures, always respond quickly, always handle duplicates, and always log everything.

Start with signature verification — it is non-negotiable. Then add idempotency to handle retries safely. Finally, as your volume grows, move processing to a background queue so your webhook endpoint stays fast and responsive. With these three principles in place, you can integrate with any webhook provider confidently.

Security-first hosting panel

Stop bolting tools onto a legacy panel.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Looking for a Plesk alternative?