Back to Documentation
Code theme:

Web App Integration

Complete guide to integrating revnu 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

REST API

In addition to webhooks, you can query the revnu REST API directly to check subscription status. This is useful for profile pages, account settings, or as backup verification.

Recommended Approach

  • Use webhooks + database for access control (real-time, fast lookups)
  • Use REST API for profile pages, account info, or backup verification

Authentication

Include your API key in the Authorization header:

Authorization: Bearer rev_your_api_key
GET/api/v1/access

Check if a user has active access to any of your products.

GET /api/v1/access?email=user@example.com
# or
GET /api/v1/access?discordId=123456789

Response

{
  "hasAccess": true,
  "customer": {
    "email": "user@example.com",
    "discordId": "123456789",
    "name": "John Doe"
  },
  "products": [
    {
      "id": "prod_abc123",
      "name": "Pro Plan",
      "status": "active",
      "isSubscription": true,
      "cancelAtPeriodEnd": false
    }
  ]
}
GET/api/v1/customers/:email

Get full details about a customer including all purchases.

GET /api/v1/customers/user@example.com
# or for Discord users
GET /api/v1/customers/discord:123456789

Caching

API responses are cached at the edge for 30 minutes. This means subscription changes may take up to 30 minutes to reflect in API responses. For real-time updates, use webhooks.

Example: Checking Access

// lib/revnu.ts
export async function checkRevnuAccess(email: string) {
  const response = await fetch(
    `https://yourstore.revnu.co/api/v1/access?email=${encodeURIComponent(email)}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.REV_API_KEY}`,
      },
      next: { revalidate: 300 }, // Cache for 5 minutes
    }
  );

  if (!response.ok) {
    throw new Error("Failed to check access");
  }

  return response.json();
}

// Usage in a route handler
export async function GET(request: Request) {
  const session = await getSession();
  const { hasAccess } = await checkRevnuAccess(session.user.email);

  if (!hasAccess) {
    return NextResponse.json({ error: "Subscription required" }, { status: 403 });
  }

  return NextResponse.json({ data: "Premium content" });
}