Milkyway.Payments.Sdk
1.1.0
dotnet add package Milkyway.Payments.Sdk --version 1.1.0
NuGet\Install-Package Milkyway.Payments.Sdk -Version 1.1.0
<PackageReference Include="Milkyway.Payments.Sdk" Version="1.1.0" />
<PackageVersion Include="Milkyway.Payments.Sdk" Version="1.1.0" />
<PackageReference Include="Milkyway.Payments.Sdk" />
paket add Milkyway.Payments.Sdk --version 1.1.0
#r "nuget: Milkyway.Payments.Sdk, 1.1.0"
#:package Milkyway.Payments.Sdk@1.1.0
#addin nuget:?package=Milkyway.Payments.Sdk&version=1.1.0
#tool nuget:?package=Milkyway.Payments.Sdk&version=1.1.0
MilkyWay Payments SDK for .NET
Official C# client for the MilkyWay Payments API (/payments/v1) — the
partner-facing API that banks use to initiate, quote, track, and cancel
cross-bank payments.
Batteries included:
- Keycloak client-credentials auth with in-memory token caching and automatic
refresh (and a one-shot refresh-and-replay on
401). - Retries via Polly: exponential backoff with jitter on transient failures (5xx, 408, network), with deterministic errors (400/401/402/404) never retried.
- Typed models & exceptions — money is
decimal, status is anenum, and each HTTP error maps to a specific exception type. IHttpClientFactory/ DI integration, plus a plainnew MilkywayPaymentsClient(options)for non-DI apps.- Multi-targets
netstandard2.0(works on .NET Framework 4.6.1+, Mono, Xamarin) andnet8.0.
Install
The package is on NuGet.org — install it with whichever tool you use:
# .NET CLI
dotnet add package Milkyway.Payments.Sdk
# Package Manager Console (Visual Studio)
Install-Package Milkyway.Payments.Sdk
<PackageReference Include="Milkyway.Payments.Sdk" Version="1.0.0" />
Targets net8.0 and netstandard2.0, so it works on modern .NET as well as
.NET Framework 4.6.1+, Mono, and Xamarin. No other setup — bring your Keycloak
ClientId/ClientSecret and you're ready.
Quick start
using Milkyway.Payments.Sdk;
using Milkyway.Payments.Sdk.Models;
using var client = new MilkywayPaymentsClient(new MilkywayOptions
{
BaseUrl = "https://milkyway.stage.planet9.ae",
TokenUrl = "https://keycloak.ac8o.planet9.ae/realms/planet9-stage/protocol/openid-connect/token",
ClientId = "your-client-id", // issued to your institution
ClientSecret = "your-client-secret",
});
// 1. Is the recipient bank's service online?
await client.HealthcheckAsync("bank-beta", "card-payout");
// 2. Quote the payment (FX markup + commission applied here).
PrecheckResult quote = await client.PrecheckAsync(new PrecheckRequest
{
ThirdPartyIdDebit = "bank-beta",
ServiceId = "card-payout",
RecipientId = "recipient-9999",
AmountCredit = 100.00m,
CurrencyCredit = "USD",
});
Console.WriteLine($"Rate {quote.Rate}, debit {quote.AmountDebit} {quote.CurrencyDebit}, commission {quote.Commission}");
// 3. Initiate the payment. Pass an Idempotency-Key so retries are safe.
long transactionId = await client.PayAsync(new PayRequest
{
ThirdPartyIdDebit = "bank-beta",
ServiceId = "card-payout",
SenderId = "sender-0001",
RecipientId = "recipient-9999",
AmountCredit = 100.00m,
CurrencyCredit = "USD",
Data = new Dictionary<string, object?> { ["passport"] = "AA1234567" },
}, idempotencyKey: Guid.NewGuid().ToString());
// 4. Poll until the payment reaches a terminal status.
PostcheckResult result = await client.WaitForCompletionAsync(transactionId);
Console.WriteLine($"Final status: {result.Status}");
Discovery
Before quoting or paying, you can browse the catalog of recipient banks and the services each offers — two read-only calls:
// List every destination, or filter by country (ISO 3166-1 alpha-2, case-insensitive).
IReadOnlyList<Destination> destinations = await client.DiscoveryAsync("TJ");
foreach (var d in destinations)
{
Console.WriteLine($"{d.ThirdPartyId} ({d.PartnerName}, {d.Country})");
foreach (var s in d.Services)
Console.WriteLine($" {s.ServiceId} via {s.PayoutMethod} → {string.Join(", ", s.PayoutCurrencies ?? Array.Empty<string>())}");
}
// Fetch the Draft-2020 JSON Schema a service validates its `data` payload against.
JsonElement schema = await client.ServiceSchemaAsync("bank-beta", "card-payout");
Console.WriteLine(schema.GetProperty("required").GetArrayLength());
DiscoveryAsync unwraps the destinations list for you; the per-service JSON
Schema is omitted from the catalog and fetched on demand with ServiceSchemaAsync
(returned as a System.Text.Json.JsonElement). Both throw MilkywayAuthException
on 401; ServiceSchemaAsync throws MilkywayNotFoundException when the
bank/service pair is unknown.
Dependency injection (ASP.NET Core)
using Milkyway.Payments.Sdk.DependencyInjection;
builder.Services.AddMilkywayPayments(o =>
{
o.BaseUrl = builder.Configuration["Milkyway:BaseUrl"]!;
o.TokenUrl = builder.Configuration["Milkyway:TokenUrl"]!;
o.ClientId = builder.Configuration["Milkyway:ClientId"]!;
o.ClientSecret = builder.Configuration["Milkyway:ClientSecret"]!;
});
// Then inject IMilkywayPaymentsClient anywhere.
public sealed class PayoutService(IMilkywayPaymentsClient milkyway) { /* ... */ }
The data field
Each service requires extra per-partner fields (sender name, document number,
birthday, …) in the Data dictionary. Which keys are required depends on your
ServiceId and the recipient bank — look them up in the Услуги registry at
https://milkyway-docs.stage.planet9.ae. The server validates data against the
service's JSON Schema during Precheck, so a missing field is rejected before any
money moves.
Errors
All API errors throw a subclass of MilkywayApiException (carrying StatusCode
and the server's message):
| HTTP | Exception | Meaning |
|---|---|---|
| 400 | MilkywayValidationException |
Bad request (invalid amount, missing field, unresolvable FX rate). |
| 401 | MilkywayAuthException |
Token missing/invalid (also thrown if token acquisition fails). |
| 402 | MilkywayExposureBlockedException |
Payment would breach a block-action exposure limit. |
| 404 | MilkywayNotFoundException |
Transaction not found or not owned by your institution. |
| 5xx | MilkywayServiceUnavailableException |
API or downstream recipient unavailable (retried automatically first). |
Retries & idempotency
Transient failures are retried automatically with exponential backoff + jitter
(tunable via MilkywayOptions.MaxRetries / RetryBaseDelay). PayAsync is only
auto-retried when you supply an idempotencyKey — without one, a retry could
create a duplicate payment, so the SDK sends it exactly once.
Configuration
| Option | Default | Purpose |
|---|---|---|
BaseUrl |
— (required) | Payments API base URL. |
TokenUrl |
— (required) | Keycloak token endpoint. |
ClientId / ClientSecret |
— (required) | Your institution's credentials. |
Scope |
none | Optional OAuth scope. |
TokenRefreshSkew |
30s | Refresh this long before token expiry. |
RequestTimeout |
30s | Per-attempt request timeout. |
MaxRetries |
3 | Max transient-failure retries. |
RetryBaseDelay |
500ms | Base delay for exponential backoff. |
Building from source
dotnet build
dotnet test
dotnet pack src/Milkyway.Payments.Sdk -c Release # produces the NuGet package
Releasing
Releases are fully automated by semantic-release
on every push to main:
- Conventional commits are analysed (
feat:→ minor,fix:/perf:→ patch,!/BREAKING CHANGE→ major). No releasable commits → no release. - The package is packed with the computed version and pushed to NuGet.org via Trusted Publishing (OIDC — no long-lived API key stored anywhere).
- A GitHub release +
vX.Y.Ztag is created with generated notes.
One-time setup (maintainers): on nuget.org → Trusted Publishing, add a policy
for owner bankplanet9, repo milkyway-csharp-sdk, workflow file ci.yml. The
nuget.org user is hardcoded in the workflow; no secrets or variables are required.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options (>= 8.0.2)
- Polly (>= 8.4.2)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options (>= 8.0.2)
- Polly (>= 8.4.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.