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
This guide is part of the Paystack integration guides by language and framework series.
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