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.Customer clicks a checkout link on your site
- 2.Customer completes payment on revnu/Stripe checkout
- 3.revnu sends a webhook to your server
- 4.Your server verifies the signature and grants access
- 5.For subscriptions, webhooks are sent on each renewal or status change
Webhook Events
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"
}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"
}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_secretBest 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/api/v1/accessCheck 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=123456789Response
{
"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
}
]
}/api/v1/customers/:emailGet full details about a customer including all purchases.
GET /api/v1/customers/user@example.com
# or for Discord users
GET /api/v1/customers/discord:123456789Caching
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" });
}