Accept Payments with Paystack in Angular
To accept Paystack payments in Angular, create a backend API that initializes transactions with your Paystack secret key. Your Angular app calls that API through an HttpClient service, gets an access code, and opens the Paystack inline popup. After payment, call your backend to verify the transaction before granting value.
Architecture Overview
Angular is a frontend framework. Like React and Vue, it runs in the browser. Paystack requires a secret key for initialization and verification, so you need a backend server. The flow:
- Angular component collects the customer email and knows the amount.
- Angular calls your backend API with the email and amount via HttpClient.
- Your backend calls Paystack's Initialize Transaction endpoint and returns the access code.
- Angular opens the Paystack inline popup with the access code.
- After payment, Angular calls your backend to verify the transaction.
This guide uses Angular 17+ with standalone components. The backend examples use Express, but any server-side framework works. If your backend is in a different language, the Angular code stays the same.
Load Paystack Inline.js
Add the Paystack script to your index.html:
<!-- src/index.html -->
<script src="https://js.paystack.co/v2/inline.js" defer></script>
Then declare the global type so TypeScript does not complain:
// src/types/paystack.d.ts
declare class PaystackPop {
checkout(options: {
accessCode: string;
onSuccess: (transaction: { reference: string }) => void;
onCancel: () => void;
}): void;
}
interface Window {
PaystackPop: typeof PaystackPop;
}
Alternatively, load the script dynamically in your service to avoid loading it on pages that do not need payments.
PaystackService
Create a service that handles initialization, popup, and verification:
// src/app/services/paystack.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
interface InitResponse {
access_code: string;
authorization_url: string;
reference: string;
}
interface VerifyResponse {
verified: boolean;
amount: number;
currency: string;
reference: string;
}
@Injectable({ providedIn: 'root' })
export class PaystackService {
private http = inject(HttpClient);
private apiUrl = environment.apiUrl;
async initializeTransaction(email: string, amount: number): Promise<InitResponse> {
return firstValueFrom(
this.http.post<InitResponse>(this.apiUrl + '/api/pay', { email, amount })
);
}
openPopup(accessCode: string): Promise<string> {
return new Promise(function(resolve, reject) {
var popup = new window.PaystackPop();
popup.checkout({
accessCode: accessCode,
onSuccess: function(transaction) {
resolve(transaction.reference);
},
onCancel: function() {
reject(new Error('Payment cancelled'));
},
});
});
}
async verifyTransaction(reference: string): Promise<VerifyResponse> {
return firstValueFrom(
this.http.get<VerifyResponse>(this.apiUrl + '/api/verify', {
params: { reference: reference },
})
);
}
async pay(email: string, amount: number): Promise<VerifyResponse> {
var init = await this.initializeTransaction(email, amount);
var reference = await this.openPopup(init.access_code);
return this.verifyTransaction(reference);
}
}
The pay() method chains the entire flow: initialize, popup, verify. Components only need to call one method.
Checkout Component
// src/app/checkout/checkout.component.ts
import { Component, inject, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { PaystackService } from '../services/paystack.service';
@Component({
selector: 'app-checkout',
standalone: true,
imports: [FormsModule],
template: '
<div>
<h1>Checkout</h1>
<input [(ngModel)]="email" type="email" placeholder="Email address" />
<p>Amount: NGN {{ amount | number }}</p>
<button (click)="handlePay()" [disabled]="loading() || !email">
{{ loading() ? "Processing..." : "Pay Now" }}
</button>
<div *ngIf="result()">
<h2>Payment Confirmed</h2>
<p>{{ result()!.currency }} {{ result()!.amount }}</p>
</div>
<div *ngIf="error()">
<p>{{ error() }}</p>
</div>
</div>
',
})
export class CheckoutComponent {
private paystack = inject(PaystackService);
email = '';
amount = 5000;
loading = signal(false);
result = signal<{ amount: number; currency: string } | null>(null);
error = signal<string | null>(null);
async handlePay() {
this.loading.set(true);
this.error.set(null);
try {
var verified = await this.paystack.pay(this.email, this.amount);
this.result.set(verified);
} catch (err: any) {
this.error.set(err.message || 'Payment failed');
} finally {
this.loading.set(false);
}
}
}
The component uses Angular signals for reactive state. The loading signal disables the button during processing. The result signal shows the confirmation once verified.
Backend Setup
Create the Express endpoints that Angular calls:
// server.js
var express = require('express');
var cors = require('cors');
var app = express();
app.use(cors({ origin: 'http://localhost:4200' }));
app.use(express.json());
var PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;
app.post('/api/pay', async function(req, res) {
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: req.body.email,
amount: Math.round(req.body.amount * 100),
reference: 'ng_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8),
}),
});
var data = await response.json();
if (!data.status) return res.status(400).json({ error: data.message });
res.json({
access_code: data.data.access_code,
authorization_url: data.data.authorization_url,
reference: data.data.reference,
});
});
app.get('/api/verify', async function(req, res) {
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(req.query.reference),
{ headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET } }
);
var data = await response.json();
if (data.status && data.data.status === 'success') {
return res.json({
verified: true,
amount: data.data.amount / 100,
currency: data.data.currency,
reference: data.data.reference,
});
}
res.status(400).json({ verified: false });
});
app.listen(4000);
Redirect Checkout
For redirect checkout where the customer leaves your Angular app:
async handleRedirectPay() {
var init = await this.paystack.initializeTransaction(this.email, this.amount);
if (init.authorization_url) {
window.location.href = init.authorization_url;
}
}
Create a callback route in Angular Router and verify on initialization:
// src/app/payment-callback/payment-callback.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PaystackService } from '../services/paystack.service';
@Component({
selector: 'app-payment-callback',
standalone: true,
template: '
<div *ngIf="loading()">Verifying payment...</div>
<div *ngIf="result()">
<h1>Payment Confirmed</h1>
<p>{{ result()!.currency }} {{ result()!.amount }}</p>
</div>
<div *ngIf="error()">
<p>Verification failed. Contact support.</p>
</div>
',
})
export class PaymentCallbackComponent implements OnInit {
private route = inject(ActivatedRoute);
private paystack = inject(PaystackService);
loading = signal(true);
result = signal<any>(null);
error = signal<string | null>(null);
async ngOnInit() {
var reference = this.route.snapshot.queryParams['reference']
|| this.route.snapshot.queryParams['trxref'];
if (!reference) {
this.error.set('No reference found');
this.loading.set(false);
return;
}
try {
var verified = await this.paystack.verifyTransaction(reference);
this.result.set(verified);
} catch (err) {
this.error.set('Verification failed');
} finally {
this.loading.set(false);
}
}
}
Production Checklist
- Environment files. Use Angular's
environment.tsfor API URLs. Point to your production backend. - CORS. Restrict backend CORS to your Angular app's production domain.
- Amount validation. Validate amounts on the backend from your database. Do not trust frontend values.
- Webhooks. Set up webhook endpoints on your backend for reliable payment confirmation.
- Error handling. Use Angular's HttpInterceptor to handle 401/403 errors globally.
- HTTPS. Both your Angular app and backend must use HTTPS in production.
Key Takeaways
- ✓Angular runs in the browser. Your Paystack secret key must live on a separate backend server. Never import it in an Angular component or service.
- ✓Create a PaystackService that handles initialization, popup, and verification. Inject it wherever you need payments.
- ✓Load Paystack Inline.js by adding the script tag to index.html or loading it dynamically in a service.
- ✓After the popup onSuccess callback fires, verify the transaction on your backend. The callback alone is not proof of payment.
- ✓Amounts must be in the smallest currency unit (kobo for NGN, pesewas for GHS, cents for ZAR/KES/USD).
- ✓Use Angular signals or BehaviorSubject for loading states to prevent double submissions.
Frequently Asked Questions
- Do I need an Angular Paystack npm package?
- No. Community packages exist but are often outdated. Loading the official Paystack Inline.js from the CDN and creating your own service gives you full control and fewer dependencies.
- Can I use Angular Universal (SSR) with Paystack?
- Yes, but the Paystack popup only runs in the browser. Use isPlatformBrowser to guard Paystack-related code in your service. The initialization and verification calls work server-side, but the popup must be client-side.
- Should I use Observables or Promises for Paystack calls?
- Either works. The examples use firstValueFrom to convert HttpClient Observables to Promises for cleaner async/await code. If you prefer Observables, use the HttpClient methods directly and handle the subscription lifecycle.
- How do I handle multiple products with different amounts?
- Pass the amount from your component to the PaystackService. Your backend should look up the correct price from the database rather than trusting the amount sent from Angular. The frontend amount is for display purposes only.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon