Verify Paystack Payments in ASP.NET Core
Call GET https://api.paystack.co/transaction/verify/:reference with your secret key in the Authorization header. In ASP.NET Core, use the registered "Paystack" named HttpClient. Check that data.status equals "success" and that data.amount divided by 100 matches the expected amount in your database. Never trust the amount from the frontend.
Build a PaystackService
Extract verification into a service class to keep controllers clean:
// Services/PaystackService.cs
using System.Text.Json;
public class PaystackVerifyResult
{
public bool Success { get; init; }
public string? Status { get; init; }
public long AmountInMinorUnit { get; init; }
public string? Currency { get; init; }
public string? Reference { get; init; }
public string? GatewayResponse { get; init; }
public string? CustomerEmail { get; init; }
public string? ErrorMessage { get; init; }
}
public class PaystackService
{
private readonly HttpClient _client;
public PaystackService(IHttpClientFactory factory)
{
_client = factory.CreateClient("Paystack");
}
public async Task<PaystackVerifyResult> VerifyAsync(string reference)
{
try
{
var response = await _client.GetAsync(
$"transaction/verify/{Uri.EscapeDataString(reference)}");
var body = await response.Content.ReadAsStringAsync();
var json = JsonSerializer.Deserialize<JsonElement>(body);
if (!json.GetProperty("status").GetBoolean())
{
return new PaystackVerifyResult
{
Success = false,
ErrorMessage = json.GetProperty("message").GetString()
};
}
var data = json.GetProperty("data");
var txStatus = data.GetProperty("status").GetString();
var customer = data.GetProperty("customer");
return new PaystackVerifyResult
{
Success = txStatus == "success",
Status = txStatus,
AmountInMinorUnit = data.GetProperty("amount").GetInt64(),
Currency = data.GetProperty("currency").GetString(),
Reference = data.GetProperty("reference").GetString(),
GatewayResponse = data.GetProperty("gateway_response").GetString(),
CustomerEmail = customer.GetProperty("email").GetString()
};
}
catch (Exception ex)
{
return new PaystackVerifyResult { Success = false, ErrorMessage = ex.Message };
}
}
}
Register the service in Program.cs:
builder.Services.AddScoped<PaystackService>();
Use in Controller
// Controllers/PaymentController.cs
[HttpGet("callback")]
public async Task<IActionResult> Callback(
[FromQuery] string reference,
[FromQuery] string trxref,
[FromServices] PaystackService paystack)
{
var ref_ = reference ?? trxref;
if (string.IsNullOrWhiteSpace(ref_))
return BadRequest("Reference missing");
var result = await paystack.VerifyAsync(ref_);
if (!result.Success)
{
return BadRequest(new
{
error = result.ErrorMessage ?? "Payment not verified",
gateway_response = result.GatewayResponse
});
}
// IMPORTANT: validate the amount against your database
// var order = await _db.Orders.FirstOrDefaultAsync(o => o.Reference == ref_);
// if (order == null) return NotFound();
// if (order.AmountInKobo != result.AmountInMinorUnit) return BadRequest("Amount mismatch");
return Ok(new
{
verified = true,
amount = result.AmountInMinorUnit / 100m,
currency = result.Currency,
reference = result.Reference,
customer = result.CustomerEmail
});
}
Amount Validation
Always validate the amount returned by Paystack against what you stored in your database before the checkout:
// Example with EF Core
var order = await _dbContext.Orders
.FirstOrDefaultAsync(o => o.Reference == ref_);
if (order == null)
return NotFound(new { error = "Order not found" });
if (order.IsVerified)
return Ok(new { verified = true, message = "Already verified" }); // idempotent
var result = await paystack.VerifyAsync(ref_);
if (!result.Success)
return BadRequest(new { error = "Payment failed", status = result.Status });
// Validate amount: order stored amount in NGN, Paystack returns in kobo
var expectedKobo = (long)(order.Amount * 100);
if (result.AmountInMinorUnit != expectedKobo)
{
// Log this as a potential attack
return BadRequest(new { error = "Amount mismatch" });
}
order.IsVerified = true;
order.PaidAt = DateTime.UtcNow;
await _dbContext.SaveChangesAsync();
return Ok(new { verified = true, reference = ref_ });
Learn More
This guide is part of the Paystack integration guides by language and framework series.
Key Takeaways
- ✓Always verify server-side. The redirect callback and popup success event are not proof of payment.
- ✓Check both status ("success") and amount (must match your database record) in the verify response.
- ✓Extract the verification logic into a PaystackService class so controllers stay thin.
- ✓Handle failed, abandoned, and pending statuses explicitly. Do not treat anything other than "success" as paid.
- ✓The gateway_response field in the verify response tells you exactly what the bank said about the transaction.
- ✓Use a database flag (e.g., IsVerified) to prevent double-fulfillment if the callback fires twice.
Frequently Asked Questions
- What does "Transaction not found" mean from Paystack verify?
- It means the reference you sent does not exist on Paystack, or you are using a test reference with live keys (or vice versa). Double-check that the reference matches what was returned during initialization and that you are using the same key environment.
- Can I verify a transaction more than once?
- Yes, the verify endpoint is idempotent and can be called multiple times for the same reference. Track verification in your database with an IsVerified flag to avoid processing the payment twice on your side.
- What is the difference between status and gateway_response in the verify response?
- status is the Paystack-level outcome: "success", "failed", "abandoned", or "pending". gateway_response is the human-readable message from the card network or bank, like "Approved" or "Insufficient Funds". Use status for logic and gateway_response for user-facing messages.
- How do I handle the case where a customer pays but the amount is wrong?
- Paystack initializes transactions with a fixed amount. A mismatch should not occur in normal usage. If it does, it likely indicates a bug in your initialization code or a replay attack. Log it, do not fulfill, and investigate. Never accept an amount less than expected.
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