Back to Documentation
Code theme:

Web App Integration

Complete guide to integrating revnu payments into your web application using webhooks

Overview

revnu provides a webhook-based integration for web applications. When a customer makes a purchase or their subscription status changes, revnu sends a signed webhook to your application so you can grant or revoke access accordingly.

Key Features

  • Real-time purchase notifications via webhooks
  • Cryptographically signed payloads for security
  • Support for one-time purchases and subscriptions
  • Automatic subscription lifecycle management (renewals, cancellations, payment failures)

Integration Flow

  1. 1.Customer clicks a checkout link on your site
  2. 2.Customer completes payment on revnu/Stripe checkout
  3. 3.revnu sends a webhook to your server
  4. 4.Your server verifies the signature and grants access
  5. 5.For subscriptions, webhooks are sent on each renewal or status change

Webhook Events

purchase.completed

Sent when a customer completes a purchase (one-time or first subscription payment).

{
  "event": "purchase.completed",
  "data": {
    "purchaseId": "purch_abc123",
    "buyerEmail": "customer@example.com",
    "buyerName": "John Doe",
    "productId": "prod_xyz789",
    "productName": "Pro Plan",
    "amountCents": 2999,
    "currency": "usd",
    "isSubscription": true,
    "status": "active",
    "currentPeriodEnd": "2024-02-15T00:00:00Z"
  },
  "timestamp": "2024-01-15T12:00:00Z"
}
purchase.cancelled

Sent when a subscription is cancelled.

{
  "event": "purchase.cancelled",
  "data": {
    "purchaseId": "purch_abc123",
    "buyerEmail": "customer@example.com",
    "productId": "prod_xyz789",
    "productName": "Pro Plan",
    "status": "cancelled"
  },
  "timestamp": "2024-01-20T12:00:00Z"
}
payment.failed

Sent when a recurring subscription payment fails.

{
  "event": "payment.failed",
  "data": {
    "purchaseId": "purch_abc123",
    "buyerEmail": "customer@example.com",
    "productId": "prod_xyz789",
    "productName": "Pro Plan",
    "status": "past_due"
  },
  "timestamp": "2024-01-15T12:00:00Z"
}

Signature Verification

All webhooks are signed with your webhook secret using HMAC-SHA256. The signature is included in the x-rev-signature header.

import crypto from "crypto";

export function verifyRevSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Complete Webhook Handler

Full example for Next.js App Router:

// app/api/webhooks/revnu/route.ts
import crypto from "crypto";
import { NextResponse } from "next/server";

interface RevnuWebhookPayload {
  event: "purchase.completed" | "purchase.cancelled" | "payment.failed";
  data: {
    purchaseId: string;
    buyerEmail: string;
    buyerName?: string;
    productId: string;
    productName: string;
    amountCents?: number;
    currency?: string;
    isSubscription?: boolean;
    status?: "active" | "cancelled" | "past_due";
    currentPeriodEnd?: string;
  };
  timestamp?: string;
}

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

export async function POST(request: Request) {
  try {
    const payload = await request.text();
    const signature = request.headers.get("x-rev-signature") ?? "";

    if (!verifySignature(payload, signature, process.env.REV_WEBHOOK_SECRET!)) {
      return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
    }

    const event: RevnuWebhookPayload = JSON.parse(payload);

    switch (event.event) {
      case "purchase.completed":
        // Grant access to the user
        // await db.user.update({
        //   where: { email: event.data.buyerEmail },
        //   data: { subscriptionStatus: "active", productId: event.data.productId }
        // });
        break;

      case "purchase.cancelled":
        // Revoke access
        // await db.user.update({
        //   where: { email: event.data.buyerEmail },
        //   data: { subscriptionStatus: "cancelled" }
        // });
        break;

      case "payment.failed":
        // Notify user about payment failure
        // await sendPaymentFailedEmail(event.data.buyerEmail);
        break;
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error("Webhook error:", error);
    return NextResponse.json({ error: "Webhook processing failed" }, { status: 500 });
  }
}

Environment Variables

Add these to your .env.local:

# revnu Integration
REV_API_KEY=rev_your_api_key
REV_WEBHOOK_SECRET=whsec_your_webhook_secret

Best Practices

Idempotency

Webhooks may be delivered more than once. Make your handlers idempotent by checking if you've already processed a purchase using the purchaseId.

Error Handling

Return appropriate status codes so revnu knows whether to retry:200 for success,4xx for client errors (no retry),5xx for server errors (will retry).

Testing

Use the "Test" button in your revnu dashboard (Developers > Integration) to send a test webhook to your endpoint. For local development, use ngrok to expose your local server.

Rate Limits

Webhook deliveries are limited to prevent abuse:

  • Maximum 10 retries per webhook
  • Exponential backoff between retries (1s, 2s, 4s, 8s, etc.)
  • Webhooks timeout after 30 seconds