Integrating PayTabs Payment Gateway with Next.js: A Scalable Approach
Muhammad Aqib Bin Azam

Muhammad Aqib Bin Azam @muhammadaqib86

About: Greetings! I'm a Full Stack Developer having experience in building user desired web and as well as mobile application with Pixel Perfect UI across all devices

Joined:
Jan 24, 2025

Integrating PayTabs Payment Gateway with Next.js: A Scalable Approach

Publish Date: Apr 29
1 0

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:

  1. A PayTabs merchant account (Sign up here)
    • Get your Profile ID, Server Key, and Region from the dashboard
  2. A Next.js 14+ project with TypeScript
  3. Node.js (v18+) installed

Install required dependencies:

npm install paytabs_pt2 axios
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

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;  
}  
Enter fullscreen mode Exit fullscreen mode

🔄 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 }  
    );  
  }  
}  
Enter fullscreen mode Exit fullscreen mode

📩 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 });  
}  
Enter fullscreen mode Exit fullscreen mode

💡 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>  
  );  
}  
Enter fullscreen mode Exit fullscreen mode

🎯 Best Practices for Production

  1. Security

    • Never expose API keys in client-side code
    • Validate webhook signatures
    • Use HTTPS everywhere
  2. Performance

    • Cache API responses with Redis
    • Deploy on Vercel's edge network
  3. Monitoring

    • Log errors with Sentry
    • Track transactions with Datadog
  4. Testing

    • Use PayTabs test cards (4111 1111 1111 1111)
    • Simulate failed payments

🔗 Resources


Have questions? Drop them in the comments below!

Comments 0 total

    Add comment