Bonaventure OgetoBy Bonaventure Ogeto|

Build Paystack Subscriptions in ASP.NET Core

To build Paystack subscriptions in ASP.NET Core, create a plan once via the Paystack API, then initialize a transaction with the plan code to subscribe a customer. Paystack handles renewals automatically. Handle subscription.create, charge.success (with plan), and invoice.payment_failed webhooks in your WebhookController to manage subscription state in your database.

Create a Plan via API

// Controllers/SubscriptionController.cs
[ApiController]
[Route("api/[controller]")]
public class SubscriptionController : ControllerBase
{
    private readonly HttpClient _paystackClient;

    public SubscriptionController(IHttpClientFactory factory)
    {
        _paystackClient = factory.CreateClient("Paystack");
    }

    [HttpPost("plans")]
    public async Task<IActionResult> CreatePlan([FromBody] CreatePlanRequest req)
    {
        var payload = new
        {
            name = req.Name,
            amount = req.AmountInKobo,
            interval = req.Interval,  // "monthly", "weekly", "annually", "daily"
            currency = req.Currency ?? "NGN"
        };

        var response = await _paystackClient.PostAsJsonAsync("plan", payload);
        var result = await response.Content.ReadFromJsonAsync<JsonElement>();

        if (!result.GetProperty("status").GetBoolean())
            return BadRequest(result.GetProperty("message").GetString());

        var data = result.GetProperty("data");
        return Ok(new
        {
            plan_code = data.GetProperty("plan_code").GetString(),
            name = data.GetProperty("name").GetString(),
            amount = data.GetProperty("amount").GetInt64()
        });
    }
}

public record CreatePlanRequest(string Name, long AmountInKobo, string Interval, string? Currency = null);

Subscribe a Customer

[HttpPost("subscribe")]
public async Task<IActionResult> Subscribe([FromBody] SubscribeRequest req)
{
    // Your plan code stored in config
    var planCode = "PLN_xxxxxxxxxxxx";

    var reference = $"sub_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}_{Guid.NewGuid():N}"[..30];

    var payload = new
    {
        email = req.Email,
        plan = planCode,
        reference = reference,
        callback_url = "https://yourapp.com/subscription/callback"
    };

    var response = await _paystackClient.PostAsJsonAsync("transaction/initialize", payload);
    var result = await response.Content.ReadFromJsonAsync<JsonElement>();

    if (!result.GetProperty("status").GetBoolean())
        return BadRequest(result.GetProperty("message").GetString());

    var data = result.GetProperty("data");
    return Ok(new
    {
        authorization_url = data.GetProperty("authorization_url").GetString(),
        access_code = data.GetProperty("access_code").GetString(),
        reference = data.GetProperty("reference").GetString()
    });
}

public record SubscribeRequest(string Email);

Subscription Webhook Events

// In WebhookController.cs ProcessEventAsync
case "subscription.create":
{
    var subCode = data.GetProperty("subscription_code").GetString();
    var emailToken = data.GetProperty("email_token").GetString();
    var customerEmail = data.GetProperty("customer").GetProperty("email").GetString();

    _logger.LogInformation("New subscription: {SubCode} for {Email}", subCode, customerEmail);
    // Activate the customer's plan in database
    // Store subCode and emailToken for future cancellation
    break;
}
case "charge.success":
{
    // Check if this is a subscription renewal
    if (data.TryGetProperty("plan", out var plan) &&
        plan.ValueKind != JsonValueKind.Null)
    {
        var subRef = data.GetProperty("reference").GetString();
        var amount = data.GetProperty("amount").GetInt64();
        _logger.LogInformation("Subscription renewal: {Ref}, {Amount}", subRef, amount / 100m);
        // Extend subscription period in database
    }
    break;
}
case "invoice.payment_failed":
{
    var subInfo = data.GetProperty("subscription");
    var subCode = subInfo.GetProperty("subscription_code").GetString();
    _logger.LogWarning("Renewal failed for subscription: {SubCode}", subCode);
    // Pause access, send payment failure email
    break;
}
case "subscription.disable":
{
    var subCode = data.GetProperty("subscription_code").GetString();
    _logger.LogInformation("Subscription cancelled: {SubCode}", subCode);
    // Deactivate plan, do not delete — keep for audit
    break;
}

Cancel a Subscription

[HttpPost("cancel")]
public async Task<IActionResult> Cancel([FromBody] CancelRequest req)
{
    var payload = new { code = req.SubscriptionCode, token = req.EmailToken };
    var response = await _paystackClient.PostAsJsonAsync("subscription/disable", payload);
    var result = await response.Content.ReadFromJsonAsync<JsonElement>();

    if (!result.GetProperty("status").GetBoolean())
        return BadRequest(result.GetProperty("message").GetString());

    return Ok(new { cancelled = true });
}

public record CancelRequest(string SubscriptionCode, string EmailToken);

Learn More

Key Takeaways

  • Create a plan once and store its plan_code. Use the plan_code when initializing subscription transactions.
  • Including a plan code in transaction/initialize causes Paystack to create a subscription after payment and store the card.
  • Renewals are automatic. You receive charge.success webhooks with a plan property on each successful renewal.
  • Store subscription_code and email_token from the subscription.create webhook. You need both to cancel.
  • Handle invoice.payment_failed to revoke or downgrade customer access when a renewal fails.
  • Use CancellationToken in async actions to handle client disconnects gracefully.

Frequently Asked Questions

Does Paystack have a .NET SDK?
Paystack does not maintain an official .NET SDK. Most .NET developers call the REST API directly using IHttpClientFactory and System.Net.Http.Json, which is what this guide covers. Third-party community libraries exist on NuGet but verify their maintenance status before using.
How do I handle failed subscription renewals in ASP.NET Core?
Listen for invoice.payment_failed in your webhook controller. When received, look up the subscription by subscription_code in your database and downgrade or pause the customer access. Send them an email with a link to update their payment method. Paystack may retry automatically.
Can I use SignalR to notify users when their subscription payment succeeds?
Yes. After processing a charge.success webhook in your webhook controller, push a real-time notification to the customer using SignalR IHubContext. Inject it into your webhook controller and call Clients.User(userId).SendAsync() after confirming the payment.
How should I store subscription data in ASP.NET Core with EF Core?
Create a Subscription entity with fields for subscription_code, email_token, status, plan_code, customer_email, start_date, next_payment_date, and cancelled_at. Update these fields from webhook events. The email_token field is needed for cancellation.

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