gRPC Integrations#
Internal service-to-service API for payment adapters, card processors, FX feeds, and platform microservices.gRPC (internal): cluster-only; never routed through KrakenD.
Overview#
| Direction | Who calls whom | Proto package | Typical use |
|---|
| Inbound to finance | Adapter → finance | finance.v1 | Transfers, card payments, FX rate publish, ledger statement export |
| Outbound from finance | finance → adapter | provider.v1 | Account open/close, card issue, PAN/CVV |
| Platform internal | Any service → identity/compliance | identity.v1, compliance.v1 | KYC gate, user lookup, step-up validation |
Proto sources live in each service repository:| Service | Proto path |
|---|
| finance-service | proto/finance/v1/*.proto, proto/provider/v1/*.proto |
| identity-service | proto/identity/v1/identity.proto |
| compliance-service | proto/compliance/v1/compliance.proto |
Generate Go stubs: make proto in the respective service directory.
Connectivity#
Kubernetes (production / Minikube)#
| K8s Service | gRPC address | Listens |
|---|
finance | finance:9090 | TransferService, CardPaymentService, FXRateService, TransactionStatementService |
identity | identity:9090 | IdentityInternal |
compliance | compliance:9090 | ComplianceInternal |
HTTP for the same pods is on port 8080 (REST). gRPC uses a separate port on the same Service object.Environment variables (ConfigMap / Deployment):| Variable | Example | Used by |
|---|
GRPC_ADDR | :9090 | finance, identity, compliance (listen) |
FINANCE_GRPC_ADDR | finance:9090 | crypto-service, future payment adapters |
IDENTITY_GRPC_ADDR | identity:9090 | finance, compliance, notification, api-gateway |
COMPLIANCE_GRPC_ADDR | compliance:9090 | finance |
PROVIDERS_CONFIG | /config/providers.yaml | finance (provider routing) |
Local dev defaults (without K8s): finance gRPC localhost:9092, compliance localhost:9091, identity localhost:9090.Call matrix#
Conventions#
Amounts and IDs#
Amounts - decimal strings with fixed scale (e.g. "1500.00", "25.99"). Never use binary floats.
UUIDs - standard string form (550e8400-e29b-41d4-a716-446655440000).
Currency - ISO 4217, 3 letters (EUR, USD).
Actor#
Every mutating finance RPC requires an Actor:| Type | When |
|---|
ACTOR_TYPE_SERVICE | Payment/card/FX adapters, crypto-service |
ACTOR_TYPE_ADMIN | Back-office automation (rare on gRPC) |
ACTOR_TYPE_USER | Not used on gRPC (users go through REST + step-up) |
Service actors skip user MFA but KYC checks on the account owner still apply.Idempotency#
All finance mutating RPCs require:idempotency_key - unique per logical operation
source_service - caller identity (e.g. "currencycloud-adapter")
Uniqueness constraint: (source_service, idempotency_key). Re-sending the same pair returns the existing transfer (success with same ID), not a duplicate ledger entry.Use provider-side IDs in keys when possible: "cc-payment-{external_id}".Traceability fields#
| Field | Purpose |
|---|
external_reference | ID in the external provider system |
source_service | Calling service name for audit |
metadata | Arbitrary string map (settlement refs, provider payload snippets) |
Errors#
Domain errors map to gRPC status codes:| gRPC code | Typical cause |
|---|
INVALID_ARGUMENT | Missing source_service, bad UUID, invalid amount |
NOT_FOUND | Transfer/account/card not found |
FAILED_PRECONDITION | Insufficient balance, KYC blocked, wrong transfer state |
PERMISSION_DENIED | Auth failure (when middleware enabled) |
INTERNAL | Unexpected server error |
Message text contains a human-readable reason; integrate retry only on UNAVAILABLE / INTERNAL with backoff.Authentication (target production setup)#
Documented target (not fully enforced on all servers today):mTLS between pods in the cluster
Service JWT with claims service_id and scoped permissions, e.g.:finance:transfer:create, finance:transfer:execute, finance:transfer:cancel
finance:card_payment:create
Identity ValidateStepUpToken additionally requires metadata x-gateway-token when called from the api-gateway plugin.
finance.v1 - Inbound APIs (adapters call finance)#
Finance exposes four gRPC services on finance:9090. All transfer and card flows converge on the same TransferPipeline used by the public REST API.TransferService#
Proto: finance-service/proto/finance/v1/transfer.proto| RPC | Purpose | Result status |
|---|
CreateTransfer | Create internal, P2P, SWIFT, SEPA, or ACH transfer | Usually PENDING (hold placed) |
ExecuteTransfer | Finalize a pending outbound rail after provider settlement | COMPLETED |
CancelTransfer | Cancel before execute | CANCELLED |
CreditTransfer | Admin credit - inbound funds (wire, crypto sell proceeds) | COMPLETED |
DebitTransfer | Admin debit - outbound funds (crypto buy) | COMPLETED |
GetTransfer | Read transfer by ID | - |
Transfer types (TransferType)#
| Enum | Rail | Typical flow |
|---|
TRANSFER_TYPE_INTERNAL | Book transfer between two platform accounts | Create → immediate complete |
TRANSFER_TYPE_P2P | To another user (by account number / email / phone) | Create → complete |
TRANSFER_TYPE_SEPA | SEPA credit transfer | Create (pending) → Execute |
TRANSFER_TYPE_SWIFT | SWIFT wire | Create (pending) → Execute |
TRANSFER_TYPE_ACH | ACH debit/credit | Create (pending) → Execute |
TRANSFER_TYPE_ADMIN_CREDIT | System credit (via CreditTransfer RPC) | Immediate |
TRANSFER_TYPE_ADMIN_DEBIT | System debit (via DebitTransfer RPC) | Immediate |
Recipient payloads (oneof recipient)#
Admin contra (AdminContra)#
Used by CreditTransfer / DebitTransfer for double-entry settlement:| Enum | Ledger behaviour |
|---|
ADMIN_CONTRA_SETTLEMENT (default) | Customer ↔ provider settlement account (system_provider_settlement) |
ADMIN_CONTRA_EXTERNAL | Top-up / withdrawal treated as external nostro (no settlement mirror) |
Settlement account resolved via accounts.provider_id + provider_settlement_accounts(provider_id, currency).Example: outbound SEPA payout#
1. CreateTransfer - adapter submits payout after user initiates transfer (or adapter receives outbound instruction):{
"idempotency_key": "cc-pay-abc123",
"type": "TRANSFER_TYPE_SEPA",
"from_account_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": "1500.00",
"currency": "EUR",
"description": "Payout to beneficiary",
"sepa": {
"name": "John Doe",
"iban": "DE89370400440532013000",
"currency": "EUR"
},
"external_reference": "cc-payment-id-789",
"source_service": "currencycloud-adapter",
"metadata": { "cc_beneficiary_id": "ben-456" },
"actor": { "type": "ACTOR_TYPE_SERVICE", "id": "currencycloud-adapter" }
}
{
"transfer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "TRANSFER_STATUS_PENDING",
"fee_amount": "2.50",
"hold_id": "hold-uuid"
}
2. ExecuteTransfer - after provider confirms settlement:{
"transfer_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"idempotency_key": "cc-exec-abc123",
"actor": { "type": "ACTOR_TYPE_SERVICE", "id": "currencycloud-adapter" },
"metadata": { "cc_settlement_ref": "settle-999" }
}
On execute, ledger posts double-entry: debit customer, credit provider settlement account (principal); fee leg to system_fee_revenue if applicable.Example: inbound credit (crypto sell)#
crypto-service calls CreditTransfer when user sells crypto for fiat:{
"idempotency_key": "sell-order-42",
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": "500.00",
"currency": "EUR",
"description": "Crypto sell proceeds",
"reference": "order-42",
"contra": "ADMIN_CONTRA_SETTLEMENT",
"external_reference": "onchain-tx-hash",
"source_service": "crypto-service",
"actor": { "type": "ACTOR_TYPE_SERVICE", "id": "crypto-service" }
}
Transfer status lifecycle#
CardPaymentService#
Proto: finance-service/proto/finance/v1/card_payment.protoCard processor adapter debits the account linked to a card (authorization / clearing).| RPC | Purpose |
|---|
CreateCardPayment | Debit card-linked account (purchase) |
ReverseCardPayment | Refund or chargeback against original transfer |
{
"idempotency_key": "auth-001",
"card_id": "card-uuid",
"amount": "25.99",
"currency": "EUR",
"description": "Purchase",
"mcc": "5411",
"merchant_name": "Supermarket XYZ",
"merchant_id": "MERCH-123",
"authorization_code": "ABC123",
"external_reference": "proc-tx-456",
"source_service": "marqeta-adapter",
"actor": { "type": "ACTOR_TYPE_SERVICE", "id": "marqeta-adapter" }
}
FXRateService#
Proto: finance-service/proto/finance/v1/fx_rate.protoMarket-data adapters publish FX rates; crypto-service reads them via ListFXRates.| RPC | Purpose |
|---|
UpsertFXRate | Publish single rate pair |
BatchUpsertFXRates | Publish many rates in one call |
ListFXRates | Read all current rates |
source_service must match actor.id; actor type must be ACTOR_TYPE_SERVICE.
TransactionStatementService#
Proto: finance-service/proto/finance/v1/transaction_statement.protoRead-only ledger statement for back-office and reporting integrations. Mirrors admin REST GET /v1/admin/transactions/statement (permission transfers:read).| RPC | Purpose |
|---|
ListTransactionStatement | Paginated ledger lines with optional account/user scope and date range |
Scope (StatementScope)#
| Enum | REST equivalent | Required fields |
|---|
STATEMENT_SCOPE_ALL | No account_id / user_id | from_date, to_date |
STATEMENT_SCOPE_ACCOUNT | account_id query param | account_id, from_date, to_date |
STATEMENT_SCOPE_USER | user_id query param | user_id, from_date, to_date |
account_id and user_id are mutually exclusive. Date range is inclusive (YYYY-MM-DD), max 366 days.Request / response#
Response: items[], next_cursor, has_more. For CSV export use REST with format=csv (streams all pages).Example (grpcurl)#
provider.v1 - Outbound APIs (finance calls adapter)#
External payment/card providers implement these services. Finance calls them when providers.yaml routes an account type or card product to type: grpc.Proto: finance-service/proto/provider/v1/account.proto, card.protoAccountProviderService#
| RPC | When finance calls |
|---|
OpenAccount | User opens account; finance needs real IBAN / external ID |
CloseAccount | Before local account close |
Finance persists provider_id and external_id on the accounts row.CardProviderService#
| RPC | When finance calls |
|---|
IssueCard | Card issuance |
GetCardNumber | PAN reveal (PCI - never stored in finance DB) |
GetCardCVV | CVV reveal |
BlockCard / UnblockCard | Sync block state with processor |
Payment provider integration guide#
This section describes how to build a deployable adapter (e.g. Currencycloud, Marqeta) that connects to CustodyCore.Architecture#
A payment provider integration is two gRPC relationships, not one:| Concern | Direction | Proto |
|---|
| Account / card provisioning | finance → adapter | provider.v1 |
| Outbound transfers (SEPA/SWIFT/ACH) | adapter → finance | finance.v1.TransferService |
| Inbound credits | adapter → finance | CreditTransfer |
| Card authorizations | adapter → finance | finance.v1.CardPaymentService |
Transfers are never exposed as outbound gRPC from finance to the adapter. The adapter receives provider webhooks and calls finance inbound gRPC.Step-by-step checklist#
1. Scaffold adapter service#
services/integrations/{provider}/
├── cmd/main.go
├── internal/
│ ├── webhook/ # provider → adapter HTTP
│ ├── providerclient/ # adapter → provider REST/SDK
│ └── financeclient/ # adapter → finance gRPC
└── deploy/
No public routes through api-gateway. Adapter runs in-cluster only.2. Implement provider.v1 (if provisioning accounts/cards)#
Run gRPC server on e.g. {provider}-adapter:9100:3. Register settlement accounts#
Before live credits/debits/executes, seed finance DB:Customer accounts opened via the provider must have accounts.provider_id = 'currencycloud'.Without this mapping, CreditTransfer, DebitTransfer, and ExecuteTransfer fail with FAILED_PRECONDITION.4. Implement transfer webhook flow#
Typical outbound SEPA sequence:Adapter-initiated flow (provider drives timing):1.
Provider webhook: outbound payment requested
2.
Adapter → CreateTransfer (SEPA/SWIFT/ACH) with external_reference
3.
Adapter calls provider API to send funds
4.
Provider webhook: settled
5.
Adapter → ExecuteTransfer with settlement ref in metadata
Store mapping external_reference → transfer_id in adapter DB for idempotent webhook handling.5. Implement inbound credit flow#
Provider webhook: incoming wire / deposit detected1.
Resolve finance account_id (via IBAN mapping or external_id)
2.
Adapter → CreditTransfer with unique idempotency_key per provider event
3.
Default contra=ADMIN_CONTRA_SETTLEMENT mirrors funds from settlement account
6. Wire finance gRPC client (Go example)#
Reference implementation: crypto-service/internal/infrastructure/financeclient/client.go7. Test with grpcurl#
Port-forward finance gRPC (dev):Call (requires proto import paths or server reflection if enabled):Server reflection may not be enabled in production; use generated stubs or -proto / -protoset flags with grpcurl.
Default local provider (no adapter)#
Without PROVIDERS_CONFIG, finance uses the built-in local provider:Accounts: synthetic IBAN via NumberFactory + PostgreSQL sequence
Cards: mock last-four from BIN prefix
provider_id = "local", empty external_id
Settlement accounts seeded for local + EUR/USD
Use local provider for development; switch routing in YAML when adapter is ready.
identity.v1 - IdentityInternal#
Address: identity:9090
Proto: identity-service/proto/identity/v1/identity.proto| RPC | Purpose | Callers |
|---|
ValidateStepUpToken | Verify step-up MFA token + operation type | api-gateway plugin, finance REST |
GetUserContact | Email and phone for notifications | notification, compliance |
GetUserProfile | Name, address, customer type | notification, compliance |
ResolveUserID | Lookup user by email or phone | finance (P2P transfers) |
ValidateStepUpToken request:
compliance.v1 - ComplianceInternal#
Address: compliance:9090
Proto: compliance-service/proto/compliance/v1/compliance.proto| RPC | Purpose | Callers |
|---|
GetKYCStatus | KYC gate before financial operations | finance-service |
Finance checks operations_allowed during transfer pipeline execution. Adapters do not call compliance directly - finance enforces it.
Quick reference - all RPCs#
finance.v1.TransferService#
| RPC | Required fields |
|---|
CreateTransfer | idempotency_key, type, from_account_id, amount, currency, recipient, source_service, actor |
ExecuteTransfer | transfer_id, idempotency_key, actor |
CancelTransfer | transfer_id, idempotency_key, actor |
CreditTransfer | idempotency_key, account_id, amount, currency, source_service, actor |
DebitTransfer | idempotency_key, account_id, amount, currency, source_service, actor |
GetTransfer | transfer_id |
finance.v1.CardPaymentService#
| RPC | Required fields |
|---|
CreateCardPayment | idempotency_key, card_id, amount, currency, source_service, actor |
ReverseCardPayment | idempotency_key, original_transfer_id, actor |
finance.v1.FXRateService#
| RPC | Required fields |
|---|
UpsertFXRate | from_currency, to_currency, rate, source_service, actor |
BatchUpsertFXRates | rates[], source_service, actor |
ListFXRates | - |
finance.v1.TransactionStatementService#
| RPC | Required fields |
|---|
ListTransactionStatement | scope, from_date, to_date, source_service; plus account_id when scope is account, or user_id when scope is user |
provider.v1 (implemented by adapter)#
| Service | RPCs |
|---|
AccountProviderService | OpenAccount, CloseAccount |
CardProviderService | IssueCard, GetCardNumber, GetCardCVV, BlockCard, UnblockCard |
identity.v1.IdentityInternal#
| RPC | Required fields |
|---|
ValidateStepUpToken | token, user_id, operation_type |
GetUserContact | user_id |
GetUserProfile | user_id |
ResolveUserID | email or phone |
compliance.v1.ComplianceInternal#
| RPC | Required fields |
|---|
GetKYCStatus | user_id |
Modified at 2026-06-23 04:24:40