To build Paystack subscriptions in Angular, create plans on your backend using the Paystack Plans API. Your Angular app displays available plans and initializes a transaction with the plan code through your backend. Paystack handles recurring billing after the first charge. Your backend listens for subscription webhooks and updates the database. Your Angular app displays subscription status by querying your backend.
Paystack subscriptions are built on plans. A plan defines the amount, billing interval (daily, weekly, monthly, quarterly, biannually, annually), and currency. When a customer pays through a transaction initialized with a plan code, Paystack creates a subscription and charges them automatically on the next billing date.
In an Angular application, the responsibilities split cleanly:
// src/app/services/subscription.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 Plan {
plan_code: string;
name: string;
amount: number;
interval: string;
}
export interface Subscription {
planName: string;
status: string;
nextPaymentDate: string;
manageUrl: string;
}
@Injectable({ providedIn: 'root' })
export class SubscriptionService {
private http = inject(HttpClient);
private apiUrl = environment.apiUrl;
async getPlans(): Promise<Plan[]> {
var result = await firstValueFrom(
this.http.get<{ plans: Plan[] }>(this.apiUrl + '/api/plans')
);
return result.plans;
}
async subscribe(email: string, planCode: string): Promise<{ access_code: string }> {
return firstValueFrom(
this.http.post<{ access_code: string }>(this.apiUrl + '/api/subscribe', {
email: email,
plan_code: planCode,
})
);
}
async getSubscription(): Promise<Subscription | null> {
try {
var result = await firstValueFrom(
this.http.get<{ subscription: Subscription }>(this.apiUrl + '/api/subscription')
);
return result.subscription;
} catch (err) {
return null;
}
}
async cancel(): Promise<void> {
await firstValueFrom(
this.http.post(this.apiUrl + '/api/subscription/cancel', {})
);
}
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// src/app/pricing/pricing.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { SubscriptionService, Plan } from '../services/subscription.service';
@Component({
selector: 'app-pricing',
standalone: true,
imports: [CommonModule, FormsModule],
template: '
<h1>Choose a Plan</h1>
<input [(ngModel)]="email" type="email" placeholder="Email address" />
<div *ngFor="let plan of plans()">
<h3>{{ plan.name }}</h3>
<p>NGN {{ plan.amount / 100 | number }} / {{ plan.interval }}</p>
<button (click)="handleSubscribe(plan)" [disabled]="loading() || !email">
{{ loading() ? "Processing..." : "Subscribe" }}
</button>
</div>
',
})
export class PricingComponent implements OnInit {
private subscriptionService = inject(SubscriptionService);
email = '';
plans = signal<Plan[]>([]);
loading = signal(false);
async ngOnInit() {
var fetchedPlans = await this.subscriptionService.getPlans();
this.plans.set(fetchedPlans);
}
async handleSubscribe(plan: Plan) {
if (!this.email) return;
this.loading.set(true);
try {
var data = await this.subscriptionService.subscribe(this.email, plan.plan_code);
var popup = new window.PaystackPop();
popup.checkout({
accessCode: data.access_code,
onSuccess: function(transaction) {
// Redirect to dashboard or verify
window.location.href = '/dashboard';
},
onCancel: function() {
// User cancelled
},
});
} catch (err) {
alert('Failed to start subscription checkout');
} finally {
this.loading.set(false);
}
}
}
// server.js (Express)
var PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;
// Initialize subscription transaction
app.post('/api/subscribe', 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,
plan: req.body.plan_code,
}),
});
var data = await response.json();
if (!data.status) return res.status(400).json({ error: data.message });
res.json({ access_code: data.data.access_code, reference: data.data.reference });
});
// Get user subscription status
app.get('/api/subscription', async function(req, res) {
// Look up from database based on authenticated user
// var sub = await db.subscriptions.findOne({ userId: req.user.id, status: 'active' });
res.json({
subscription: {
planName: 'Pro',
status: 'active',
nextPaymentDate: '2026-08-22',
manageUrl: 'https://paystack.com/manage/subscription/xxx',
},
});
});
// Cancel subscription
app.post('/api/subscription/cancel', async function(req, res) {
// Get subscription from database
// var sub = await db.subscriptions.findOne({ userId: req.user.id });
var response = await fetch('https://api.paystack.co/subscription/disable', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: 'SUB_xxxx', // sub.subscription_code
token: 'xxxx', // sub.email_token from webhook
}),
});
var data = await response.json();
res.json(data);
});
// src/app/dashboard/dashboard.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SubscriptionService, Subscription } from '../services/subscription.service';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, RouterLink],
template: '
<div *ngIf="loading()">Loading subscription...</div>
<div *ngIf="subscription()">
<h2>Your Subscription</h2>
<p>Plan: {{ subscription()!.planName }}</p>
<p>Status: {{ subscription()!.status }}</p>
<p>Next billing: {{ subscription()!.nextPaymentDate }}</p>
<a [href]="subscription()!.manageUrl" target="_blank">Manage Subscription</a>
<button (click)="handleCancel()" *ngIf="subscription()!.status === 'active'">
Cancel Subscription
</button>
</div>
<div *ngIf="!loading() && !subscription()">
<p>No active subscription.</p>
<a routerLink="/pricing">View Plans</a>
</div>
',
})
export class DashboardComponent implements OnInit {
private subscriptionService = inject(SubscriptionService);
subscription = signal<Subscription | null>(null);
loading = signal(true);
async ngOnInit() {
var sub = await this.subscriptionService.getSubscription();
this.subscription.set(sub);
this.loading.set(false);
}
async handleCancel() {
if (!confirm('Are you sure you want to cancel?')) return;
try {
await this.subscriptionService.cancel();
var current = this.subscription();
if (current) {
this.subscription.set({ ...current, status: 'cancelled' });
}
} catch (err) {
alert('Failed to cancel. Please try again.');
}
}
}
Subscription lifecycle events come as webhooks to your backend. Angular cannot receive these. Your backend must handle:
subscription.create - Save the subscription code and email token.charge.success - Record successful recurring charge.invoice.payment_failed - Mark subscription as past due. Notify customer.subscription.not_renew - Customer cancelled. Keep access until period ends.subscription.disable - Subscription deactivated. Revoke access.See Handle Paystack Webhooks in Angular for the architectural explanation and links to backend webhook guides.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course