This guide will help you implement PayTabs in a production-ready way using:
- Next.js 14+ (App Router)
- TypeScript for type safety
- React Query for API state management
- Design Patterns (Facade, Adapter, Observer) for clean architecture
🛠️ Prerequisites
Before diving in, ensure you have:
- A PayTabs merchant account (Sign up here)
- Get your Profile ID, Server Key, and Region from the dashboard
- A Next.js 14+ project with TypeScript
- Node.js (v18+) installed
Install required dependencies:
npm install paytabs_pt2 axios
Set up environment variables (.env.local
):
PAYTABS_PROFILE_ID=your_profile_id
PAYTABS_SERVER_KEY=your_server_key
PAYTABS_REGION=EGY # Adjust based on your region
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Step 1: Create the PayTabs Service (Backend)
This service acts as a facade over the PayTabs SDK, providing a clean interface for transactions and webhook verification.
// lib/services/PayTabsService.ts
import paytabs from "paytabs_pt2";
import { PaymentResponse, TransactionRequest } from "@/types/paytabs";
export class PayTabsService {
constructor() {
const profileID = process.env.PAYTABS_PROFILE_ID!;
const serverKey = process.env.PAYTABS_SERVER_KEY!;
const region = process.env.PAYTABS_REGION!;
paytabs.setConfig(profileID, serverKey, region);
}
async createTransaction(request: TransactionRequest): Promise<PaymentResponse> {
try {
const response = await new Promise<PaymentResponse>((resolve, reject) => {
paytabs.createTransaction(request, (error, result) => {
if (error) reject(error);
else resolve(result);
});
});
if (!response.redirect_url) {
throw new Error("Missing redirect_url in PayTabs response");
}
return response;
} catch (error) {
throw new Error(`Payment failed: ${error.message}`);
}
}
verifyWebhookSignature(payload: any, signature: string): boolean {
// Implement HMAC-SHA256 verification
return true; // Simplified for example
}
}
Type Definitions:
// types/paytabs.ts
export interface TransactionRequest {
cart_id: string;
cart_description: string;
cart_currency: string;
cart_amount: number;
return_url: string;
callback_url: string;
customer_details: {
name: string;
email: string;
phone: string;
street1: string;
city: string;
state: string;
country: string;
zip: string;
};
}
export interface PaymentResponse {
tran_ref: string;
redirect_url: string;
status: string;
}
🔄 Step 2: API Route for Transaction Initiation
Create a Next.js API route to handle payment requests:
// app/api/paytabs/create-transaction/route.ts
import { NextResponse } from "next/server";
import { PayTabsService } from "@/lib/services/PayTabsService";
export async function POST(req: Request) {
try {
const { amount, currency, customer } = await req.json();
const request = {
cart_id: `order_${Date.now()}`,
cart_description: "Online Purchase",
cart_currency: currency,
cart_amount: amount,
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/payment/success`,
callback_url: `${process.env.NEXT_PUBLIC_BASE_URL}/api/paytabs/webhook`,
customer_details: customer,
};
const payTabsService = new PayTabsService();
const { redirect_url, tran_ref } = await payTabsService.createTransaction(request);
return NextResponse.json({ redirect_url, tran_ref });
} catch (error) {
return NextResponse.json(
{ error: "Payment initiation failed" },
{ status: 500 }
);
}
}
📩 Step 3: Webhook Handling
Set up a webhook endpoint to process real-time payment updates:
// app/api/paytabs/webhook/route.ts
import { NextResponse } from "next/server";
import { PayTabsService } from "@/lib/services/PayTabsService";
export async function POST(req: Request) {
const payload = await req.json();
const signature = req.headers.get("x-paytabs-signature") || "";
const payTabsService = new PayTabsService();
const isValid = payTabsService.verifyWebhookSignature(payload, signature);
if (!isValid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// Update your database here
console.log("Payment status:", payload.payment_result.response_status);
return NextResponse.json({ success: true });
}
💡 Pro Tip: Use ngrok to test webhooks locally!
💻 Step 4: Frontend Payment Form
Build a React component to initiate payments:
// components/PaymentForm.tsx
"use client";
import { useMutation } from "@tanstack/react-query";
export default function PaymentForm() {
const mutation = useMutation({
mutationFn: (data) =>
fetch("/api/paytabs/create-transaction", {
method: "POST",
body: JSON.stringify(data),
}).then((res) => res.json()),
onSuccess: (data) => {
window.location.href = data.redirect_url; // Redirect to PayTabs
},
});
const handleSubmit = () => {
mutation.mutate({
amount: 100,
currency: "USD",
customer: {
name: "John Doe",
email: "john@example.com",
// ...other fields
},
});
};
return (
<button onClick={handleSubmit} disabled={mutation.isPending}>
{mutation.isPending ? "Processing..." : "Pay Now"}
</button>
);
}
🎯 Best Practices for Production
-
Security
- Never expose API keys in client-side code
- Validate webhook signatures
- Use HTTPS everywhere
-
Performance
- Cache API responses with Redis
- Deploy on Vercel's edge network
-
Monitoring
- Log errors with Sentry
- Track transactions with Datadog
-
Testing
- Use PayTabs test cards (
4111 1111 1111 1111
) - Simulate failed payments
- Use PayTabs test cards (
🔗 Resources
Have questions? Drop them in the comments below!