DEV Community

Cover image for ๐Ÿ’ณ Building a Lightweight PayPal Payment Gateway Service in C#
David Au Yeung
David Au Yeung

Posted on

๐Ÿ’ณ Building a Lightweight PayPal Payment Gateway Service in C#

Introduction

Processing online payments is one of the core parts of modern web applications. Whether you're selling subscriptions, digital goods, or services, integrating a secure and reusable payment gateway should be simple and clean.

In this guide, you'll learn how to build a reusable PayPal Payment Service in C#, built for dependency injection, making it easy to plug into your ASP.NET Core apps.

We'll use the PayPal REST API, connecting to the Sandbox environment for safe testing.

Why a Service-Based Architecture?

In production apps, putting payment logic directly into controllers (or Program.cs) quickly becomes messy.

A service-based design makes it easy to:

  • Inject payment functionality anywhere
  • Reuse core logic across multiple controllers or jobs
  • Centralize configuration (e.g., PayPal credentials)
  • Unit test using mock interfaces

That's what we'll build next: a self-contained, secure IPayPalService implementation.

Prerequisites

You'll need:

  • A PayPal Developer Account https://developer.paypal.com
  • A Sandbox App with Client ID and Secret
  • .NET SDK (6.0 or later)
  • Newtonsoft.Json package

API Credentials:

Install dependencies:

dotnet add package Newtonsoft.Json
Enter fullscreen mode Exit fullscreen mode

Step 1: Create the PayPal Service Interface

This defines what your service can do, keeping it easy to stub for testing.

public interface IPayPalService
{
    Task<string> CreatePaymentAsync(decimal amount, string currency, string description);
    Task<string> ExecutePaymentAsync(string paymentId, string payerId);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Implement the PayPal Service

This class handles authentication, payment creation, and execution.

It's structured for reuse and injected HttpClient for proper connection management.

using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Text;

public class PayPalService : IPayPalService
{
    private readonly HttpClient _client;
    private readonly string _clientId;
    private readonly string _clientSecret;
    private readonly string _baseUrl;

    public PayPalService(HttpClient client, IConfiguration config)
    {
        _client = client;
        _clientId = config["PayPal:ClientId"];
        _clientSecret = config["PayPal:ClientSecret"];
        _baseUrl = config.GetValue<string>("PayPal:BaseUrl") ?? "https://api.sandbox.paypal.com";
    }

    public async Task<string> CreatePaymentAsync(decimal amount, string currency, string description)
    {
        var token = await GetAccessTokenAsync();
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        var payment = new
        {
            intent = "sale",
            payer = new { payment_method = "paypal" },
            transactions = new[]
            {
                new { amount = new { total = amount.ToString("F2"), currency }, description }
            },
            redirect_urls = new
            {
                return_url = "https://example.com/success",
                cancel_url = "https://example.com/cancel"
            }
        };

        var json = JsonConvert.SerializeObject(payment);
        var response = await _client.PostAsync($"{_baseUrl}/v1/payments/payment",
            new StringContent(json, Encoding.UTF8, "application/json"));

        return await response.Content.ReadAsStringAsync();
    }

    public async Task<string> ExecutePaymentAsync(string paymentId, string payerId)
    {
        var token = await GetAccessTokenAsync();
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        var body = new { payer_id = payerId };
        var json = JsonConvert.SerializeObject(body);

        var response = await _client.PostAsync($"{_baseUrl}/v1/payments/payment/{paymentId}/execute",
            new StringContent(json, Encoding.UTF8, "application/json"));

        return await response.Content.ReadAsStringAsync();
    }

    private async Task<string> GetAccessTokenAsync()
    {
        var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}"));
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", auth);

        var form = new StringContent("grant_type=client_credentials",
            Encoding.UTF8, "application/x-www-form-urlencoded");

        var response = await _client.PostAsync($"{_baseUrl}/v1/oauth2/token", form);
        var content = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
            throw new Exception($"Failed to get token: {response.StatusCode} - {content}");

        var token = JsonConvert.DeserializeObject<TokenResponse>(content);
        return token.access_token;
    }

    private class TokenResponse
    {
        public string access_token { get; set; }
        public int expires_in { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure the Service in Program.cs

Register the PayPalService and load its settings from configuration.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

var builder = WebApplication.CreateBuilder(args);

// Add configuration
builder.Configuration.AddJsonFile("appsettings.json");

// Register HttpClient + PayPal service
builder.Services.AddHttpClient<IPayPalService, PayPalService>();

var app = builder.Build();

app.MapGet("/create-payment", async (IPayPalService payPalService) =>
{
    var result = await payPalService.CreatePaymentAsync(25.00m, "USD", "Test Product");
    return Results.Content(result, "application/json");
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

Example appsettings.json

{
  "PayPal": {
    "ClientId": "YOUR_PAYPAL_CLIENT_ID",
    "ClientSecret": "YOUR_PAYPAL_SECRET",
    "BaseUrl": "https://api.sandbox.paypal.com"
  }
}
Enter fullscreen mode Exit fullscreen mode

(Better store the credential in environment variables)

Step 4: Try It in Sandbox

1) Run your app:

   dotnet run
Enter fullscreen mode Exit fullscreen mode

2) Visit:

   http://localhost:5000/create-payment
Enter fullscreen mode Exit fullscreen mode

3) You should see a PayPal JSON response similar to this:

{
  "id": "PAYID-NEKHSNA8G910153S62578812",
  "intent": "sale",
  "state": "created",
  "payer": {
    "payment_method": "paypal"
  },
  "transactions": [
    {
      "amount": {
        "total": "25.00",
        "currency": "USD"
      },
      "description": "Test Product",
      "related_resources": []
    }
  ],
  "create_time": "2025-11-12T12:10:28Z",
  "links": [
    {
      "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-NEKHSNA8G910153S62578812",
      "rel": "self",
      "method": "GET"
    },
    {
      "href": "https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=EC-1KS533720P2426713",
      "rel": "approval_url",
      "method": "REDIRECT"
    },
    {
      "href": "https://api.sandbox.paypal.com/v1/payments/payment/PAYID-NEKHSNA8G910153S62578812/execute",
      "rel": "execute",
      "method": "POST"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

4) From this response, take note of:

  • id โ†’ The payment ID (PAYID-...) for later execution
  • approval_url โ†’ The URL where you redirect the user to confirm the payment
  • execute link โ†’ Used to finalize (capture) the payment after the user approves it

To test the flow, redirect your user to the approval URL. Once the buyer completes the transaction in the sandbox UI, PayPal will return them to your return_url along with the paymentId and PayerID parameters.

You can then use those values to call your /execute-payment endpoint.

Step 5: (Optional) Execute and Confirm the Payment

Once PayPal redirects users back to your site with a paymentId and PayerId, execute the payment:

app.MapPost("/execute-payment", async (string paymentId, string payerId, IPayPalService payPalService) =>
{
    var result = await payPalService.ExecutePaymentAsync(paymentId, payerId);
    return Results.Content(result, "application/json");
});
Enter fullscreen mode Exit fullscreen mode

Why This Design is Good

Reusable: Inject IPayPalService anywhere: controllers, APIs, or background workers.

Clean Configuration: Credentials and base URL in appsettings.json.

Testable: Mock the interface in unit tests.

Scalable: Add more gateways (Stripe, Razorpay, etc.) behind different service implementations.

This structure follows SOLID and modern ASP.NET Core best practices.

Next Steps

  • Add webhook handling for automatic payment updates
  • Support subscriptions or invoicing
  • Use IOptions<PayPalOptions> for typed configuration
  • Switch to live mode when ready (`https://api.paypal.com)

Conclusion

By wrapping PayPal's REST API in a C# service with dependency injection, you create a flexible, maintainable payment gateway integration suitable for real-world apps.

This design simplifies future enhancements, improves testability, and allows clean extension to other providers.

Love C#!

Top comments (0)