Verify Paystack Payments in Angular
Angular cannot verify Paystack payments directly because verification requires your secret key. Build a backend endpoint that calls Paystack's Verify Transaction API, then call that endpoint from Angular using HttpClient after the popup callback or on the redirect callback page. Always check both the status and amount.
Why Verification Must Happen on the Backend
When a customer finishes paying through the Paystack popup, your Angular component receives a callback with the transaction reference. This callback runs in the browser. It tells you the customer completed the payment form, but it does not prove the money actually landed.
The Paystack Verify Transaction API (GET /transaction/verify/:reference) requires your secret key in the Authorization header. That key must never appear in Angular code. Angular is a client-side framework. Everything in your Angular bundle is visible in the browser. So your Angular app calls your backend, and your backend calls Paystack.
Verification Service
Create a dedicated verification method in your PaystackService:
// 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';
export interface VerifyResult {
verified: boolean;
amount: number;
currency: string;
reference: string;
channel: string;
paidAt: string;
}
@Injectable({ providedIn: 'root' })
export class PaystackService {
private http = inject(HttpClient);
private apiUrl = environment.apiUrl;
async verify(reference: string): Promise<VerifyResult> {
return firstValueFrom(
this.http.get<VerifyResult>(this.apiUrl + '/api/verify', {
params: { reference: reference },
})
);
}
}
The service sends the reference to your backend. Your backend calls Paystack, checks the result, and returns a clean response. The Angular service never touches the Paystack API directly.
Verify After the Popup
// In your checkout component
import { Component, inject, signal } from '@angular/core';
import { PaystackService, VerifyResult } from '../services/paystack.service';
@Component({
selector: 'app-checkout',
standalone: true,
template: '
<button (click)="handlePay()" [disabled]="verifying()">
{{ verifying() ? "Verifying..." : "Pay NGN 5,000" }}
</button>
<div *ngIf="result()">
<h2>Payment Confirmed</h2>
<p>{{ result()!.currency }} {{ result()!.amount }}</p>
<p>Reference: {{ result()!.reference }}</p>
</div>
<div *ngIf="error()">
<p>{{ error() }}</p>
</div>
',
})
export class CheckoutComponent {
private paystack = inject(PaystackService);
verifying = signal(false);
result = signal<VerifyResult | null>(null);
error = signal<string | null>(null);
async handlePay() {
// After popup onSuccess:
// var reference = transaction.reference;
this.verifying.set(true);
try {
var reference = 'example_ref'; // from popup callback
var verified = await this.paystack.verify(reference);
if (verified.verified) {
this.result.set(verified);
} else {
this.error.set('Payment could not be verified');
}
} catch (err) {
this.error.set('Verification failed. Contact support.');
} finally {
this.verifying.set(false);
}
}
}
Verify on the Redirect Callback Page
For redirect checkout, create a callback component that reads the reference from the URL and verifies immediately:
// src/app/payment-callback/payment-callback.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { PaystackService, VerifyResult } from '../services/paystack.service';
@Component({
selector: 'app-payment-callback',
standalone: true,
template: '
<div *ngIf="loading()">
<p>Verifying your payment...</p>
</div>
<div *ngIf="result()">
<h1>Payment Successful</h1>
<p>Amount: {{ result()!.currency }} {{ result()!.amount }}</p>
<p>Reference: {{ result()!.reference }}</p>
<p>Paid via: {{ result()!.channel }}</p>
</div>
<div *ngIf="error()">
<h1>Verification Failed</h1>
<p>{{ error() }}</p>
<p>If you were charged, contact support.</p>
</div>
',
})
export class PaymentCallbackComponent implements OnInit {
private route = inject(ActivatedRoute);
private paystack = inject(PaystackService);
loading = signal(true);
result = signal<VerifyResult | null>(null);
error = signal<string | null>(null);
async ngOnInit() {
var params = this.route.snapshot.queryParams;
var reference = params['reference'] || params['trxref'];
if (!reference) {
this.error.set('No payment reference found in URL');
this.loading.set(false);
return;
}
try {
var verified = await this.paystack.verify(reference);
this.result.set(verified);
} catch (err) {
this.error.set('Could not verify payment');
} finally {
this.loading.set(false);
}
}
}
Register the route:
// app.routes.ts
{ path: 'payment/callback', component: PaymentCallbackComponent }
Backend Verification Endpoint
Your backend endpoint calls the Paystack Verify API and validates the result:
// server.js (Express)
app.get('/api/verify', async function(req, res) {
var reference = req.query.reference;
if (!reference) {
return res.status(400).json({ error: 'Reference required' });
}
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{ headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY } }
);
var data = await response.json();
var txn = data.data;
// Look up expected amount from database
// var order = await db.orders.findOne({ reference: reference });
// if (txn.amount !== order.amountInKobo) {
// return res.status(400).json({ verified: false, reason: 'Amount mismatch' });
// }
if (data.status && txn.status === 'success') {
return res.json({
verified: true,
amount: txn.amount / 100,
currency: txn.currency,
reference: txn.reference,
channel: txn.channel,
paidAt: txn.paid_at,
});
}
res.status(400).json({ verified: false, status: txn.status });
});
Always compare the verified amount against your database. A successful status alone does not guarantee the correct amount was paid.
Error Handling with HttpInterceptor
Use an Angular interceptor to handle common errors globally:
// src/app/interceptors/error.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
export const errorInterceptor: HttpInterceptorFn = function(req, next) {
return next(req).pipe(
catchError(function(error) {
if (error.status === 0) {
// Network error
console.error('Network error during payment verification');
}
if (error.status === 400) {
// Verification failed
console.error('Payment verification failed:', error.error);
}
return throwError(function() { return error; });
})
);
};
Register it in your app config:
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { errorInterceptor } from './interceptors/error.interceptor';
export const appConfig = {
providers: [
provideHttpClient(withInterceptors([errorInterceptor])),
],
};
Key Takeaways
- ✓Angular runs in the browser. Verification must happen on your backend because it requires the Paystack secret key.
- ✓After the popup onSuccess callback fires, call your backend verification endpoint. The popup callback alone is not reliable.
- ✓For redirect checkout, use ActivatedRoute to read the reference from query parameters and verify in ngOnInit.
- ✓Always validate the verified amount against your backend records. Check both status and amount before granting value.
- ✓Use Angular signals or BehaviorSubject for verification state to keep the UI responsive during the verification call.
- ✓Webhooks provide backup confirmation. Even if the Angular verification call fails, the webhook confirms the payment independently.
Frequently Asked Questions
- Can I verify Paystack payments directly from Angular?
- No. The Verify API requires your secret key. If you call it from Angular, the key is visible in browser DevTools. Always verify through your backend.
- What if the verification call fails but the customer was charged?
- Show a message like "We are confirming your payment. You will receive an email shortly." Your webhook handler will confirm the payment independently and fulfill the order.
- Should I use HttpClient Observables or Promises for verification?
- Either works. Promises with async/await are simpler for one-off calls like verification. Observables are better if you need cancellation, retry logic, or want to compose with other streams.
- How do I prevent double verification calls?
- Use the loading signal to disable the button during verification. You can also debounce in the service or track verified references in a Set to skip already-verified transactions.
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