@reevit/node
v0.3.2
Published
Reevit Node.js SDK
Downloads
189
Maintainers
Readme
Reevit TypeScript SDK
The official Node.js/TypeScript SDK for Reevit — a unified payment orchestration platform for Africa.
Table of Contents
- Installation
- Quick Start
- Configuration
- Payments
- Connections
- Subscriptions
- Fraud Protection
- Error Handling
- TypeScript Types
- Supported Providers
- Examples
Installation
npm install @reevit/nodeOr using yarn:
yarn add @reevit/nodeOr using pnpm:
pnpm add @reevit/nodeQuick Start
import { Reevit } from '@reevit/node';
// Initialize the client
const reevit = new Reevit(
'pfk_live_your_api_key', // Your API key
'org_your_org_id' // Your organization ID
);
// Create a payment
const payment = await reevit.payments.createIntent({
amount: 5000, // 50.00 GHS (amount in smallest currency unit)
currency: 'GHS',
method: 'momo',
country: 'GH',
customer_id: 'cust_123'
});
console.log('Payment ID:', payment.id);
console.log('Status:', payment.status);Configuration
Basic Configuration
import { Reevit } from '@reevit/node';
const reevit = new Reevit(
'pfk_live_your_api_key', // API Key (required)
'org_your_org_id', // Organization ID (required)
'https://api.reevit.io' // Base URL (optional, defaults to localhost:8080)
);Environment Variables (Recommended)
import { Reevit } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!,
process.env.REEVIT_BASE_URL || 'https://api.reevit.io'
);API Key Types
| Key Prefix | Environment | Description |
|------------|-------------|-------------|
| pfk_live_ | Production | Live transactions, real money |
| pfk_test_ | Sandbox | Test transactions, no real charges |
Payments
Create Payment Intent
Create a new payment intent to initiate a transaction.
const payment = await reevit.payments.createIntent({
amount: 10000, // 100.00 in smallest currency unit
currency: 'GHS', // Currency code (GHS, NGN, KES, USD)
method: 'momo', // Payment method
country: 'GH', // ISO country code
customer_id: 'cust_123', // Optional: Your customer reference
metadata: { // Optional: Custom metadata
order_id: 'order_456',
product: 'Premium Plan'
}
});Payment Methods by Country
| Country | Code | Supported Methods |
|---------|------|-------------------|
| Ghana | GH | momo, card, bank_transfer |
| Nigeria | NG | card, bank_transfer, ussd |
| Kenya | KE | mpesa, card |
Full Example with All Options
const payment = await reevit.payments.createIntent({
amount: 25000,
currency: 'NGN',
method: 'card',
country: 'NG',
customer_id: 'cust_789',
metadata: {
order_id: 'ORD-2024-001',
customer_email: '[email protected]',
description: 'Monthly subscription'
},
policy: {
prefer: ['paystack', 'flutterwave'], // Preferred providers
max_amount: 100000, // Max transaction amount
velocity_max_per_minute: 5 // Rate limiting
}
});
console.log('Payment created:', {
id: payment.id,
status: payment.status,
provider: payment.provider,
providerRef: payment.provider_ref_id
});Get Payment
Retrieve details of a specific payment.
const payment = await reevit.payments.get('pay_abc123');
console.log('Payment Details:', {
id: payment.id,
status: payment.status,
amount: payment.amount,
currency: payment.currency,
fee: payment.fee_amount,
net: payment.net_amount,
provider: payment.provider,
method: payment.method,
createdAt: payment.created_at
});
// Check routing attempts (useful for debugging)
if (payment.route && payment.route.length > 0) {
console.log('Routing attempts:');
payment.route.forEach((attempt, index) => {
console.log(` ${index + 1}. ${attempt.provider}: ${attempt.status}`);
if (attempt.error) {
console.log(` Error: ${attempt.error}`);
}
});
}List Payments
Retrieve a paginated list of payments.
// Basic listing (default: 50 payments)
const payments = await reevit.payments.list();
// With pagination
const page1 = await reevit.payments.list(10, 0); // First 10
const page2 = await reevit.payments.list(10, 10); // Next 10
// Process payments
payments.forEach(payment => {
console.log(`${payment.id}: ${payment.status} - ${payment.currency} ${payment.amount / 100}`);
});Pagination Example
import { PaymentSummary } from '@reevit/node';
async function getAllPayments(): Promise<PaymentSummary[]> {
const allPayments: PaymentSummary[] = [];
const pageSize = 50;
let offset = 0;
let hasMore = true;
while (hasMore) {
const batch = await reevit.payments.list(pageSize, offset);
allPayments.push(...batch);
if (batch.length < pageSize) {
hasMore = false;
} else {
offset += pageSize;
}
}
return allPayments;
}Refund Payment
Issue a full or partial refund for a payment.
// Full refund
const fullRefund = await reevit.payments.refund('pay_abc123');
// Partial refund
const partialRefund = await reevit.payments.refund(
'pay_abc123',
2500, // Refund 25.00
'Customer requested' // Reason (optional)
);
console.log('Refund:', {
id: partialRefund.id,
paymentId: partialRefund.payment_id,
amount: partialRefund.amount,
status: partialRefund.status,
reason: partialRefund.reason
});Connections
Connections represent your integrations with payment service providers (PSPs).
Create Connection
// Paystack Connection (Nigeria)
const paystackConnection = await reevit.connections.create({
provider: 'paystack',
mode: 'live', // 'live' or 'test'
credentials: {
secret_key: 'sk_live_xxxxx'
},
labels: ['nigeria', 'primary'],
routing_hints: {
country_preference: ['NG'],
method_bias: { card: 'high', bank_transfer: 'medium' },
fallback_only: false
}
});
// Flutterwave Connection (Multi-country)
const flutterwaveConnection = await reevit.connections.create({
provider: 'flutterwave',
mode: 'live',
credentials: {
secret_key: 'FLWSECK-xxxxx',
encryption_key: 'xxxxx'
},
labels: ['multi-country', 'backup'],
routing_hints: {
country_preference: ['GH', 'NG', 'KE'],
fallback_only: true
}
});
// Hubtel Connection (Ghana)
const hubtelConnection = await reevit.connections.create({
provider: 'hubtel',
mode: 'live',
credentials: {
client_id: 'xxxxx',
client_secret: 'xxxxx'
},
labels: ['ghana', 'momo'],
routing_hints: {
country_preference: ['GH'],
method_bias: { momo: 'high' }
}
});
// M-Pesa Connection (Kenya)
const mpesaConnection = await reevit.connections.create({
provider: 'mpesa',
mode: 'live',
credentials: {
consumer_key: 'xxxxx',
consumer_secret: 'xxxxx',
passkey: 'xxxxx',
shortcode: '174379',
initiator_name: 'testapi',
security_credential: 'xxxxx' // Pre-encrypted
},
labels: ['kenya', 'mpesa'],
routing_hints: {
country_preference: ['KE'],
method_bias: { mpesa: 'high' }
}
});
// Monnify Connection (Nigeria)
const monnifyConnection = await reevit.connections.create({
provider: 'monnify',
mode: 'live',
credentials: {
api_key: 'xxxxx',
secret_key: 'xxxxx',
contract_code: 'xxxxx'
},
labels: ['nigeria', 'bank_transfer'],
routing_hints: {
country_preference: ['NG'],
method_bias: { bank_transfer: 'high' }
}
});List Connections
const connections = await reevit.connections.list();
connections.forEach(conn => {
console.log(`${conn.provider} (${conn.mode}): ${conn.status}`);
console.log(` Labels: ${conn.labels.join(', ')}`);
console.log(` Countries: ${conn.routing_hints.country_preference.join(', ')}`);
});Test Connection
Verify credentials before creating a connection.
const isValid = await reevit.connections.test({
provider: 'paystack',
mode: 'live',
credentials: {
secret_key: 'sk_live_xxxxx'
}
});
if (isValid) {
console.log('Connection credentials are valid');
} else {
console.log('Invalid credentials');
}Subscriptions
Manage recurring billing and subscriptions.
Create Subscription
// Monthly subscription
const monthlySubscription = await reevit.subscriptions.create({
customer_id: 'cust_123',
plan_id: 'plan_premium',
amount: 9900, // 99.00 per month
currency: 'GHS',
method: 'momo',
interval: 'monthly',
metadata: {
plan_name: 'Premium',
features: ['unlimited_access', 'priority_support']
}
});
// Yearly subscription
const yearlySubscription = await reevit.subscriptions.create({
customer_id: 'cust_456',
plan_id: 'plan_enterprise',
amount: 99900, // 999.00 per year
currency: 'NGN',
method: 'card',
interval: 'yearly',
metadata: {
plan_name: 'Enterprise',
discount_applied: '2_months_free'
}
});
console.log('Subscription created:', {
id: monthlySubscription.id,
status: monthlySubscription.status,
nextRenewal: monthlySubscription.next_renewal_at
});List Subscriptions
const subscriptions = await reevit.subscriptions.list();
subscriptions.forEach(sub => {
console.log(`${sub.id}: ${sub.status}`);
console.log(` Customer: ${sub.customer_id}`);
console.log(` Amount: ${sub.currency} ${sub.amount / 100}/${sub.interval}`);
console.log(` Next renewal: ${sub.next_renewal_at}`);
});Fraud Protection
Configure fraud rules to protect your transactions.
Get Fraud Policy
const policy = await reevit.fraud.get();
console.log('Current Fraud Policy:', {
preferredProviders: policy.prefer,
maxAmount: policy.max_amount,
blockedBins: policy.blocked_bins,
allowedBins: policy.allowed_bins,
velocityLimit: policy.velocity_max_per_minute
});Update Fraud Policy
const updatedPolicy = await reevit.fraud.update({
prefer: ['paystack', 'flutterwave'],
max_amount: 500000, // Max 5,000.00
blocked_bins: ['123456', '654321'], // Block specific card BINs
allowed_bins: [], // Empty = allow all (except blocked)
velocity_max_per_minute: 10 // Max 10 transactions per minute
});
console.log('Policy updated successfully');Fraud Policy Options
| Option | Type | Description |
|--------|------|-------------|
| prefer | string[] | Preferred provider order for routing |
| max_amount | number | Maximum transaction amount (in smallest unit) |
| blocked_bins | string[] | Card BIN prefixes to block |
| allowed_bins | string[] | Only allow these BINs (empty = allow all) |
| velocity_max_per_minute | number | Rate limit per customer |
Error Handling
The SDK throws errors for failed API calls. Always wrap calls in try-catch blocks.
import { AxiosError } from 'axios';
async function createPaymentSafely() {
try {
const payment = await reevit.payments.createIntent({
amount: 5000,
currency: 'GHS',
method: 'momo',
country: 'GH'
});
return payment;
} catch (error) {
if (error instanceof AxiosError) {
// API error
console.error('API Error:', {
status: error.response?.status,
message: error.response?.data?.message || error.message,
code: error.response?.data?.code
});
// Handle specific error codes
switch (error.response?.status) {
case 400:
console.error('Bad request - check your parameters');
break;
case 401:
console.error('Unauthorized - check your API key');
break;
case 403:
console.error('Forbidden - check your permissions');
break;
case 404:
console.error('Resource not found');
break;
case 429:
console.error('Rate limited - slow down requests');
break;
case 500:
console.error('Server error - try again later');
break;
}
} else {
// Network or other error
console.error('Unexpected error:', error);
}
throw error;
}
}Retry Logic Example
import { Payment, PaymentIntentRequest } from '@reevit/node';
import { AxiosError } from 'axios';
async function createPaymentWithRetry(
data: PaymentIntentRequest,
maxRetries = 3,
delayMs = 1000
): Promise<Payment> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await reevit.payments.createIntent(data);
} catch (error) {
lastError = error as Error;
if (error instanceof AxiosError) {
// Don't retry client errors (4xx)
if (error.response?.status && error.response.status < 500) {
throw error;
}
}
if (attempt < maxRetries) {
console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
delayMs *= 2; // Exponential backoff
}
}
}
throw lastError;
}TypeScript Types
The SDK exports all types for use in your application.
import {
// Payment types
Payment,
PaymentSummary,
PaymentIntentRequest,
PaymentRouteAttempt,
Refund,
// Connection types
Connection,
ConnectionRequest,
RoutingHints,
// Subscription types
Subscription,
SubscriptionRequest,
// Fraud types
FraudPolicy,
FraudPolicyInput
} from '@reevit/node';
// Use types in your code
function processPayment(payment: Payment): void {
console.log(`Processing ${payment.id}`);
}
function buildPaymentRequest(): PaymentIntentRequest {
return {
amount: 5000,
currency: 'GHS',
method: 'momo',
country: 'GH'
};
}Supported Providers
Reevit currently supports the following payment service providers:
| Provider | Countries | Methods | Features | |----------|-----------|---------|----------| | Paystack | Nigeria, Ghana | Card, Bank Transfer, USSD | Refunds, Webhooks | | Flutterwave | Nigeria, Ghana, Kenya, +30 | Card, Mobile Money, Bank | Refunds, Webhooks | | Hubtel | Ghana | Mobile Money | Webhooks | | M-Pesa | Kenya | M-Pesa (STK Push) | Reversals, Webhooks | | Monnify | Nigeria | Bank Transfer, Card | Refunds, Webhooks | | Stripe | Global | Card (incl. Apple Pay/Google Pay via card rails) | Refunds, Webhooks |
Stripe webhooks: configure the Reevit webhook URL in Stripe; store the signing secret in the connection credentials (
stripe_webhook_secret,webhook_secret, orsigning_secret). Ensure PaymentIntent metadata includesorg_id,connection_id, andpayment_idso events map correctly.
Examples
E-commerce Checkout
import { Reevit, Payment, PaymentIntentRequest } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!,
process.env.REEVIT_BASE_URL
);
interface CheckoutData {
orderId: string;
customerId: string;
amount: number;
currency: string;
country: string;
paymentMethod: string;
customerEmail: string;
}
async function processCheckout(checkout: CheckoutData): Promise<Payment> {
const paymentRequest: PaymentIntentRequest = {
amount: checkout.amount,
currency: checkout.currency,
method: checkout.paymentMethod,
country: checkout.country,
customer_id: checkout.customerId,
metadata: {
order_id: checkout.orderId,
customer_email: checkout.customerEmail,
source: 'web_checkout'
}
};
const payment = await reevit.payments.createIntent(paymentRequest);
console.log(`Payment ${payment.id} created for order ${checkout.orderId}`);
console.log(`Status: ${payment.status}`);
console.log(`Provider: ${payment.provider}`);
return payment;
}
// Usage
processCheckout({
orderId: 'ORD-2024-001',
customerId: 'cust_abc123',
amount: 15000, // 150.00
currency: 'GHS',
country: 'GH',
paymentMethod: 'momo',
customerEmail: '[email protected]'
});Subscription Service
import { Reevit, Subscription } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!
);
interface Plan {
id: string;
name: string;
monthlyPrice: number;
yearlyPrice: number;
currency: string;
}
const plans: Plan[] = [
{ id: 'basic', name: 'Basic', monthlyPrice: 1999, yearlyPrice: 19990, currency: 'GHS' },
{ id: 'pro', name: 'Pro', monthlyPrice: 4999, yearlyPrice: 49990, currency: 'GHS' },
{ id: 'enterprise', name: 'Enterprise', monthlyPrice: 9999, yearlyPrice: 99990, currency: 'GHS' }
];
async function subscribeToPlan(
customerId: string,
planId: string,
interval: 'monthly' | 'yearly',
paymentMethod: string
): Promise<Subscription> {
const plan = plans.find(p => p.id === planId);
if (!plan) {
throw new Error(`Plan ${planId} not found`);
}
const amount = interval === 'monthly' ? plan.monthlyPrice : plan.yearlyPrice;
const subscription = await reevit.subscriptions.create({
customer_id: customerId,
plan_id: planId,
amount,
currency: plan.currency,
method: paymentMethod,
interval,
metadata: {
plan_name: plan.name,
billing_interval: interval
}
});
console.log(`Subscription created: ${subscription.id}`);
console.log(`Next renewal: ${subscription.next_renewal_at}`);
return subscription;
}
// Usage
subscribeToPlan('cust_123', 'pro', 'monthly', 'momo');Multi-Provider Setup
import { Reevit } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!
);
async function setupProviders() {
// Primary provider for Nigeria
await reevit.connections.create({
provider: 'paystack',
mode: 'live',
credentials: {
secret_key: process.env.PAYSTACK_SECRET_KEY!
},
labels: ['nigeria', 'primary'],
routing_hints: {
country_preference: ['NG'],
method_bias: { card: 'high' },
fallback_only: false
}
});
// Backup provider for Nigeria
await reevit.connections.create({
provider: 'flutterwave',
mode: 'live',
credentials: {
secret_key: process.env.FLUTTERWAVE_SECRET_KEY!,
encryption_key: process.env.FLUTTERWAVE_ENCRYPTION_KEY!
},
labels: ['nigeria', 'backup'],
routing_hints: {
country_preference: ['NG'],
fallback_only: true
}
});
// Primary provider for Ghana
await reevit.connections.create({
provider: 'hubtel',
mode: 'live',
credentials: {
client_id: process.env.HUBTEL_CLIENT_ID!,
client_secret: process.env.HUBTEL_CLIENT_SECRET!
},
labels: ['ghana', 'momo'],
routing_hints: {
country_preference: ['GH'],
method_bias: { momo: 'high' },
fallback_only: false
}
});
// Primary provider for Kenya
await reevit.connections.create({
provider: 'mpesa',
mode: 'live',
credentials: {
consumer_key: process.env.MPESA_CONSUMER_KEY!,
consumer_secret: process.env.MPESA_CONSUMER_SECRET!,
passkey: process.env.MPESA_PASSKEY!,
shortcode: process.env.MPESA_SHORTCODE!,
initiator_name: process.env.MPESA_INITIATOR_NAME!,
security_credential: process.env.MPESA_SECURITY_CREDENTIAL!
},
labels: ['kenya', 'mpesa'],
routing_hints: {
country_preference: ['KE'],
method_bias: { mpesa: 'high' },
fallback_only: false
}
});
console.log('All providers configured successfully');
// List all connections
const connections = await reevit.connections.list();
console.log(`Total connections: ${connections.length}`);
connections.forEach(c => {
console.log(` - ${c.provider} (${c.mode}): ${c.status}`);
});
}
setupProviders();Payment Status Polling
import { Reevit, Payment } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!
);
async function waitForPaymentCompletion(
paymentId: string,
timeoutMs = 300000, // 5 minutes
pollIntervalMs = 5000
): Promise<Payment> {
const startTime = Date.now();
const terminalStatuses = ['succeeded', 'failed', 'canceled'];
while (Date.now() - startTime < timeoutMs) {
const payment = await reevit.payments.get(paymentId);
console.log(`Payment ${paymentId}: ${payment.status}`);
if (terminalStatuses.includes(payment.status)) {
return payment;
}
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
}
throw new Error(`Payment ${paymentId} did not complete within ${timeoutMs}ms`);
}
// Usage
async function processAndWait() {
const payment = await reevit.payments.createIntent({
amount: 5000,
currency: 'GHS',
method: 'momo',
country: 'GH'
});
console.log(`Created payment: ${payment.id}`);
const completedPayment = await waitForPaymentCompletion(payment.id);
if (completedPayment.status === 'succeeded') {
console.log('Payment successful!');
console.log(`Net amount: ${completedPayment.net_amount}`);
} else {
console.log(`Payment ${completedPayment.status}`);
}
}Refund Management
import { Reevit, Payment, Refund } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!
);
interface RefundRequest {
paymentId: string;
amount?: number; // Optional for partial refund
reason: string;
}
async function processRefund(request: RefundRequest): Promise<Refund> {
// First, verify the payment exists and is refundable
const payment = await reevit.payments.get(request.paymentId);
if (payment.status !== 'succeeded') {
throw new Error(`Cannot refund payment with status: ${payment.status}`);
}
// Validate refund amount
const refundAmount = request.amount || payment.amount;
if (refundAmount > payment.amount) {
throw new Error(`Refund amount (${refundAmount}) exceeds payment amount (${payment.amount})`);
}
// Process the refund
const refund = await reevit.payments.refund(
request.paymentId,
request.amount,
request.reason
);
console.log(`Refund processed: ${refund.id}`);
console.log(` Amount: ${refund.amount}`);
console.log(` Status: ${refund.status}`);
console.log(` Reason: ${refund.reason}`);
return refund;
}
// Full refund
processRefund({
paymentId: 'pay_abc123',
reason: 'Customer requested cancellation'
});
// Partial refund
processRefund({
paymentId: 'pay_abc123',
amount: 2500, // Refund 25.00
reason: 'Partial order cancellation'
});Express.js Integration
import express from 'express';
import { Reevit, PaymentIntentRequest } from '@reevit/node';
const app = express();
app.use(express.json());
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!,
process.env.REEVIT_BASE_URL
);
// Create payment endpoint
app.post('/api/payments', async (req, res) => {
try {
const { amount, currency, method, country, customerId, orderId } = req.body;
const payment = await reevit.payments.createIntent({
amount,
currency,
method,
country,
customer_id: customerId,
metadata: { order_id: orderId }
});
res.json({
success: true,
payment: {
id: payment.id,
status: payment.status,
provider: payment.provider
}
});
} catch (error: any) {
res.status(error.response?.status || 500).json({
success: false,
error: error.response?.data?.message || error.message
});
}
});
// Get payment status endpoint
app.get('/api/payments/:id', async (req, res) => {
try {
const payment = await reevit.payments.get(req.params.id);
res.json({ success: true, payment });
} catch (error: any) {
res.status(error.response?.status || 500).json({
success: false,
error: error.response?.data?.message || error.message
});
}
});
// Refund endpoint
app.post('/api/payments/:id/refund', async (req, res) => {
try {
const { amount, reason } = req.body;
const refund = await reevit.payments.refund(req.params.id, amount, reason);
res.json({ success: true, refund });
} catch (error: any) {
res.status(error.response?.status || 500).json({
success: false,
error: error.response?.data?.message || error.message
});
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});Next.js API Routes
// pages/api/payments/create.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { Reevit } from '@reevit/node';
const reevit = new Reevit(
process.env.REEVIT_API_KEY!,
process.env.REEVIT_ORG_ID!,
process.env.REEVIT_BASE_URL
);
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { amount, currency, method, country, customerId } = req.body;
const payment = await reevit.payments.createIntent({
amount,
currency,
method,
country,
customer_id: customerId
});
res.status(200).json({ payment });
} catch (error: any) {
res.status(500).json({
error: error.response?.data?.message || error.message
});
}
}Webhook Verification
Reevit sends webhooks to notify your application of payment events. Always verify webhook signatures to ensure authenticity.
Understanding Webhooks
There are two types of webhooks in Reevit:
Inbound Webhooks (PSP → Reevit): Webhooks from payment providers to Reevit. You configure these in the PSP dashboard (e.g., Paystack). Reevit handles these automatically.
Outbound Webhooks (Reevit → Your App): Webhooks from Reevit to your application. You configure these in the Reevit Dashboard and create a handler in your app.
Signature Format
Reevit signs webhooks with HMAC-SHA256:
- Header:
X-Reevit-Signature: sha256=<hex-signature> - Signature:
HMAC-SHA256(request_body, signing_secret)
Getting Your Signing Secret
- Go to Reevit Dashboard > Developers > Webhooks
- Configure your webhook endpoint URL
- Copy the signing secret (starts with
whsec_) - Add to your environment:
REEVIT_WEBHOOK_SECRET=whsec_xxx...
Next.js App Router Webhook Handler
// app/api/webhooks/reevit/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
// Webhook payload types
interface PaymentData {
id: string;
status: string;
amount: number;
currency: string;
provider: string;
customer_id?: string;
metadata?: Record<string, string>;
}
interface SubscriptionData {
id: string;
customer_id: string;
plan_id: string;
status: string;
amount: number;
currency: string;
interval: string;
next_renewal_at?: string;
}
interface WebhookPayload {
id: string;
type: string;
org_id: string;
created_at: string;
data?: PaymentData | SubscriptionData;
message?: string;
}
/**
* Verify webhook signature using HMAC-SHA256
* Always verify signatures in production to prevent spoofed webhooks
*/
function verifySignature(payload: string, signature: string, secret: string): boolean {
if (!signature.startsWith("sha256=")) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
const received = signature.slice(7);
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
export async function POST(request: NextRequest) {
try {
const rawBody = await request.text();
const signature = request.headers.get("x-reevit-signature") || "";
const secret = process.env.REEVIT_WEBHOOK_SECRET;
// Verify signature (required in production)
if (secret && !verifySignature(rawBody, signature, secret)) {
console.error("[Webhook] Invalid signature");
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const event: WebhookPayload = JSON.parse(rawBody);
console.log(`[Webhook] Received: ${event.type} (${event.id})`);
// Handle different event types
switch (event.type) {
// Test event
case "reevit.webhook.test":
console.log("[Webhook] Test received:", event.message);
break;
// Payment events
case "payment.succeeded":
await handlePaymentSucceeded(event.data as PaymentData);
break;
case "payment.failed":
await handlePaymentFailed(event.data as PaymentData);
break;
case "payment.refunded":
await handlePaymentRefunded(event.data as PaymentData);
break;
case "payment.pending":
console.log(`[Webhook] Payment pending: ${(event.data as PaymentData)?.id}`);
break;
// Subscription events
case "subscription.created":
await handleSubscriptionCreated(event.data as SubscriptionData);
break;
case "subscription.renewed":
await handleSubscriptionRenewed(event.data as SubscriptionData);
break;
case "subscription.canceled":
await handleSubscriptionCanceled(event.data as SubscriptionData);
break;
case "subscription.paused":
console.log(`[Webhook] Subscription paused: ${(event.data as SubscriptionData)?.id}`);
break;
default:
console.log(`[Webhook] Unhandled event: ${event.type}`);
}
return NextResponse.json({ received: true });
} catch (error) {
console.error("[Webhook] Processing error:", error);
return NextResponse.json({ error: "Webhook processing failed" }, { status: 500 });
}
}
// Payment handlers
async function handlePaymentSucceeded(data: PaymentData) {
const orderId = data.metadata?.order_id;
console.log(`[Webhook] Payment succeeded: ${data.id} for order ${orderId}`);
// TODO: Implement your business logic
// - Update order status to "paid"
// - Send confirmation email to customer
// - Trigger fulfillment process
// - Update inventory
}
async function handlePaymentFailed(data: PaymentData) {
console.log(`[Webhook] Payment failed: ${data.id}`);
// TODO: Implement your business logic
// - Update order status to "payment_failed"
// - Send notification to customer
// - Allow retry
}
async function handlePaymentRefunded(data: PaymentData) {
const orderId = data.metadata?.order_id;
console.log(`[Webhook] Payment refunded: ${data.id} for order ${orderId}`);
// TODO: Implement your business logic
// - Update order status to "refunded"
// - Restore inventory if applicable
// - Send refund confirmation email
}
// Subscription handlers
async function handleSubscriptionCreated(data: SubscriptionData) {
console.log(`[Webhook] Subscription created: ${data.id} for customer ${data.customer_id}`);
// TODO: Implement your business logic
// - Grant access to subscription features
// - Send welcome email
}
async function handleSubscriptionRenewed(data: SubscriptionData) {
console.log(`[Webhook] Subscription renewed: ${data.id}`);
// TODO: Implement your business logic
// - Extend access period
// - Send renewal confirmation
}
async function handleSubscriptionCanceled(data: SubscriptionData) {
console.log(`[Webhook] Subscription canceled: ${data.id}`);
// TODO: Implement your business logic
// - Revoke access at end of billing period
// - Send cancellation confirmation
}Express.js Webhook Handler
import express from 'express';
import crypto from 'crypto';
const app = express();
function verifySignature(payload: string, signature: string, secret: string): boolean {
if (!signature.startsWith('sha256=')) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const received = signature.slice(7);
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
// IMPORTANT: Use raw body for signature verification
app.post('/webhooks/reevit', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const signature = req.headers['x-reevit-signature'] as string;
const payload = req.body.toString();
const secret = process.env.REEVIT_WEBHOOK_SECRET!;
if (secret && !verifySignature(payload, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
console.log(`[Webhook] Received: ${event.type}`);
switch (event.type) {
case 'reevit.webhook.test':
console.log('[Webhook] Test received:', event.message);
break;
case 'payment.succeeded':
// Fulfill order, send confirmation email
break;
case 'payment.failed':
// Notify customer, allow retry
break;
case 'payment.refunded':
// Update order status
break;
case 'subscription.renewed':
// Extend access
break;
case 'subscription.canceled':
// Revoke access
break;
}
res.status(200).json({ received: true });
} catch (error) {
console.error('[Webhook] Error:', error);
res.status(500).json({ error: 'Webhook processing failed' });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));Environment Variables
# .env.local
REEVIT_API_KEY=pfk_live_xxx
REEVIT_ORG_ID=org_xxx
REEVIT_WEBHOOK_SECRET=whsec_xxx # Get from Dashboard > Developers > WebhooksSupport
- Documentation: https://docs.reevit.io
- API Reference: https://api.reevit.io/docs
- GitHub Issues: https://github.com/reevit/reevit-node/issues
- Email: [email protected]
License
MIT License - see LICENSE for details.
