Build Paystack Subscriptions in Angular
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.
How Paystack Subscriptions Work
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:
- Angular: Displays available plans, opens checkout, shows subscription status.
- Backend: Creates plans, initializes transactions, receives webhooks, manages subscription state.
- Paystack: Handles recurring billing, card storage, and retry logic.
SubscriptionService
// 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', {})
);
}
}
Pricing Page Component
// 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);
}
}
}
Backend Subscription Endpoints
// 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);
});
Subscription Status Component
// 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.');
}
}
}
Webhook Handling (Backend)
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.
Key Takeaways
- ✓Subscription management lives on the backend. Angular displays plans, collects the initial payment, and shows subscription status.
- ✓Create plans on your backend using the Paystack Plans API. Fetch and display them in Angular via HttpClient.
- ✓Initialize subscription checkout by passing the plan code when initializing a transaction. Paystack bills automatically after the first charge.
- ✓Subscription webhooks (subscription.create, invoice.payment_failed, subscription.disable) must be handled on your backend.
- ✓Create an Angular SubscriptionService to centralize all subscription-related API calls.
- ✓Let customers cancel via the Paystack manage link or through your own Angular UI backed by the backend Disable Subscription API.
Frequently Asked Questions
- Can I create Paystack plans from Angular?
- No. The Plans API requires your secret key. Create plans from your backend or from the Paystack dashboard. Your Angular app fetches available plans from your backend.
- Does Paystack charge the customer automatically after the first payment?
- Yes. When you initialize a transaction with a plan code, Paystack creates a subscription after the first successful charge and handles all future billing automatically.
- How do I know if a recurring charge failed?
- Paystack sends an invoice.payment_failed webhook to your backend. Your backend updates the database, and your Angular app sees the updated status when it queries the subscription endpoint.
- Can I use NgRx for subscription state management?
- You can, but a simple service with signals is usually sufficient. Subscription state is straightforward: active, past_due, cancelled. NgRx adds complexity that is rarely justified for this use case.
- What happens when a subscription is cancelled?
- The customer keeps access until the end of the current billing period. Paystack stops future charges. Your backend receives subscription.not_renew when cancelled and subscription.disable when the period ends.
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