Skip to content

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.

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).
  • GetCatalogAsynclive catalogue including capability metadata (countries, currencies, amount bounds, sequence types). Called by the admin activation flow to snapshot the capability onto PaymentMethodConfiguration. See Availability & provider catalog.
  • ChargeAsync / RefundAsync / GetStatusAsync — the charge lifecycle. Providers map their native API responses into ProviderChargeStatus and RefundStatus via 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.

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.

ClassStripe APIPurpose
StripePaymentProviderPaymentIntentCharge, refund, status check, catalog
StripeCheckoutSessionFactoryCheckout SessionHosted payment pages
StripePaymentMethodManagerCustomer + PaymentMethodList, attach, detach
StripeWebhookVerifierEventUtilityHMAC-SHA256 signature validation
StripeCatalogStatic capability catalogue (countries, bounds, sequences)

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).

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.

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.

{
"Payments": {
"Stripe": {
"SecretKey": "sk_test_xxx",
"WebhookSecret": "whsec_xxx",
"PublishableKey": "pk_test_xxx"
}
}
}

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.

ClassMollie APIPurpose
MolliePaymentProviderPayments APICharge, refund, status check, catalog
MollieCheckoutSessionFactoryPayments APIHosted payment pages
MolliePaymentMethodManagerCustomers + MandatesMandate setup for recurring
MollieWebhookVerifierPayment pollingID-based verification (no signature)
MollieCatalogStatic capability catalogue
{
"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 ProcessingSucceeded.

ComponentPurpose
SepaTransferPaymentProviderCharge returns pending tx with reference; refund is manual
SepaTransferCheckoutSessionFactoryHosted page showing IBAN + reference
StructuredReferenceGeneratorBelgian OGM-VCS / SEPA RF reference formats
Camt053ParserParses camt.053.001.xx account statements
DefaultBankReconciliationProcessorMatches CAMT entries to open transactions

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:

OutcomeStatus
Catalog returned ≥ 1 methodHealthy
Catalog empty (auth OK, nothing to offer)Degraded
OperationCanceledException (5 s timeout)Unhealthytimed out
UnauthorizedAccessExceptionUnhealthyauth failed
Any other exceptionUnhealthyunreachable: {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.

The shortest viable provider:

  1. Create Granit.Payments.{ProviderName} with a Granit{ProviderName}Module.
  2. Implement IPaymentProviderName, SupportedMethods, GetCatalogAsync, ChargeAsync, RefundAsync, GetStatusAsync.
  3. Implement IPaymentWebhookVerifier keyed on the provider name.
  4. Register the provider via services.AddScoped<IPaymentProvider, …>() and the verifier via AddScoped<IPaymentWebhookVerifier, …>().
  5. Register the readiness probe via services.AddHealthChecks().AddGranitPaymentProviderHealthCheck(name).
  6. Maintain a {Provider}Catalog static 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.