Vertueal Docs
SubsManager

Webhooks & Event Streams

Receive real-time event notifications with cryptographic HMAC-SHA256 signature verification.

Webhooks & Event Streams

SubsManager delivers asynchronous, cryptographically signed webhooks to your server whenever critical lifecycle events occur (such as invoice payments, subscription state transitions, or renewal warnings).


1. Supported Event Catalog

Event NameTrigger ContextRecommended Action
subscription.createdSubscription initialized (trialing or pending)Provision user record in your local database.
subscription.updatedState change (active, past_due, cancelled)Grant, throttle, or revoke feature access.
subscription.upcoming_renewal3 days before period endSurface in-app banner reminding subscriber of renewal.
invoice.createdNew invoice generated (initial or renewal)Record invoice in internal accounting ledger.
invoice.paidInvoice confirmed paid (EFT or Card)Issue receipt to customer and extend active period.
payment.createdOne-time payment initializedLog checkout attempt.
payment.updatedPayment transitioned to completed or failedFulfill digital goods or alert customer to card decline.

2. Cryptographic Signature Verification (HMAC-SHA256)

Every webhook request transmitted by SubsManager includes an x-subsmanager-signature HTTP header. This signature is an HMAC-SHA256 hash of the exact raw request body calculated with your application's webhookSecret (whsec_...).

Timing-Safe Comparison

Always perform string comparison using constant-time equality checks (such as crypto.timingSafeEqual) to prevent timing attacks.

Node.js / Express Example

const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/api/webhooks/subsmanager', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-subsmanager-signature'];
  const secret = process.env.SUBSMANAGER_WEBHOOK_SECRET;

  if (!signature) {
    return res.status(401).send('Missing signature');
  }

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'utf8'),
    Buffer.from(expectedSignature, 'utf8')
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(req.body.toString('utf8'));
  const { event, data } = payload;

  if (event === 'subscription.updated' && data.status === 'active') {
    // Unlock premium SaaS features for subscriber
    console.log(`Activated subscription for client: ${data.clientId}`);
  }

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

Next.js Route Handler Example (App Router)

// app/api/webhooks/subsmanager/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(req: NextRequest) {
  const signature = req.headers.get('x-subsmanager-signature');
  const secret = process.env.SUBSMANAGER_WEBHOOK_SECRET!;

  const rawBody = await req.text();

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  if (signature !== expectedSignature) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }

  const { event, data } = JSON.parse(rawBody);

  if (event === 'invoice.paid') {
    console.log(`Invoice ${data.invoiceNumber} paid: ${data.currency} ${data.amount}`);
  }

  return NextResponse.json({ received: true });
}

On this page