McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Build Paystack Subscriptions in Angular
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

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.

In this article

  1. 1. How Paystack Subscriptions Work
  2. 2. SubscriptionService
  3. 3. Pricing Page Component
  4. 4. Backend Subscription Endpoints
  5. 5. Subscription Status Component
  6. 6. Webhook Handling (Backend)
  7. 7. Ship Payments Faster

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', {})
    );
  }
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

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.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series.

Sign up for the McTaba newsletter

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.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Verify Paystack Payments in Svelte and SvelteKit

Verify Paystack transactions in SvelteKit using server endpoints for popup verification and load functions for redirect callback verification. Covers amount validation, idempotent fulfillment, and SSR verification for instant results.

Read more

Handle Paystack Webhooks in Flask

Receive and verify Paystack webhooks in your Flask application. This guide covers accessing request.data for raw bytes, HMAC SHA-512 signature verification, processing charge.success events, and making handlers idempotent.

Read more

Build Paystack Subscriptions in ASP.NET Core

Implement Paystack recurring billing in ASP.NET Core. This guide covers creating plans, subscribing customers through transaction initialization, handling renewal webhooks with C#, and cancelling subscriptions.

Read more

Why You Must Never Call the Paystack API From Your Frontend

Putting your Paystack secret key in frontend JavaScript is the most dangerous mistake in payment integration. This guide shows exactly what goes wrong, why CORS will also fight you, and the correct pattern: frontend talks to your server, your server talks to Paystack.

Read more

Paystack Signature Verification Failed: The Definitive Fix

Paystack signature verification fails when the body you hash does not match the exact bytes Paystack signed. The most common cause is your framework parsing the JSON body before you compute the HMAC. This guide covers every cause and gives you working fixes for Express, Django, Laravel, and Next.js.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.