Docs Webhook Integration

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.

Multiple endpoints: You can configure as many webhook endpoints as you need. Each endpoint can subscribe to different event types, making it easy to route events to different services.

Configuration

To set up a webhook endpoint:

  1. Go to Webhooks in your admin dashboard
  2. Click New Integration
  3. Enter the URL where you want to receive events (must be HTTPS in production)
  4. Add an optional description (e.g., "Production Server" or "Staging Environment")
  5. Select which events to subscribe to (all are selected by default)
  6. 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:

payload.json
{
  "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

  1. Extract the timestamp (t) and signature (v1) from the X-Stryhub-Signature header
  2. Construct the signed content: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 using your signing secret
  4. Compare the computed signature with the v1 value
  5. 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.

Auto-deactivation: If an endpoint accumulates 50 consecutive failures across all deliveries, it is automatically deactivated to prevent unnecessary load. You can reactivate it from the dashboard after fixing the issue.

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.