Providers — Stripe, Mollie, SEPA
Each payment integration implements the same IPaymentProvider contract. The host
decides which providers to register, the admin decides which methods on each provider
to activate, and the runtime resolves a concrete provider per (tenant, method) at
charge time.
The IPaymentProvider contract
Section titled “The IPaymentProvider contract”public interface IPaymentProvider{ string Name { get; }
IReadOnlyList<PaymentMethodDescriptor> SupportedMethods { get; }
Task<IReadOnlyList<PaymentMethodCatalogEntry>> GetCatalogAsync( CancellationToken cancellationToken = default);
Task<PaymentProviderChargeResult> ChargeAsync( PaymentChargeRequest request, CancellationToken cancellationToken = default);
Task<PaymentProviderRefundResult> RefundAsync( PaymentRefundRequest request, CancellationToken cancellationToken = default);
Task<PaymentProviderStatus> GetStatusAsync( string providerTransactionId, CancellationToken cancellationToken = default);}SupportedMethods— the fixed list of method identifiers this provider can handle, used primarily by the admin listing endpoint (one row per method).GetCatalogAsync— live catalogue including capability metadata (countries, currencies, amount bounds, sequence types). Called by the admin activation flow to snapshot the capability ontoPaymentMethodConfiguration. See Availability & provider catalog.ChargeAsync/RefundAsync/GetStatusAsync— the charge lifecycle. Providers map their native API responses intoProviderChargeStatusandRefundStatusvia small mapper helpers.
Providers also typically ship an I{Provider}WebhookVerifier (signature validation)
and an I{Provider}PaymentMethodManager (attach / detach saved methods) — see each
provider’s section below.
Stripe (Granit.Payments.Stripe)
Section titled “Stripe (Granit.Payments.Stripe)”Full implementation using the Stripe.NET SDK and the PaymentIntent API (not the legacy Charges API). Covers the European surface plus Alipay / WeChat Pay for merchants with China exposure.
| Class | Stripe API | Purpose |
|---|---|---|
StripePaymentProvider | PaymentIntent | Charge, refund, status check, catalog |
StripeCheckoutSessionFactory | Checkout Session | Hosted payment pages |
StripePaymentMethodManager | Customer + PaymentMethod | List, attach, detach |
StripeWebhookVerifier | EventUtility | HMAC-SHA256 signature validation |
StripeCatalog | — | Static capability catalogue (countries, bounds, sequences) |
StripeClientFactory (no static config)
Section titled “StripeClientFactory (no static config)”StripeConfiguration.ApiKey is never used (global static, unsafe for
multi-tenant). StripeClientFactory creates an IStripeClient per request via
IHttpClientFactory:
// DI registration (in GranitPaymentsStripeModule)services.AddScoped<IStripeClient>(sp => sp.GetRequiredService<StripeClientFactory>().Create());This enables future Stripe Connect (per-tenant API keys) and integrates with
IHttpClientFactory lifecycle (pooling, logging, resilience policies).
Provider customer mapping
Section titled “Provider customer mapping”ProviderCustomerMapping maps Granit TenantId → provider customer ID
(cus_xxx on Stripe, cst_xxx on Mollie). It’s a generic entity reusable by any
provider — Granit.Payments owns the abstraction, provider packages create and
read rows through it. Auto-created on the first AttachAsync() call.
Amount conversion
Section titled “Amount conversion”StripeAmountConverter handles zero-decimal currencies (JPY, KRW, VND, …)
automatically — Stripe uses cents for EUR/USD but major units for JPY. The
converter also handles three-decimal currencies (BHD, JOD, KWD) and the
special-case KRW that Stripe treats as zero-decimal even though ISO-4217 is
ambiguous.
Configuration
Section titled “Configuration”{ "Payments": { "Stripe": { "SecretKey": "sk_test_xxx", "WebhookSecret": "whsec_xxx", "PublishableKey": "pk_test_xxx" } }}Mollie (Granit.Payments.Mollie)
Section titled “Mollie (Granit.Payments.Mollie)”EU-sovereign provider. Mollie’s strength over Stripe is direct support for Belgian local methods (Belfius, KBC), Nordic banking (Trustly), vouchers (Paysafecard), and BNPL variants that Stripe does not expose in the same account surface.
| Class | Mollie API | Purpose |
|---|---|---|
MolliePaymentProvider | Payments API | Charge, refund, status check, catalog |
MollieCheckoutSessionFactory | Payments API | Hosted payment pages |
MolliePaymentMethodManager | Customers + Mandates | Mandate setup for recurring |
MollieWebhookVerifier | Payment polling | ID-based verification (no signature) |
MollieCatalog | — | Static capability catalogue |
Configuration
Section titled “Configuration”{ "Payments": { "Mollie": { "ApiKey": "test_xxx" } }}SEPA Direct Debit (Granit.Payments.SepaDirectDebit.Builtin)
Section titled “SEPA Direct Debit (Granit.Payments.SepaDirectDebit.Builtin)”Self-hosted SEPA Direct Debit — no external gateway. Creates a collection in
Processing state; reconciliation from a CAMT.053 bank statement import moves the
transaction to Succeeded or Failed.
Use case: you have a banking relationship with IBAN / BIC, you issue SDD mandates yourself, and you want to keep the entire flow on-premises (no Cloud Act exposure for the payment data).
Requires a PAIN.008 batch generator (provided — Pain008Generator) and an upstream
CAMT.053 import path (Granit.Payments.SepaTransfer ships a parser shared between
SDD and SCT).
SEPA Transfer (Granit.Payments.SepaTransfer)
Section titled “SEPA Transfer (Granit.Payments.SepaTransfer)”Bank transfers with structured reference reconciliation. The charge returns a
pending transaction with a structured reference that the payer includes in their
transfer. The reconciliation processor matches incoming CAMT.053 entries against
open transactions by reference + amount, promoting Processing → Succeeded.
| Component | Purpose |
|---|---|
SepaTransferPaymentProvider | Charge returns pending tx with reference; refund is manual |
SepaTransferCheckoutSessionFactory | Hosted page showing IBAN + reference |
StructuredReferenceGenerator | Belgian OGM-VCS / SEPA RF reference formats |
Camt053Parser | Parses camt.053.001.xx account statements |
DefaultBankReconciliationProcessor | Matches CAMT entries to open transactions |
Health check
Section titled “Health check”Every provider ships a readiness probe that calls GetCatalogAsync — one hit covers
authentication, reachability and response parsing, and the API-backed providers
already cache the catalogue 60 s so K8s probes do not hammer the provider.
Register it from the provider module:
context.Services.AddHealthChecks() .AddGranitPaymentProviderHealthCheck("mollie");The registration produces a check named payments-mollie, tagged readiness
and payments, with HealthStatus.Degraded as the failure status so a single
misbehaving provider does not cascade a /ready outage. Mapping:
| Outcome | Status |
|---|---|
| Catalog returned ≥ 1 method | Healthy |
| Catalog empty (auth OK, nothing to offer) | Degraded |
OperationCanceledException (5 s timeout) | Unhealthy — timed out |
UnauthorizedAccessException | Unhealthy — auth failed |
| Any other exception | Unhealthy — unreachable: {ExceptionType} |
Error messages contain only the provider name and the exception type name —
URLs, API keys and tokens are never exposed, matching the convention from
HttpServiceHealthCheckBase. Static-catalogue providers (SEPA Transfer, SEPA
Direct Debit) are trivially Healthy; the check is still registered to keep the
readiness surface uniform at zero cost.
Adding a new provider
Section titled “Adding a new provider”The shortest viable provider:
- Create
Granit.Payments.{ProviderName}with aGranit{ProviderName}Module. - Implement
IPaymentProvider—Name,SupportedMethods,GetCatalogAsync,ChargeAsync,RefundAsync,GetStatusAsync. - Implement
IPaymentWebhookVerifierkeyed on the provider name. - Register the provider via
services.AddScoped<IPaymentProvider, …>()and the verifier viaAddScoped<IPaymentWebhookVerifier, …>(). - Register the readiness probe via
services.AddHealthChecks().AddGranitPaymentProviderHealthCheck(name). - Maintain a
{Provider}Catalogstatic class with the real capability metadata (countries, currencies, bounds, sequence types). Don’t return wildcard in production — the availability filter won’t filter anything meaningful.
If the provider supports saved methods, also implement IPaymentMethodManager for
attach / detach. If the provider needs per-request credentials (e.g., Stripe Connect,
Mollie Partner), inject an IHttpClientFactory and mint clients per scope.
See Availability & provider catalog for
how the capability data plugs into the runtime filtering on
GET /methods/available.