Webhook Integration
Receive real-time notifications when events happen in your stryhub account. Every webhook is signed with HMAC-SHA256, delivered with automatic retries, and logged for full visibility.
Overview
stryhub sends HTTP POST requests to your configured endpoint URLs whenever significant events occur — such as completed payments, subscription changes, or new customer registrations.
Unlike raw Stripe webhooks, stryhub delivers simplified, consistent payloads that are easy to parse and act on. You don't need to understand Stripe's event structure — just handle the stryhub events.
Configuration
To set up a webhook endpoint:
- Go to Webhooks in your admin dashboard
- Click New Integration
- Enter the URL where you want to receive events (must be HTTPS in production)
- Add an optional description (e.g., "Production Server" or "Staging Environment")
- Select which events to subscribe to (all are selected by default)
- Click Create Integration
After creation, you'll receive a signing secret that starts with whsec_. Store this securely — you'll need it to verify webhook signatures.
Payload Format
Every webhook delivery contains a JSON payload with this structure:
{
"id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "payment.completed",
"created_at": "2026-02-16T14:30:00.000Z",
"data": {
"customer": {
"email": "john@example.com",
"name": "John Doe",
"stripe_customer_id": "cus_abc123"
},
"amount": 49.99,
"net_amount": 48.49,
"platform_fee": 1.50,
"currency": "usd",
"subscription_id": "sub_xyz789",
"mode": "subscription"
}
}
| Field | Type | Description |
|---|---|---|
id |
string | Unique event identifier (UUID v4) |
type |
string | Event type (e.g., payment.completed) |
created_at |
string | ISO 8601 timestamp |
data |
object | Event-specific data (varies by event type) |
HTTP Headers
Each webhook request includes these headers:
| Header | Example | Description |
|---|---|---|
Content-Type |
application/json |
Always JSON |
X-Stryhub-Signature |
t=1708100000,v1=abc123... |
HMAC-SHA256 signature for verification |
X-Stryhub-Event |
payment.completed |
Event type |
X-Stryhub-Delivery |
del_uuid |
Unique delivery ID (for idempotency) |
User-Agent |
Stryhub-Webhooks/1.0 |
User agent string |
Signature Verification
Every webhook delivery is signed with your endpoint's unique signing secret using HMAC-SHA256. You should always verify the signature before processing the event.
How it works
- Extract the timestamp (
t) and signature (v1) from theX-Stryhub-Signatureheader - Construct the signed content:
{timestamp}.{raw_body} - Compute HMAC-SHA256 using your signing secret
- Compare the computed signature with the
v1value - Optionally check that the timestamp is recent (within 5 minutes) to prevent replay attacks
Code Examples
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
// Parse the signature header
const parts = {};
signatureHeader.split(',').forEach(part => {
const [key, value] = part.split('=');
parts[key] = value;
});
const timestamp = parts['t'];
const signature = parts['v1'];
// Construct signed content
const signedContent = `${timestamp}.${rawBody}`;
// Compute expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
// Compare signatures (timing-safe)
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
// Check timestamp freshness (5 min tolerance)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
throw new Error('Webhook timestamp too old');
}
return isValid;
}
// Express.js example
app.post('/webhooks/stryhub', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-stryhub-signature'];
const secret = process.env.STRYHUB_WEBHOOK_SECRET;
try {
const isValid = verifyWebhook(req.body.toString(), signature, secret);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body);
console.log('Received event:', event.type, event.id);
// Handle the event
switch (event.type) {
case 'payment.completed':
// Activate subscription, send welcome email, etc.
break;
case 'subscription.canceled':
// Revoke access, send retention email, etc.
break;
// ... handle other events
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).json({ error: err.message });
}
});
import hmac
import hashlib
import time
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
"""Verify stryhub webhook signature."""
# Parse the signature header
parts = {}
for part in signature_header.split(','):
key, value = part.split('=', 1)
parts[key] = value
timestamp = parts.get('t', '')
signature = parts.get('v1', '')
# Construct signed content
signed_content = f"{timestamp}.{raw_body.decode('utf-8')}"
# Compute expected signature
expected = hmac.new(
secret.encode('utf-8'),
signed_content.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Compare signatures (timing-safe)
is_valid = hmac.compare_digest(signature, expected)
# Check timestamp freshness (5 min tolerance)
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError('Webhook timestamp too old')
return is_valid
@app.route('/webhooks/stryhub', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Stryhub-Signature', '')
secret = os.environ['STRYHUB_WEBHOOK_SECRET']
try:
is_valid = verify_webhook(request.data, signature, secret)
if not is_valid:
return jsonify({'error': 'Invalid signature'}), 401
event = json.loads(request.data)
print(f"Received event: {event['type']} {event['id']}")
# Handle the event
if event['type'] == 'payment.completed':
# Activate subscription, send welcome email, etc.
pass
elif event['type'] == 'subscription.canceled':
# Revoke access, send retention email, etc.
pass
return jsonify({'received': True}), 200
except Exception as e:
print(f"Webhook error: {e}")
return jsonify({'error': str(e)}), 400
<?php
// PHP webhook handler
function verifyWebhook(string $rawBody, string $signatureHeader, string $secret): bool
{
// Parse the signature header
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? '';
$signature = $parts['v1'] ?? '';
// Construct signed content
$signedContent = "{$timestamp}.{$rawBody}";
// Compute expected signature
$expected = hash_hmac('sha256', $signedContent, $secret);
// Compare signatures (timing-safe)
$isValid = hash_equals($signature, $expected);
// Check timestamp freshness (5 min tolerance)
$age = time() - intval($timestamp);
if ($age > 300) {
throw new Exception('Webhook timestamp too old');
}
return $isValid;
}
// Handle the webhook
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_STRYHUB_SIGNATURE'] ?? '';
$secret = getenv('STRYHUB_WEBHOOK_SECRET');
try {
$isValid = verifyWebhook($rawBody, $signature, $secret);
if (!$isValid) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$event = json_decode($rawBody, true);
error_log("Received event: {$event['type']} {$event['id']}");
// Handle the event
switch ($event['type']) {
case 'payment.completed':
// Activate subscription, send welcome email, etc.
break;
case 'subscription.canceled':
// Revoke access, send retention email, etc.
break;
}
http_response_code(200);
echo json_encode(['received' => true]);
} catch (Exception $e) {
error_log("Webhook error: " . $e->getMessage());
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}
?>
Event Types
stryhub translates complex Stripe events into simple, actionable notifications:
| Event | Description | When it fires |
|---|---|---|
checkout.completed |
Checkout session completed | Customer finishes a checkout (one-time or subscription) |
payment.completed |
Payment succeeded | Successful charge (initial or recurring) |
payment.failed |
Payment failed | Card declined or payment error |
subscription.created |
New subscription | Customer subscribes to a recurring product |
subscription.renewed |
Subscription renewed | Recurring payment succeeds |
subscription.canceled |
Subscription canceled | Subscription is fully canceled |
subscription.paused |
Subscription paused | Subscription billing is paused |
subscription.resumed |
Subscription resumed | Paused subscription is reactivated |
subscription.past_due |
Payment past due | Recurring payment failed; subscription at risk |
customer.created |
New customer | First-time customer registered |
Retry Policy
If your endpoint returns a non-2xx status code (or times out after 15 seconds), stryhub automatically retries the delivery:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1st | Immediate | 0 |
| 2nd | 1 minute | 1 minute |
| 3rd | 5 minutes | 6 minutes |
| 4th | 30 minutes | 36 minutes |
| 5th | 2 hours | ~2.5 hours |
| 6th (final) | 24 hours | ~26.5 hours |
After 6 failed attempts, the delivery is marked as failed. You can manually retry failed deliveries from the dashboard at any time.
Best Practices
1. Respond quickly
Return a 2xx response as fast as possible (within 15 seconds). If you need to do heavy processing, acknowledge the webhook immediately and process it asynchronously (e.g., using a job queue).
2. Verify signatures
Always verify the X-Stryhub-Signature header before trusting the payload. This prevents attackers from sending fake events to your endpoint.
3. Handle duplicates (idempotency)
Due to retries, your endpoint may receive the same event more than once. Use the id field in the payload to detect and skip duplicate events.
4. Use the delivery ID
The X-Stryhub-Delivery header contains a unique delivery ID. Log this for debugging and use it to track individual delivery attempts.
5. Monitor your logs
Regularly check the webhook delivery logs in your dashboard. Failed deliveries might indicate issues with your endpoint, network, or application logic.
6. Use HTTPS
Always use HTTPS for your endpoint URL in production. This ensures the payload and signature are encrypted in transit.