Accept Payments with Paystack in ASP.NET Core
To accept Paystack payments in ASP.NET Core, inject IHttpClientFactory into a PaymentController. Create a POST action that calls the Paystack transaction/initialize endpoint with your secret key from IConfiguration. Return the authorization URL to the frontend. In the callback GET action, verify the transaction with a second API call before granting access.
Setup: Configuration and HttpClient
Add your Paystack credentials to appsettings.json:
{
"Paystack": {
"SecretKey": "sk_test_xxxxxxxxxxxx",
"PublicKey": "pk_test_xxxxxxxxxxxx",
"BaseUrl": "https://api.paystack.co"
}
}
Register a named HttpClient in Program.cs:
// Program.cs
builder.Services.AddHttpClient("Paystack", (provider, client) =>
{
var config = provider.GetRequiredService<IConfiguration>();
var secretKey = config["Paystack:SecretKey"];
client.BaseAddress = new Uri("https://api.paystack.co/");
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", secretKey);
client.DefaultRequestHeaders.Accept.Add(
new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
});
Payment Controller
// Controllers/PaymentController.cs
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class PaymentController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
public PaymentController(IHttpClientFactory httpClientFactory, IConfiguration config)
{
_httpClientFactory = httpClientFactory;
_config = config;
}
[HttpPost("initialize")]
public async Task<IActionResult> Initialize([FromBody] InitializeRequest req)
{
if (string.IsNullOrWhiteSpace(req.Email))
return BadRequest(new { error = "Email is required" });
var reference = "dotnet_" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() +
"_" + Guid.NewGuid().ToString("N")[..6];
var payload = new
{
email = req.Email,
amount = (long)(req.Amount * 100), // Convert to kobo
reference = reference,
callback_url = _config["Paystack:CallbackUrl"]
};
var client = _httpClientFactory.CreateClient("Paystack");
var json = JsonSerializer.Serialize(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("transaction/initialize", content);
var responseBody = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<JsonElement>(responseBody);
if (!result.GetProperty("status").GetBoolean())
{
return BadRequest(new { error = 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 InitializeRequest(string Email, decimal Amount);
Callback Handler
// Controllers/PaymentController.cs (continued)
[HttpGet("callback")]
public async Task<IActionResult> Callback([FromQuery] string reference, [FromQuery] string trxref)
{
var ref_ = reference ?? trxref;
if (string.IsNullOrWhiteSpace(ref_))
return BadRequest("Reference missing");
var client = _httpClientFactory.CreateClient("Paystack");
var response = await client.GetAsync($"transaction/verify/{Uri.EscapeDataString(ref_)}");
var responseBody = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<JsonElement>(responseBody);
if (!result.GetProperty("status").GetBoolean())
return BadRequest("Verification failed");
var data = result.GetProperty("data");
var txStatus = data.GetProperty("status").GetString();
if (txStatus != "success")
return BadRequest(new { status = txStatus });
var amount = data.GetProperty("amount").GetInt64();
var currency = data.GetProperty("currency").GetString();
// Grant access, fulfill order, update database
return Ok(new { verified = true, amount = amount / 100m, currency, reference = ref_ });
}
Minimal API Alternative
If you prefer Minimal APIs in .NET 6+:
// Program.cs additions
app.MapPost("/api/pay", async (InitializeRequest req, IHttpClientFactory factory, IConfiguration config) =>
{
var reference = "dotnet_" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var payload = new { email = req.Email, amount = (long)(req.Amount * 100), reference };
var client = factory.CreateClient("Paystack");
var response = await client.PostAsJsonAsync("transaction/initialize", payload);
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
if (!result.GetProperty("status").GetBoolean())
return Results.BadRequest(result.GetProperty("message").GetString());
var data = result.GetProperty("data");
return Results.Ok(new
{
authorization_url = data.GetProperty("authorization_url").GetString(),
reference = data.GetProperty("reference").GetString()
});
});
app.MapGet("/api/pay/callback", async (string reference, IHttpClientFactory factory) =>
{
var client = factory.CreateClient("Paystack");
var result = await client.GetFromJsonAsync<JsonElement>($"transaction/verify/{reference}");
if (result.GetProperty("data").GetProperty("status").GetString() != "success")
return Results.BadRequest("Payment not verified");
return Results.Ok(new { verified = true, reference });
});
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Store your Paystack secret key in appsettings.json under a Paystack section, and load it via IConfiguration.
- ✓Register a named HttpClient for Paystack in Program.cs with the base address and Authorization header set once.
- ✓Amounts are in kobo (NGN), pesewas (GHS), or cents (ZAR/KES/USD). Multiply display price by 100.
- ✓Always verify the transaction server-side in the callback action. The redirect alone is not proof of payment.
- ✓Use IHttpClientFactory rather than new HttpClient() to avoid socket exhaustion in production.
- ✓Anti-forgery tokens apply to form POST actions but not to the Paystack callback GET redirect.
Frequently Asked Questions
- Should I use IHttpClientFactory or new HttpClient() for Paystack in ASP.NET Core?
- Always use IHttpClientFactory. Creating HttpClient with new HttpClient() in controllers can cause socket exhaustion because sockets are not released immediately on disposal. IHttpClientFactory manages a pool of handlers and recycles connections correctly.
- Where should I store the Paystack secret key in ASP.NET Core?
- In development, use appsettings.Development.json or user secrets (dotnet user-secrets). In production, use environment variables, Azure Key Vault, or your hosting platform secret management. Never commit the key to source control.
- How do I convert Nigerian Naira to kobo in C#?
- Multiply the amount in Naira by 100. Use decimal for the conversion to avoid floating point issues: (long)(amount * 100). Paystack expects an integer in the smallest currency unit (kobo for NGN, pesewas for GHS, cents for others).
- Can I use Razor Pages instead of Controllers for Paystack?
- Yes. Inject IHttpClientFactory in your PageModel constructor and call the Paystack API in OnPostAsync. The verification logic is the same. The main difference is you handle the callback URL in a separate GET handler or use a dedicated page.
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