NordAPI.Swish
1.0.5
dotnet add package NordAPI.Swish --version 1.0.5
NuGet\Install-Package NordAPI.Swish -Version 1.0.5
<PackageReference Include="NordAPI.Swish" Version="1.0.5" />
<PackageVersion Include="NordAPI.Swish" Version="1.0.5" />
<PackageReference Include="NordAPI.Swish" />
paket add NordAPI.Swish --version 1.0.5
#r "nuget: NordAPI.Swish, 1.0.5"
#:package NordAPI.Swish@1.0.5
#addin nuget:?package=NordAPI.Swish&version=1.0.5
#tool nuget:?package=NordAPI.Swish&version=1.0.5
NordAPI.Swish SDK
Production notice In-memory nonce store is for development only. In production you must use a persistent store (Redis/DB). Set
SWISH_REDIS(orREDIS_URL/SWISH_REDIS_CONN). The sample fails fast inProductionif none is set.
Licensing notice: NordAPI is an SDK. You need your own Swish/BankID production agreements and certificates. NordAPI does not provide them.
Official NordAPI SDK for Swish and upcoming BankID integrations.
πΈπͺ Swedish version: README.sv.md β See also: Integration Checklist
A lightweight and secure .NET SDK for integrating Swish payments and refunds, with a focus on safe test and development workflows.
Includes built-in HMAC signing, optional mTLS, and retry/rate limiting via HttpClientFactory.
π‘ BankID SDK support is planned next β stay tuned for the NordAPI.BankID package.
Supported .NET versions: .NET 8 (LTS). Planned: .NET 10 (LTS) support.
π Table of Contents
- Requirements
- Installation
- Quickstart β Minimal Program.cs
- Usage Example: Creating a Payment
- Typical Swish Flow (high-level)
- Configuration β Environment Variables & User-Secrets
- mTLS (optional)
- Running Samples and Tests
- Webhook Smoke Test
- API Overview (Signatures & Models)
- Error Scenarios & Retry Policy
- Security Recommendations
- Contributing (PR/CI Requirements)
- Release & Versioning
- FAQ
- License
Requirements
- .NET 8+ (SDK and Runtime)
- Windows / macOS / Linux
- (Optional) Redis if you want distributed replay protection for webhooks
Installation
Install from NuGet:
dotnet add package NordAPI.Swish
Or via PackageReference in .csproj:
<ItemGroup>
<PackageReference Include="NordAPI.Swish" />
</ItemGroup>
Tip: omit a fixed version to pull the latest stable. Pin a concrete version in production deployments.
Quickstart β Minimal Program.cs
This block is compilable as a full file in a new
webproject (dotnet new web). File:Program.cs
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NordAPI.Swish;
using NordAPI.Swish.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Register the Swish client using environment variables
// NOTE (Quickstart): uses simple dev fallbacks; in production, use secrets and remove fallbacks.
builder.Services.AddSwishClient(opts =>
{
opts.BaseAddress = new Uri(Environment.GetEnvironmentVariable("SWISH_BASE_URL")
?? "https://example.invalid");
opts.ApiKey = Environment.GetEnvironmentVariable("SWISH_API_KEY")
?? "dev-key";
opts.Secret = Environment.GetEnvironmentVariable("SWISH_SECRET")
?? "dev-secret";
});
var app = builder.Build();
app.MapGet("/ping", async (ISwishClient swish) =>
{
var result = await swish.PingAsync();
return Results.Ok(new { ping = result });
});
app.Run();
Run:
dotnet new web -n SwishQuickStart
cd SwishQuickStart
dotnet add package NordAPI.Swish
# Replace Program.cs content
dotnet run
Usage Example: Creating a Payment
Replace
payeeAliaswith your Swish merchant number (MSISDN without β+β). Signatures and model names match the API Overview below.
using Microsoft.AspNetCore.Mvc;
// Minimal API endpoint that creates a payment using the SDK model
app.MapPost("/payments", async ([FromBody] CreatePaymentRequest input, ISwishClient swish, CancellationToken ct) =>
{
// Tip: validate Amount/Currency/aliases before calling Swish
var payment = await swish.CreatePaymentAsync(input, ct);
return Results.Ok(payment);
});
Example request body
{
"payerAlias": "46701234567",
"payeeAlias": "1231181189",
"amount": "100.00",
"currency": "SEK",
"message": "Test purchase",
"callbackUrl": "https://yourdomain.test/webhook/swish"
}
curl
curl -v -X POST http://localhost:5000/payments \
-H "Content-Type: application/json; charset=utf-8" \
--data-raw '{
"payerAlias": "46701234567",
"payeeAlias": "1231181189",
"amount": "100.00",
"currency": "SEK",
"message": "Test purchase",
"callbackUrl": "https://yourdomain.test/webhook/swish"
}'
In production, the flow is asynchronous: Swish will notify your backend via the webhook (
callbackUrl). See the smoke test for signature details.
Typical Swish Flow (high-level)
1) Client (web/app)
|
v
2) Your Backend
|
v
3) Swish API
|
v
4) User approves payment in Swish app
|
v
5) Swish sends callback (payment result)
|
v
6) Your Webhook endpoint
|
v
Update order status / notify client
- Your backend creates the payment via
CreatePaymentAsync. - The end-user approves in the Swish app.
- Swish POSTs the result to your webhook (
callbackUrl). Your webhook must verify HMAC (X-Swish-Signature) as Base64 HMAC-SHA256 of"<ts>\n<nonce>\n<body>"(UTF-8).
Configuration β Environment Variables & User-Secrets
| Variable | Purpose | Example |
|---|---|---|
SWISH_BASE_URL |
Base URL for Swish API | https://example.invalid |
SWISH_API_KEY |
API key for HMAC authentication | dev-key |
SWISH_SECRET |
Shared secret for HMAC | dev-secret |
SWISH_PFX_PATH |
Path to client certificate (.pfx) | C:\certs\swish-client.pfx |
SWISH_PFX_PASSWORD |
Password for the certificate | β’β’β’β’ |
SWISH_WEBHOOK_SECRET |
Secret for webhook HMAC | dev_secret |
SWISH_REDIS |
Redis connection string (nonce store) | localhost:6379 |
SWISH_DEBUG |
Verbose logging / enable dev modes | 1 |
SWISH_ALLOW_OLD_TS |
Allow older timestamps (dev only) | 1 |
Set with User-Secrets (example):
dotnet user-secrets init
dotnet user-secrets set "SWISH_API_KEY" "dev-key"
dotnet user-secrets set "SWISH_SECRET" "dev-secret"
dotnet user-secrets set "SWISH_BASE_URL" "https://example.invalid"
π Never commit secrets or certificates. Use environment variables, User-Secrets, or a vault (e.g., Azure Key Vault).
mTLS (optional)
Enable client certificate (PFX):
$env:SWISH_PFX_PATH = "C:\certs\swish-client.pfx"
$env:SWISH_PFX_PASSWORD = "secret-password"
Behavior
- No certificate β fallback without mTLS.
- Debug: relaxed server certificate validation (local only).
- Release: strict certificate chain (no βallow invalid chainβ).
Running Samples and Tests
# Build the repository
dotnet restore
dotnet build
# Run the sample web app
dotnet run --project .\samples\SwishSample.Web\SwishSample.Web.csproj --urls http://localhost:5000
# Run tests
dotnet test
Webhook Smoke Test
Start the sample server in one terminal:
$env:SWISH_WEBHOOK_SECRET = "dev_secret"
dotnet run --project .\samples\SwishSample.Web\SwishSample.Web.csproj --urls http://localhost:5000
Run the smoke test in another terminal:
.\scripts\smoke-webhook.ps1 -Secret dev_secret -Url http://localhost:5000/webhook/swish
For quick manual testing you can also POST the webhook using curl (bash/macOS/Linux).
Signature spec: HMAC-SHA256 over the canonical string "<timestamp>\n<nonce>\n<body>", using SWISH_WEBHOOK_SECRET. Encode as Base64.
π§© Note: Sign the exact UTFβ8 bytes of the compact JSON body (Content-Type:
application/json; charset=utf-8). Any whitespace or prettifying will break signature verification.
Required request headers
| Header | Description | Example |
|---|---|---|
X-Swish-Timestamp |
Unix timestamp in seconds | 1735589201 |
X-Swish-Nonce |
Unique ID to prevent replay | 550e8400-e29b-41d4-a716-446655440000 |
X-Swish-Signature |
Base64 HMAC-SHA256 of "<ts>\n<nonce>\n<body>" |
W9CzL8f...== |
Example webhook payload
{
"event": "payment_received",
"paymentId": "pay_123456",
"amount": "100.00",
"currency": "SEK",
"payer": { "phone": "46701234567" },
"metadata": { "orderId": "order_987" }
}
curl smoke test (bash / macOS / Linux)
# 1) Prepare values
ts="$(date +%s)"
nonce="$(uuidgen)"
body='{"event":"payment_received","paymentId":"pay_123456","amount":"100.00","currency":"SEK","payer":{"phone":"46701234567"},"metadata":{"orderId":"order_987"}}'
# 2) Compute canonical and Base64 signature (uses SWISH_WEBHOOK_SECRET)
canonical="$(printf "%s\n%s\n%s" "$ts" "$nonce" "$body")"
sig="$(printf "%s" "$canonical" | openssl dgst -sha256 -hmac "${SWISH_WEBHOOK_SECRET:-dev_secret}" -binary | openssl base64)"
# 3) Send
curl -v -X POST "http://localhost:5000/webhook/swish" \
-H "Content-Type: application/json; charset=utf-8" \
-H "X-Swish-Timestamp: $ts" \
-H "X-Swish-Nonce: $nonce" \
-H "X-Swish-Signature: $sig" \
--data-raw "$body"
β Expected (HTTP 200)
{"received": true}
β Expected on replay (HTTP 409)
{"reason": "replay detected (nonce seen before)"}
In production: set
SWISH_REDIS(aliasesREDIS_URLandSWISH_REDIS_CONNare accepted). Without Redis, an in-memory store is used (suitable for local development).
API Overview (Signatures & Models)
The types below illustrate the expected surface. Names/namespaces should match the library you reference.
public interface ISwishClient
{
Task<string> PingAsync(CancellationToken ct = default);
Task<CreatePaymentResponse> CreatePaymentAsync(CreatePaymentRequest request, CancellationToken ct = default);
Task<CreatePaymentResponse> GetPaymentStatusAsync(string paymentId, CancellationToken ct = default);
Task<CreateRefundResponse> CreateRefundAsync(CreateRefundRequest request, CancellationToken ct = default);
Task<CreateRefundResponse> GetRefundStatusAsync(string refundId, CancellationToken ct = default);
}
public sealed record CreatePaymentRequest(
string PayerAlias,
string PayeeAlias,
string Amount,
string Currency,
string Message,
string CallbackUrl
);
public sealed record CreatePaymentResponse(
string Id,
string Status,
string? ErrorCode = null,
string? ErrorMessage = null
);
public sealed record CreateRefundRequest(
string OriginalPaymentReference,
string Amount,
string Currency,
string Message,
string CallbackUrl
);
public sealed record CreateRefundResponse(
string Id,
string Status,
string? ErrorCode = null,
string? ErrorMessage = null
);
Error Scenarios & Retry Policy
The SDK registers a named HttpClient "Swish" with:
- Timeout: 30 seconds
- Retry: up to 3 attempts (exponential backoff + jitter) on
408,429,5xx,HttpRequestException,TaskCanceledException(timeout).
Enable/extend:
services.AddSwishHttpClient(); // registers "Swish" (timeout + retry + mTLS if env vars exist)
services.AddHttpClient("Swish")
.AddHttpMessageHandler(_ => new MyCustomHandler()); // outside SDK retry-pipeline
Common responses:
- 400 Bad Request β validation error (check required fields).
- 401 Unauthorized β invalid
SWISH_API_KEY/SWISH_SECRETor missing headers. - 429 Too Many Requests β follow retry policy/backoff.
- 5xx β transient; retry automatically triggered by pipeline.
Security Recommendations
- Use User-Secrets / Key Vault for secrets β never hardcode in code or commit to the repo.
- mTLS βallow invalid chainβ must only be used locally (Debug). In production, enforce a valid chain.
- Webhook secret (
SWISH_WEBHOOK_SECRET) should be rotated regularly and stored securely (e.g., Key Vault).
Contributing (PR/CI Requirements)
- Create a feature branch from
main. - Verify locally:
dotnet build,dotnet test, and webhook smoke test if modified. - Ensure README examples compile (Quickstart must be copy-paste runnable).
- Open PR with description + checklist. CI must pass:
- Build & tests green
- (Optional) Lint/format
- Code review β squash/merge.
Release & Versioning
- SemVer:
MAJOR.MINOR.PATCH - CI publish is gated; tag the repo (e.g.,
v1.0.0) to publish to NuGet. - The README in the package (
PackageReadmeFile) is shown on NuGet.
Install a specific version:
dotnet add package NordAPI.Swish --version 1.2.3
FAQ
401 in tests β Check SWISH_API_KEY/SWISH_SECRET and ensure your clock is synchronized.
Replay always denied β Change nonce between calls and clear in-memory/Redis. Check SWISH_REDIS.
mTLS error in production β Validate SWISH_PFX_PATH + SWISH_PFX_PASSWORD and the certificate chain.
License
MIT License. Security contact: security@nordapi.com.
Last updated: November 2025
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.Extensions.Http (>= 9.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.9)
- Microsoft.Extensions.Options (>= 9.0.9)
- StackExchange.Redis (>= 2.9.25)
- System.Net.Http.Json (>= 9.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.5 | 174 | 12/24/2025 |