Real-World Webhook Integrations: Stripe, GitHub, and Slack
Integrating webhooks from third-party services is a common task for developers. Each provider has its own payload format, signature verification method, and event types. This guide covers three of the most popular webhook providers.
Stripe Webhooks
Stripe uses webhooks to notify you about events in your account - successful payments, failed charges, subscription updates, and more.
Setting Up Stripe Webhooks
- Go to Developers > Webhooks in your Stripe Dashboard
- Click Add endpoint and enter your webhook URL
- Select the events you want to receive
- Copy the signing secret (starts with
whsec_)
Verifying Stripe Signatures
Stripe signs every webhook with a timestamp and signature in the Stripe-Signature header:
$payload = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'];
$endpointSecret = getenv('STRIPE_WEBHOOK_SECRET');
try {
$event = \Stripe\Webhook::constructEvent(
$payload,
$sigHeader,
$endpointSecret
);
} catch (\UnexpectedValueException $e) {
// Invalid JSON payload
error_log('Stripe webhook error: Invalid payload');
http_response_code(400);
exit('Invalid payload');
} catch (\Stripe\Exception\SignatureVerificationException $e) {
// Invalid signature - possible attack or misconfiguration
error_log('Stripe webhook error: Invalid signature from ' . $_SERVER['REMOTE_ADDR']);
http_response_code(400);
exit('Invalid signature');
}
// Handle the event
switch ($event->type) {
case 'payment_intent.succeeded':
$paymentIntent = $event->data->object;
// Handle successful payment
break;
case 'customer.subscription.deleted':
$subscription = $event->data->object;
// Handle subscription cancellation
break;
default:
// Unexpected event type
break;
}
http_response_code(200);
Common Stripe Events
| Event | Description |
|---|---|
payment_intent.succeeded |
Payment was successful |
payment_intent.payment_failed |
Payment failed |
customer.subscription.created |
New subscription started |
customer.subscription.deleted |
Subscription was cancelled |
invoice.payment_failed |
Invoice payment failed |
GitHub Webhooks
GitHub webhooks notify you about repository events - pushes, pull requests, issues, and more.
Setting Up GitHub Webhooks
- Go to your repository Settings > Webhooks
- Click Add webhook
- Enter your Payload URL
- Set Content type to
application/json - Create a Secret for signature verification
- Choose which events to receive
Verifying GitHub Signatures
GitHub uses HMAC-SHA256 to sign payloads. The signature is in the X-Hub-Signature-256 header:
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$secret = getenv('GITHUB_WEBHOOK_SECRET');
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) {
error_log('GitHub webhook: Invalid signature from ' . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
exit('Invalid signature');
}
$event = $_SERVER['HTTP_X_GITHUB_EVENT'];
$data = json_decode($payload, true);
switch ($event) {
case 'push':
$branch = str_replace('refs/heads/', '', $data['ref']);
$commits = $data['commits'];
// Trigger deployment or CI pipeline
break;
case 'pull_request':
$action = $data['action']; // opened, closed, synchronize, reopened
$prNumber = $data['number'];
// Note: "merged" is not an action - check the merged field instead
if ($action === 'closed' && $data['pull_request']['merged'] === true) {
// PR was merged
}
break;
case 'issues':
$action = $data['action'];
$issue = $data['issue'];
// Track issue updates
break;
}
http_response_code(200);
Common GitHub Events
| Event | Description |
|---|---|
push |
Commits pushed to a branch |
pull_request |
PR opened, closed, synchronized, or reopened |
issues |
Issue created or updated |
release |
New release published |
workflow_run |
GitHub Actions workflow completed |
Slack Webhooks
Slack offers two types of webhooks: Incoming Webhooks (send messages to Slack) and Event API (receive events from Slack).
Incoming Webhooks (Sending to Slack)
To send messages to a Slack channel:
$webhookUrl = getenv('SLACK_WEBHOOK_URL');
$message = [
'text' => 'New order received!',
'blocks' => [
[
'type' => 'section',
'text' => [
'type' => 'mrkdwn',
'text' => '*Order #1234* has been placed by john@example.com'
]
]
]
];
// Using Guzzle (recommended for production)
$client = new \GuzzleHttp\Client();
$response = $client->post($webhookUrl, [
'json' => $message,
]);
// Or with Symfony HttpClient:
// $response = $httpClient->request('POST', $webhookUrl, ['json' => $message]);
Event API (Receiving from Slack)
Slack sends events to your endpoint and requires signature verification:
$payload = file_get_contents('php://input');
$timestamp = (int) $_SERVER['HTTP_X_SLACK_REQUEST_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_SLACK_SIGNATURE'];
$signingSecret = getenv('SLACK_SIGNING_SECRET');
// Prevent replay attacks - reject requests older than 5 minutes
if (abs(time() - $timestamp) > 300) {
error_log('Slack webhook: Request too old, possible replay attack');
http_response_code(400);
exit('Request too old');
}
$sigBasestring = 'v0:' . $timestamp . ':' . $payload;
$expectedSignature = 'v0=' . hash_hmac('sha256', $sigBasestring, $signingSecret);
if (!hash_equals($expectedSignature, $signature)) {
error_log('Slack webhook: Invalid signature from ' . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
exit('Invalid signature');
}
$data = json_decode($payload, true);
// Handle URL verification challenge
if ($data['type'] === 'url_verification') {
header('Content-Type: text/plain');
echo $data['challenge'];
exit;
}
// Handle events
if ($data['type'] === 'event_callback') {
$event = $data['event'];
switch ($event['type']) {
case 'message':
// New message in a channel
$text = $event['text'];
$user = $event['user'];
break;
case 'app_mention':
// Bot was mentioned
break;
case 'reaction_added':
// Emoji reaction added
break;
}
}
http_response_code(200);
Common Slack Events
| Event | Description |
|---|---|
message |
Message posted to a channel |
app_mention |
Your app was mentioned |
reaction_added |
Emoji reaction added to a message |
member_joined_channel |
User joined a channel |
file_shared |
File was shared |
Handling Idempotency
Webhooks can be delivered multiple times due to network issues or retry logic. Your handler must be idempotent - processing the same event twice should have the same result as processing it once.
// Store processed event IDs in your database or cache
function handleWebhook(array $event): void
{
$eventId = $event['id']; // Stripe, GitHub, Slack all provide unique event IDs
// Check if we've already processed this event
if ($this->eventRepository->exists($eventId)) {
// Already processed - return success without doing anything
return;
}
// Process the event
$this->processEvent($event);
// Mark as processed AFTER successful processing
$this->eventRepository->markAsProcessed($eventId);
}
Key strategies:
- Store event IDs in a database or Redis with TTL
- Check for duplicates before processing
- Use database transactions to ensure atomicity
- Consider using unique constraints on order IDs, payment IDs, etc.
Testing Your Integrations
Before connecting to production systems, use WebhookApp to:
- Generate a test URL - Get a unique endpoint instantly
- Configure the service - Point Stripe, GitHub, or Slack to your test URL
- Trigger test events - Most services have a "Send test webhook" button
- Inspect the payload - See exactly what data is being sent
- Debug signature issues - Check headers and verify your implementation
Summary
Each webhook provider has its own quirks:
| Provider | Signature Header | Algorithm | Extras |
|---|---|---|---|
| Stripe | Stripe-Signature |
HMAC-SHA256 + timestamp | Use official SDK |
| GitHub | X-Hub-Signature-256 |
HMAC-SHA256 | Event type in header |
| Slack | X-Slack-Signature |
HMAC-SHA256 + timestamp | URL verification challenge |
Key takeaways:
- Always verify signatures - Each provider uses slightly different methods
- Use official SDKs when available - They handle edge cases for you
- Test thoroughly - Use WebhookApp to inspect payloads before writing code
- Handle events idempotently - Webhooks may be delivered more than once
Happy integrating!