DEV Community

Cover image for PayPal integration in ASP.NET Core: a safe checkout flow with Orders v2
GANESH KUMAR
GANESH KUMAR

Posted on

PayPal integration in ASP.NET Core: a safe checkout flow with Orders v2

Adding a PayPal button takes an afternoon. Making sure you never lose money, never ship unpaid orders and never charge for an order you didn't save takes more care.

This is the PayPal checkout flow I use in eCommerceMVCCore, my ASP.NET Core 10 e-commerce platform, with simplified C# code for each step. It works for PayPal accounts and for debit and credit cards through PayPal.

The safe PayPal flow: recalculate, create, capture, save, and refund automatically if saving fails

1. Never trust the browser's total

The browser can send any price it likes. Before creating a PayPal order, rebuild the cart on the server from product IDs and quantities, and read prices, discounts and promo codes from your database.

var lines = await cartService.GetLinesAsync(userId);          // product IDs + quantities
var total = await pricingService.CalculateTotalAsync(lines);  // prices from the database
var paypalOrder = await payPal.CreateOrderAsync(total, currency);
return Ok(new { id = paypalOrder.Id });
Enter fullscreen mode Exit fullscreen mode

2. Capture first

After the shopper approves the payment, capture it on the server with PayPal's Orders API (POST /v2/checkout/orders/{id}/capture). Only a successful capture means you were paid.

var capture = await payPal.CaptureOrderAsync(orderId);
if (capture.Status != "COMPLETED")
    return BadRequest("Payment was not completed.");
Enter fullscreen mode Exit fullscreen mode

3. Save the order only after a successful capture

If you save the order before capturing, a failed payment leaves an unpaid order in your dashboard. So save it after the capture:

try
{
    await orderService.CreatePaidOrderAsync(userId, lines, capture.CaptureId);
}
catch (Exception ex)
{
    logger.LogError(ex, "Order save failed after capture {CaptureId}", capture.CaptureId);
    await payPal.RefundCaptureAsync(capture.CaptureId); // POST /v2/payments/captures/{id}/refund
    return Problem("We couldn't save your order, so your payment has been refunded.");
}
Enter fullscreen mode Exit fullscreen mode

4. Refund automatically if saving fails

That catch block is the important part. If the database is down or stock ran out at the last second, the shopper gets their money back automatically instead of waiting on a support email.

See the full flow: the ASP.NET Core PayPal checkout in eCommerceMVCCore includes one-click refunds from the admin dashboard and a Sandbox / Live switch.

5. Keep statuses in sync with verified webhooks

Refunds and disputes can also happen in the PayPal dashboard, outside your app. Subscribe to PayPal webhooks, and verify every webhook's signature with PayPal (POST /v1/notifications/verify-webhook-signature) before you change an order's status. Without verification, anyone can post fake "payment completed" events to your endpoint.

6. Test in Sandbox

Keep a Sandbox / Live switch in your settings, with separate credentials for each. Test the whole flow, including the refund path, in Sandbox. A quick way to test the refund is to make the order save throw on purpose.

Summary

  1. Recalculate the total on the server.
  2. Capture the payment.
  3. Save the order after the capture.
  4. Refund automatically if the save fails.
  5. Verify webhooks before trusting them.

FAQ

Should I use the PayPal JavaScript SDK or a server-side integration?

Both. The JavaScript SDK renders the buttons and handles the buyer's approval. Creating and capturing the order happens on your server through the Orders v2 API, so the server, not the browser, decides the amount.

Can customers pay by card without a PayPal account?

Yes. PayPal's debit and credit card option lets guests pay by card without signing up. On the server, you create and capture the order exactly as you do for a PayPal account payment.

How do I test PayPal payments in ASP.NET Core?

Create a Sandbox app in the PayPal developer dashboard, use its credentials and Sandbox buyer accounts, and test the refund path as well as the happy path. eCommerceMVCCore has a Sandbox / Live switch in its settings for exactly this.

Get the source code

This flow, plus one-click refunds from the admin dashboard, is built into eCommerceMVCCore. Get the ASP.NET Core 10 e-commerce source code with PayPal checkout.

How do you handle the "paid but not saved" case in your apps? Let me know in the comments.

Top comments (0)