ASP.NET Core implementation of a donation form supporting both one-time and recurring payments using the Global Payments Portico gateway. Uses GlobalPayments Hosted Fields for PCI SAQ-A compliant tokenization — card data never touches your server.
- .NET 9.0+
- Global Payments Portico account with API credentials
dotnet/
├── Program.cs # ASP.NET Core minimal API — all endpoints and payment logic
├── wwwroot/
│ └── index.html # Donation form frontend (served as static file)
├── appsettings.json # ASP.NET Core app settings
├── dotnet.csproj # GlobalPayments.Api + dotenv.net
├── .env.sample
├── Dockerfile
├── run.sh
├── .devcontainer/
└── .codesandbox/
1. Restore dependencies
dotnet restore2. Configure credentials
cp .env.sample .envEdit .env:
PUBLIC_API_KEY=pkapi_cert_jKc1FtuyAydZhZfbB3
SECRET_API_KEY=skapi_cert_MTyMAQBiHVEAewvIzXVFcmUd2UcyBge_eCpaASUp0A3. Start the server
dotnet run
# Open http://localhost:5000Or use the convenience script:
./run.sh| Variable | Description | Required | Example |
|---|---|---|---|
PUBLIC_API_KEY |
Public key for GlobalPayments Hosted Fields (browser) | ✅ | pkapi_cert_jKc1FtuyAydZhZfbB3 |
SECRET_API_KEY |
Secret key for server-side Portico API calls | ✅ | skapi_cert_MTyMAQBiHVEA... |
Configured once at startup in Program.cs:
using GlobalPayments.Api;
using GlobalPayments.Api.ServiceConfigs.Gateways;
using dotenv.net;
DotEnv.Load();
private static void ConfigureGlobalPaymentsSDK()
{
var config = new PorticoConfig
{
SecretApiKey = GetEnvVar("SECRET_API_KEY"),
DeveloperId = "000000",
VersionNumber = "0000",
ServiceUrl = "https://cert.api2.heartlandportico.com"
};
ServicesContainer.Configure(config);
}Returns the public API key for GlobalPayments Hosted Fields initialization.
Response:
{
"success": true,
"data": {
"publicApiKey": "pkapi_cert_jKc1FtuyAydZhZfbB3"
}
}Routes to one-time or recurring logic based on payment_type.
One-time request:
{
"payment_type": "one-time",
"payment_reference": "supt_xxxxxxxxxxxxxx",
"amount": "50.00",
"first_name": "Jane",
"last_name": "Doe",
"donor_email": "jane@example.com",
"billing_zip": "12345"
}Recurring request:
{
"payment_type": "recurring",
"payment_reference": "supt_xxxxxxxxxxxxxx",
"amount": "25.00",
"first_name": "Jane",
"last_name": "Doe",
"donor_email": "jane@example.com",
"billing_zip": "12345",
"phone": "555-555-5555",
"street_address": "123 Main St",
"city": "Anytown",
"state": "GA",
"country": "US",
"frequency": "monthly",
"duration_type": "ongoing",
"start_date": "2025-05-01"
}duration_type options:
| Value | Additional Field | Description |
|---|---|---|
"ongoing" |
— | Runs indefinitely |
"end_date" |
end_date (YYYY-MM-DD) |
Stops on a specific date |
"num_payments" |
num_payments (integer) |
Stops after N payments |
frequency options: "monthly", "quarterly", "annually"
One-time success response:
{
"success": true,
"message": "Thank you for your donation!",
"data": {
"transactionId": "1234567890",
"amount": 50.00,
"currency": "USD"
}
}Recurring success response:
{
"success": true,
"message": "Recurring donation created successfully!",
"data": {
"scheduleKey": "schedule_xxxxx",
"customerKey": "customer_xxxxx",
"paymentMethodKey": "pm_xxxxx",
"amount": 25.00,
"currency": "USD",
"frequency": "monthly",
"startDate": "2025-05-01"
}
}Error response:
{
"success": false,
"message": "Payment processing failed",
"error": {
"code": "API_ERROR",
"details": "Error message details"
}
}var card = new CreditCardData
{
Token = paymentReference,
CardHolderName = $"{firstName} {lastName}"
};
var address = new Address { PostalCode = SanitizePostalCode(billingZip) };
var response = await card.Charge(amount)
.WithCurrency("USD")
.WithAddress(address)
.Execute();
return new { transactionId = response.TransactionId };// Step 1 — Create customer
var customer = new Customer
{
Id = Guid.NewGuid().ToString(),
FirstName = firstName,
Email = donorEmail,
Address = new Address { StreetAddress1 = streetAddress, ... }
};
var savedCustomer = customer.Create();
// Step 2 — Store payment method
var savedMethod = savedCustomer
.AddPaymentMethod(Guid.NewGuid().ToString(), card)
.Create();
// Step 3 — Create schedule
var schedule = savedMethod.AddSchedule(Guid.NewGuid().ToString())
.WithStatus("Active")
.WithAmount(amount)
.WithCurrency("USD")
.WithFrequency(MapFrequency(frequency))
.WithStartDate(DateTime.Parse(startDate));
// Apply duration constraint
if (durationType == "end_date") schedule.WithEndDate(DateTime.Parse(endDate));
if (durationType == "num_payments") schedule.WithNumberOfPayments(numPayments);
var savedSchedule = schedule.Create();| Brand | Card Number | CVV | Expiry |
|---|---|---|---|
| Visa | 4012002000060016 | 123 | Any future date |
| Mastercard | 5473500000000014 | 123 | Any future date |
| Discover | 6011000990156527 | 123 | Any future date |
| Amex | 372700699251018 | 1234 | Any future date |
docker build -t portico-donation-dotnet .
docker run -p 8006:8000 \
-e ASPNETCORE_URLS=http://+:8000 \
-e PUBLIC_API_KEY=your_key \
-e SECRET_API_KEY=your_key \
portico-donation-dotnet
# Open http://localhost:8006Or via docker-compose from the project root:
docker-compose up dotnetHosted Fields not loading
Verify GET /config returns a 200 with a valid publicApiKey. If it fails, confirm .env is present and PUBLIC_API_KEY is set. The SDK reads env vars at startup via DotEnv.Load() — restart dotnet run after editing .env.
"Missing required fields" (400)
Recurring requires additional fields beyond one-time: phone, street_address, city, state, country. Confirm all required fields are in the JSON body.
"Payment processing failed" — Portico error
Confirm SECRET_API_KEY starts with skapi_cert_. The GetEnvVar() helper strips inline comments (e.g. #gitleaks:allow) automatically. Check the server console for the raw Portico exception message.
dotnet run fails with package error
Verify .NET 9+ is installed: dotnet --version. Run dotnet restore to pull fresh packages. If GlobalPayments.Api restore fails, clear the NuGet cache: dotnet nuget locals all --clear.
Static files not served (index.html 404)
The frontend is served from wwwroot/. Ensure app.UseStaticFiles() and app.UseDefaultFiles() are both present in Program.cs and that wwwroot/index.html exists.
Schedule created but no immediate charge
Recurring schedule creation does not charge the donor immediately. The first charge occurs on start_date. If omitted, the default is the first day of the following month.