Bonaventure OgetoBy Bonaventure Ogeto|

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

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