EUR 3P Payout via Schuman

📅 Last updated: 2026-05-11 👥 Audience: Saloni, Ishaan, Sagar Draft

Engineering reference for the EUR 3P Payout product. Covers the client-facing Platform API design and the Schuman Financial provider integration layer. Use this alongside the Schuman sandbox docs and Tyler's v1 Platform API spec.

What This Covers#

This docs site is the single engineering reference for the EUR payouts workstream. It covers two things:

  • Client-Facing API - the /payments resource as clients see it: endpoints, state machine, fee model, beneficiary validation, and webhooks.
  • Provider Integration - the internal Schuman abstraction layer: how client resources map to Schuman objects, provisioning sequence, payment lifecycle orchestration, polling service, balance proxy, and reconciliation.

Audience#

PersonRolePrimary sections
SaloniEngineering Lead (Primary audience)All sections. This doc is the single reference for Saloni's team to build against.
Ishaan NegiEngineeringProvider Integration, Payment Lifecycle, Polling Service
Sagar ChudamaniEngineeringAll sections - architecture sign-off

Product Context#

OpenFX is building a single API that lets clients fund in USDC, GBP, or USD, convert to EUR via OpenFX's trading engine, and pay a third-party beneficiary via SEPA - all in one instruction. Schuman Financial is the licensed entity executing the SEPA payout. OpenFX is the FX provider and technical integrator.

OpenFX controls the FX leg (1P trade between client and OpenFX). Schuman handles the payout leg (3P SEPA payment from the client's own EUR account at Schuman). OpenFX is never in the 3P payment flow of funds.

Build Target#

ItemDetail
First live payout1st week of June 2026
Day-one clientsFinmo, One.io, Mural, Rollafi
Funding flowsUSDC, GBP, USD (SWIFT)
PartnerSchuman Financial Infrastructure
Schuman APIPlatform API v1.1 (OAS 3.0) - 1P/3P module, currently POC status

What OpenFX Builds#

  • Payout instruction API (quote, confirm, execute)
  • FX conversion triggered by payout instruction (just-in-time)
  • EUR delivery to client's Schuman EUR IBAN
  • Payment instruction relay to Schuman (held until EUR lands in client's Schuman EUR IBAN account)
  • Status tracking (polling Schuman, delivering webhooks to client)
  • Balance visibility (USDC, GBP, USD, EUR) on OpenFX platform

What Schuman Provides#

  • Licensed SEPA payment infrastructure (Banking Circle)
  • IBAN per client (EUR account for payout execution)
  • 3P payout execution via POST /1p-3p/payment/3p/{customerId}
  • IBAN-to-name validation (Confirmation of Payee check)
  • Compliance and AML screening on the payout leg

What OpenFX Does NOT Do#

  • Touch the payment flow of funds. OpenFX's involvement ends when EUR lands in the client's Schuman account.
  • Compliance screening on the payout leg. Schuman handles this as the licensed entity.
  • Hold customer funds in EUR. EUR transits through the client's Schuman account, not OpenFX.

Fund Flow

All EUR payout funding flows follow a two-leg pattern: a 1P FX leg (OpenFX controls) and a 3P payout leg (Schuman controls). The two patterns are just-in-time FX-funded and collection-funded.

Universal Flow (FX-Funded)#

All three funding currencies (USDC, GBP, USD) follow this sequence. The only variable is what the client sends to OpenFX's trade engine in step 4.

1
Client sends payout instruction
Client provides beneficiary IBAN, payout amount, and funding currency to OpenFX.
2
OpenFX returns quote
FX rate, fee breakdown, net amount to beneficiary. Quote has an expiry window (5-10 minutes).
3
Client confirms quote
POST /payments/{id}/submit - triggers execution pipeline.
4
Client funds trade engine
USDC (from pre-funded balance), GBP (FPS/CHAPS/SWIFT), or USD (Fedwire/SWIFT).
5
OpenFX executes FX conversion
USDC/GBP/USD sold for EUR via OpenFX's trade engine. cnv_ record created.
6
OpenFX delivers EUR to client's Schuman IBAN
EUR sent to the VIBAN associated with the client's acc_. OpenFX exits the fund flow here.
7
OpenFX polls balance until EUR lands
GET /1p-3p/balance/{customerId} - polled on schedule. Payout instruction held until balance confirms. Payment in pending_funding internal stage.
8
OpenFX submits payment instruction to Schuman
POST /1p-3p/payment/3p/{customerId} with beneficiary IBAN, name, amount, reference, and SEPA type.
9
Schuman executes SEPA credit transfer
Payment sent from client's EUR IBAN to beneficiary. SCT settles D+1; SEPA Instant settles within seconds.
10
OpenFX polls transaction status, delivers webhook
GET /1p-3p/transactions/{customerId}/{transactionId} polled to terminal state. On completion, fires payment.completed webhook to client.
💡
Key handoff: Steps 6-8 are the most important engineering problem. OpenFX must deliver EUR, poll until it lands, and only then submit the payout instruction. If EUR delivery fails or is delayed, the payment enters pending_funding state. The client's FX conversion has already executed, so the client holds EUR exposure until the payout completes.

Just-in-Time Funding Design Principle#

For FX-funded payouts, the trade is only placed when a payout instruction is made. The sequence is: instruction then trade then EUR delivery then payout execution. EUR arrives and is immediately paid out. This means OpenFX does not pre-fund EUR positions or hold EUR on behalf of clients.

Collection-Funded Flow#

A client may collect EUR from their own customers (invoices, marketplace settlements) into their Schuman EUR IBAN. That balance sits in the account until the client initiates a payout instruction against it. No FX conversion occurs.

1
EUR accumulates in client's Schuman IBAN
Client's customers pay EUR invoices or marketplace settlements into the client's Schuman IBAN. This is independent of payout instructions.
2
OpenFX checks EUR balance
GET /1p-3p/balance/{customerId} - confirms sufficient EUR available. No FX conversion needed.
3
OpenFX submits payment instruction
POST /1p-3p/payment/3p/{customerId} - same as FX-funded flow, step 8.
4
Schuman executes SEPA transfer, OpenFX polls and delivers webhook
Same as FX-funded flow steps 9-10. Payment object has exchangeRate: null and no cnv_ record.
For collection-funded payouts, the state machine skips fx_executing and funding_schuman, going straight from confirmed to submitted_to_schuman.

Schuman API Endpoints Referenced#

StepSchuman API CallPurposeNotes
Pre-checkPOST /1p-3p/iban/validationVerify IBAN-to-name match (CoP)Call before FX conversion. Reject mismatches early.
Balance checkGET /1p-3p/balance/{customerId}Confirm EUR has landed in IBANPoll until balance >= payout amount
ExecutePOST /1p-3p/payment/3p/{customerId}Submit 3P SEPA payoutIBAN must match provided beneficiary name
StatusGET /1p-3p/transactions/{customerId}/{transactionId}Poll payout statusNo webhook from Schuman - must poll

Other Useful Schuman Endpoints#

EndpointUse
GET /1p-3p/status/{customerId}Check if 1P/3P is enabled for the client
GET /1p-3p/fda-viban/{customerId}Get the client's IBAN details (needed for EUR delivery addressing)
GET /1p-3p/transactions/{customerId}List all 1P/3P transactions for reconciliation
GET /transfer/{customerId}/bank-transfersFiat transfer history (reconciliation against EUR deliveries)
GET /address/{customerId}/allowed-chainsSupported blockchains (if USDC receipt via Schuman ever needed)
API constraints: Rate limits unknown - must be confirmed during sandbox testing. Schuman enforces CoP: rejects 3P payouts where IBAN does not match the provided name. Pre-validate via POST /1p-3p/iban/validation before submitting payouts.

Resource Model

EUR payouts use the unified /payments resource with the sepa_bank payment method type. The platform API is aligned with Tyler McIntyre's v1 Platform API architecture. Six core resources are involved, each with a typed ID prefix for traceability.

Core Resources#

ResourcePrefixWhat it representsEUR payout example
Accountacc_A client's money account (type: demand_deposit)Client's Schuman EUR IBAN, client's USDC balance on OpenFX
Account Numberan_The identifier on an account (IBAN, wallet address)The EUR IBAN string itself
Counterpartycpt_Beneficiary identity shell (name, address, jurisdiction)The 3P beneficiary receiving EUR
Payment Methodpm_Rail-specific delivery details on a counterpartyType sepa_bank with iban + optional bic. Covers both SCT and SEPA Instant (rail variant selected per-payment via sepaType, not per-payment-method).
Paymentpmt_The payment instruction and its lifecycleThe EUR payout object
Conversioncnv_FX conversion recordUSDC/GBP/USD to EUR trade
Quoteqte_A locked rate offerFX rate + fees, valid for confirmation window
Transactiontxn_Ledger entryEUR credit to Schuman IBAN

Money Format#

All monetary amounts use string-based decimal values:

{ "currency": "EUR", "value": "10000.00" }

This avoids floating-point issues and is consistent across all amount fields: source, destination, fees, and balances.

SEPA Variant#

The sepaType field on the payment specifies the SEPA variant:

sepaType valueRailSettlement speed
credit_transferStandard SCTD+1
instantSEPA InstantSeconds

Both use the same endpoints, state machine, and webhook events. The difference is settlement speed and potentially fee. SEPA Instant availability on client VIBANs to be confirmed with Schuman during sandbox testing.

Endpoints

EUR payouts use three payment creation patterns. The staged intent flow is the default for EUR payouts because clients need to review the FX quote and CoP result before committing.

Payment Creation Patterns#

PatternEndpointsUse caseEUR payout fit
Staged intent POST /payments/intents then POST /payments/{id}/submit Client gets a preview (quote, fees, CoP result), reviews, then confirms Primary. Maps to the current quote-then-confirm flow.
Direct with quote POST /payments with use_quote: true Lock rate and execute in one call Repeat payments to known beneficiaries where CoP has already passed.
Direct at market POST /payments with at_market: true Execute immediately at current rate No review step. Client accepts market rate at execution time.

Staged Intent Flow (Primary)#

StepEndpointPurposeKey fields
1. Create intent POST /payments/intents Get quote with FX rate, fees, CoP result. Returns qte_ with expiresAt. sourceAccountId (acc_), counterpartyId (cpt_), paymentMethodId (pm_), amount, currency, amountInputSide (send or receive), sepaType: credit_transfer or instant
2. Submit POST /payments/{id}/submit Confirm and trigger execution (FX conversion + payout). Returns pmt_ in processing state. Payment ID. For close_match or unavailable CoP results, must include acknowledgement.
3. Get status GET /payments/{id} Current payment state, processing stage, tracking identifiers. Payment ID
4. Get timeline GET /payments/{id}/timeline Chronological audit trail of all state transitions and events. Payment ID
5. Cancel POST /payments/{id}/cancel Cancel before FX executes. Only valid in created state. After processing begins, cancellation is no longer possible. Payment ID
Quote validity: The intent response includes expiresAt. This is a payment quote (FX + delivery + compliance), not a bare FX quote. Validity window should be 5-10 minutes to allow for detail review and CoP acknowledgement. Higher spread compensates for the extended rate-lock cost. TBC with Core Trading.

Cross-Currency Fields#

amountInputSide controls quoting direction:

  • send - client specifies the source amount (funding currency)
  • receive - client specifies the destination amount (EUR to beneficiary)

This replaces the previous source_amount / destination_amount split.

Beneficiary Validation#

EndpointPurposeNotes
POST /counterparties/{id}/payment-methods/{id}/validate Trigger VoP/CoP check against Schuman POST /1p-3p/iban/validation Call before creating a payment intent.

Balances#

EndpointPurposeNotes
GET /accounts/{accountId}/balances Four-bucket balance model: available, pending, held, total Two checks per payout.

Balance check 1 (at payment creation): Verify the client has sufficient funding currency (USDC/USD/GBP) in their OpenFX account. This is the available balance on the funding acc_.

Balance check 2 (before payout submission to Schuman): Verify EUR has landed in the client's Schuman IBAN. Proxied from Schuman's GET /1p-3p/balance/{customerId} and mapped into the four-bucket model on the EUR acc_.

Idempotency#

All POST endpoints require an Idempotency-Key header (client-generated UUID, unique per client, valid for 48 hours). On duplicate submission, the API returns the original response without re-executing.

Idempotency is critical for POST /payments/{id}/submit. Duplicate confirmations must not trigger duplicate FX trades.

Engineering owns the detailed request/response schemas. The fund flow defines what data is needed at each step.

Fee Model

Fees are returned in the intent response as a feeComponents array with typed entries, plus a totalDebitAmount showing what the client actually pays (source amount + all fees).

Fee Types#

Fee typeDescription
fx_markupFX spread component
processing_feeOpenFX processing fee
sepa_feeSEPA rail fee (pass-through from Schuman, if applicable). May differ between credit_transfer and instant.

Response Structure#

{
  "feeComponents": [
    { "type": "fx_markup", "amount": { "currency": "EUR", "value": "12.50" } },
    { "type": "processing_fee", "amount": { "currency": "EUR", "value": "2.00" } },
    { "type": "sepa_fee", "amount": { "currency": "EUR", "value": "0.50" } }
  ],
  "totalDebitAmount": { "currency": "USD", "value": "10015.00" }
}

The feeComponents array is extensible for future fee types.

Fee structure and amounts TBC with Dan/pricing. Schuman's per-transaction payout fee is not yet confirmed - this must be locked before go-live pricing can be finalized.

Payment State Machine

The client sees a simplified state machine. Internal orchestration granularity is hidden behind processing. Internal stages are exposed via processingStage on the payment object and via timeline events.

Client-Facing States#

StateDescriptionTrigger to next state
createdIntent created, quote returned, awaiting client submissionPOST /payments/{id}/submit
requires_actionClient action needed (CoP acknowledgement, RFI from Schuman compliance). requiresActionReason specifies what.Client resolves the action
processingPayment is executing. Internal stages (FX, EUR delivery, Schuman submission) hidden from client.Schuman confirms delivery
in_reviewSchuman AML/compliance screening in progress. Client cannot act, only wait. Depends on Schuman exposing a compliance hold status via GET /1p-3p/transactions - unconfirmed.Schuman clears or escalates
completedBeneficiary received EUR-
failedAny failure (FX, funding, Schuman rejection, SEPA failure)-
canceledClient cancelled before FX executionPOST /payments/{id}/cancel from created
returnedSEPA credit transfer returned by beneficiary's bank post-completionBeneficiary bank returns funds (e.g., account closed)

requiresActionReason Enum#

ReasonWhen it firesClient must...
beneficiary_verification_requiredCoP result is close_match or unavailableAcknowledge the mismatch/unavailability and resubmit
rfi_pendingSchuman compliance requests additional info (beneficiary docs, source of funds)Submit requested information via OpenFX. Depends on Schuman providing an RFI submission mechanism - unconfirmed.
quote_expiredClient took too long to submitRequest a new intent

Internal Processing Stages#

Not client-facing. Exposed via processingStage on the payment object and via timeline events for clients that want granular visibility.

processingStageMaps to client stateWhat is happening
fx_executingprocessingFX trade in progress
funding_schumanprocessingEUR delivery to client's Schuman IBAN in progress
pending_fundingprocessingWaiting for EUR to land in IBAN (GET /1p-3p/balance polling)
submitted_to_schumanprocessingPayout instruction sent to Schuman, awaiting execution
schuman_compliance_holdin_reviewSchuman AML/compliance check in progress
For collection-funded payouts (no FX leg), processingStage skips fx_executing and funding_schuman, starting at submitted_to_schuman.

Tracking & Reconciliation

Every payment object carries a trackingIdentifiers object for end-to-end traceability across all three legs. The three-leg reconciliation model links FX conversion, EUR funding, and Schuman payout into a single traceable chain.

Tracking Identifiers#

FieldSourcePurpose
trackingIdentifiers.endToEndIdSEPA end-to-end IDTraces the SEPA credit transfer through the banking network
trackingIdentifiers.providerTransactionIdSchuman transaction IDLinks to Schuman's GET /1p-3p/transactions records
exchangeRate.conversionIdcnv_ from OpenFX trade engineLinks to the FX conversion record (null for collection-funded payouts)

Three-Leg Reconciliation Model#

Each EUR payout has three legs that must be traceable:

LegResourceHow it links to the payment
1. FX Conversioncnv_exchangeRate.conversionId on the payment object
2. EUR Fundingtxn_Ledger entry for EUR delivery to Schuman IBAN, visible in timeline events
3. Schuman PayoutSchuman transactiontrackingIdentifiers.providerTransactionId and trackingIdentifiers.endToEndId
Ledger dependency: The txn_ linking in leg 2 assumes a ledger exists. OpenFX does not currently have a ledger. For MVP with four clients and low volume, the per-transaction reference model (payout_id, trade_id, schuman_transaction_id) provides an interim audit trail. This gap is being addressed through the Embed acquisition and a multi-currency ledger effort. The Schuman payout integration should be designed so that ledger entries can be retrofitted without rearchitecting the reconciliation flow.

Reconciliation Surfaces#

Two reconciliation surfaces:

  1. OpenFX ledger vs. Schuman IBAN balance: Every EUR delivery to Schuman must match a corresponding debit on OpenFX's side. Use GET /transfer/{customerId}/bank-transfers for Schuman's view.
  2. Payout instructions vs. Schuman execution: Every POST /1p-3p/payment/3p must result in a matching transaction in GET /1p-3p/transactions/{customerId}.

Beneficiary Validation

Pre-validate beneficiaries before payment creation via VoP/CoP. Validation lives on the counterparty/payment-method path and wraps Schuman's IBAN validation API. This runs before FX conversion - invalid IBANs are rejected before any money moves.

Endpoint#

POST /counterparties/{cpt_id}/payment-methods/{pm_id}/validate

Wraps Schuman's POST /1p-3p/iban/validation.

Validation Sequence#

1
IBAN format check (ISO 13616)
Local validation. No Schuman call.
2
IBAN-to-name match (CoP/VoP)
Calls Schuman POST /1p-3p/iban/validation. EU VoP regulation mandatory from Oct 2025.
3
Sanctions screening
Schuman handles as part of their compliance.

CoP Result Handling#

CoP ResultMeaningOpenFX handling
matchIBAN and name match exactlyProceed. No client action needed.
close_matchName is similar but not identical (e.g., "ABC Ltd" vs "ABC Limited")Payment moves to requires_action with reason beneficiary_verification_required. Client must explicitly acknowledge before proceeding.
no_matchName does not match account holderReject payment. Client must correct beneficiary details on the counterparty/payment-method.
unavailableReceiving bank does not support VoP, or check timed outPayment moves to requires_action with reason beneficiary_verification_required. Client must explicitly acknowledge the risk before proceeding.
CoP result is surfaced in the validate response and persisted on the payment object. For close_match and unavailable, the POST /payments/{id}/submit call requires the client to have resolved the requires_action state. Eng to design the acknowledgement mechanism (likely via POST /payments/{id}/actions).

Webhooks

Webhook event naming follows the platform convention: payment.{state}. Webhook payload always includes the full payment object. Clients should not need to poll after receiving a webhook.

Event Reference#

EventFires whenKey payload fields
payment.createdIntent created, quote returnedPayment object with quote details, feeComponents, exchangeRate
payment.requires_actionCoP acknowledgement needed, or Schuman RFIrequiresActionReason, action details
payment.processingClient submits, execution beginsPayment object with processingStage
payment.completedBeneficiary received EURFull payment object with timestamps, trackingIdentifiers
payment.failedAny failure stateError code, processingStage at failure, suggested remediation
payment.canceledClient cancels paymentPayment object with cancellation timestamp
payment.returnedSEPA transfer returned post-completionPayment object with return reason code, original completion timestamp
No webhooks from Schuman: Schuman's current API has no push notifications. OpenFX must poll Schuman for status changes and translate them into webhook deliveries to clients via Tyler's webhook infrastructure. Webhook support from Schuman is an open item - being explored for a future improvement.

Resource Mapping

How each client-facing resource maps to Schuman's API objects. Key point: Schuman has no concept of counterparties or payment methods. It receives raw beneficiary details (IBAN, name) on each payout call. OpenFX owns the identity layer entirely.

Provider Abstraction Principle#

Schuman is never exposed to API consumers. Clients interact exclusively with Tyler's unified /payments resource. Schuman is one provider behind the SEPA rail.

This abstraction exists for one reason: when OpenFX's NL PI licence transfers (via the Embed acquisition), EUR volume migrates off Schuman onto owned infrastructure. The provider layer swaps out. The client-facing API does not change. No client migration, no breaking changes, no version bump.

Every Schuman-specific integration must be designed as swappable. No Schuman concepts, IDs, or terminology should leak into the client-facing API surface. The provider interface for Schuman is implemented behind Schuman's EMI entity, Salvus SAS.

Design rule: Every piece of Schuman-specific logic - API calls, status mappings, polling cadences, balance construction - must be isolated behind the provider interface. No Schuman assumptions should be baked into the orchestration layer or the client-facing API.

Mapping Table#

Client-Facing ResourceID PrefixSchuman EquivalentNotes
Account (EUR IBAN) acc_ Schuman customerId + VIBAN from POST /address/{customerId}/viban Client's Schuman EUR IBAN is surfaced as a first-class acc_ with type demand_deposit and an_ account number (the IBAN itself)
Counterparty cpt_ No Schuman equivalent Beneficiary identity is an OpenFX-only construct. Schuman receives beneficiary details (IBAN + name) inline per-transaction.
Payment Method pm_ No Schuman equivalent sepa_bank type with iban + optional bic. Stored in OpenFX, passed to Schuman at payout time.
Payment pmt_ Schuman transactionId from POST /1p-3p/payment/3p/{customerId} OpenFX pmt_ wraps the full lifecycle (FX + funding + Schuman payout). Schuman only knows about the final payout leg.
Conversion cnv_ No Schuman equivalent FX conversion is internal to OpenFX's trade engine. Schuman has no visibility into FX.

ID Mapping to Store#

OpenFXSchuman
entityIdcustomerId
acc_ (EUR account)Schuman VIBAN

Provisioning

When a new client is activated for EUR payouts, Tyler's Layer 0 (Identity/Entities) provisioning triggers the following Schuman calls in sequence. This is a one-time setup per client.

Onboarding is manual in v1. Schuman KYC/AML is handled offline before these API calls are made. Provisioning APIs are called only after Schuman compliance approves the client.

Four-Step Provisioning Sequence#

1
POST /customer
Create B2B customer record in Schuman. OpenFX collects onboarding docs manually and submits to Schuman compliance team before this call is made.
2
POST /customer/{customerId}/tc-acceptance
Record T&C acceptance. Client must accept Schuman's T&Cs (surfaced via OpenFX).
3
POST /address/{customerId}/viban
Create EUR VIBAN. This becomes the client's acc_ with the IBAN stored as an_. This is the account that holds EUR for payouts.
4
PATCH /1p-3p/activate/3p/{customerId}
Enable 3P payment capability. 3P is off by default and must be explicitly activated.
Failure at any step should halt the sequence and alert ops. Do not partially provision a client.

Manual Onboarding Flow (v1)#

StepWhoWhat
1OpenFXCollect required docs from client (KYB, corporate docs, UBO declarations)
2OpenFX opsSubmit docs to Schuman compliance team
3SchumanApproves client. OpenFX calls provisioning APIs (four-step sequence above)
4OpenFXClient's IBAN is created and whitelisted for EUR delivery
5ClientActivates Send on OpenFX platform

Target: Client goes from "I want EUR payouts" to "first payout executed" within 5 business days, assuming docs are ready.

Payment Lifecycle

Orchestration logic for payment execution. Two flows: staged intent (FX-funded, primary) and collection-funded. Both converge at Schuman payout submission.

Staged Intent Flow (Primary)#

Standard flow: client holds a non-EUR balance and wants to pay out EUR via SEPA.

Step 1 - Create Intent#

POST /payments/intents

  • OpenFX generates a quote (qte_)
  • Check 1: Verify available balance on funding acc_ covers the source amount
  • Returns preview: FX rate, fee breakdown, net amount to beneficiary
  • No Schuman call. Nothing is committed.

Step 2 - Submit Payment#

POST /payments/{id}/submit

Triggers the execution pipeline in sequence:

Sub-stepActionNotes
2aFX conversion via trade enginecnv_ created. Source currency sold for EUR.
2bEUR delivery to client's Schuman IBANEUR sent to the VIBAN associated with the client's acc_.
2cBalance pollingGET /1p-3p/balance/{customerId} - poll until EUR balance reflects the expected amount. Check 2: Confirm EUR has landed before proceeding.
2dPayout submissionPOST /1p-3p/payment/3p/{customerId} with beneficiary IBAN (from pm_), beneficiary name (from cpt_), amount (EUR), payment reference, and SEPA type.
2eTransaction status pollingGET /1p-3p/transactions/{customerId}/{transactionId} - poll until terminal state (completed, failed, rejected, returned).
SEPA type mapping: credit_transfer (SCT, D+1) or instant (seconds). Mapped from the sepaType field on the client-facing payment. TBC whether Schuman's API accepts this as a parameter or if instant requires a different endpoint/flag.

Collection-Funded Flow#

Client already holds EUR in their Schuman IBAN (from a prior pay-in or collection). Skip steps 2a-2c:

  • Check 2: GET /1p-3p/balance/{customerId} to confirm sufficient EUR available
  • Payout submission (step 2d)
  • Transaction status polling (step 2e)

No FX conversion, no cnv_ created. The payment object's exchangeRate field is null.

VoP/CoP Validation#

Triggered by: POST /counterparties/{cptId}/payment-methods/{pmId}/validate

Internally calls: POST /1p-3p/iban/validation (Schuman)

Schuman VoP ResultClient-Facing ResultPayment Effect
MatchmatchPayment proceeds normally
Close matchclose_matchPayment enters requires_action with requiresActionReason: beneficiary_verification_required
No matchno_matchPayment blocked. Client must correct beneficiary details.
UnavailableunavailablePayment enters requires_action with requiresActionReason: beneficiary_verification_required

When CoP acknowledgement is required, the client must explicitly acknowledge the mismatch via POST /payments/{id}/actions before the payment can proceed.

Internal State Machine

Full mapping from client-facing state to OpenFX internal stage to Schuman API state. The client sees simplified states; OpenFX tracks granular processing stages internally.

Schuman's exact status values from GET /1p-3p/transactions are unconfirmed. The mapping below uses assumed values based on POC observations. Must be validated during sandbox testing (Week 1 priority).

Full State Mapping#

Client-Facing StateInternal StageSchuman API StateTrigger
createdintent_created(none)POST /payments/intents
requires_actionawaiting_cop_ack(none)VoP returns close_match or unavailable
processingfx_executing(none)POST /payments/{id}/submit
processingfx_completed(none)Trade engine confirms conversion
processingfunding_schuman(none)EUR delivery initiated to VIBAN
processingpending_funding(none)Polling GET /1p-3p/balance for EUR arrival
processingsubmitted_to_schumanpending / processingPOST /1p-3p/payment/3p returns
processingschuman_processingSchuman processing statePolling GET /1p-3p/transactions
in_reviewschuman_compliance_holdCompliance hold (TBC)Schuman flags compliance review
completedcompletedcompleted / settledSchuman confirms execution
failedfx_failed(none)Trade engine failure
failedfunding_failed(none)EUR delivery failure
failedpayout_failedfailed / rejectedSchuman rejects payout
canceledcanceled(none)Client cancels before FX execution
returnedreturnedreturnedBeneficiary bank returns funds

Balance Proxy

Tyler's v1 API exposes a four-bucket balance model. Schuman's balance API returns a single figure. OpenFX constructs the four buckets by overlaying internal state on Schuman's reported balance.

Four-Bucket Construction#

BucketSourceDescription
availableGET /1p-3p/balance/{customerId}Actual available EUR as reported by Schuman
pendingOpenFX internal stateEUR deliveries in transit - known to OpenFX but not yet reflected in Schuman's balance
heldOpenFX internal stateEUR earmarked for submitted-but-not-yet-completed payouts
totalComputedavailable + pending + held

Why OpenFX Must Maintain Its Own View#

Schuman only reports a single balance number. It does not distinguish between available, pending, or held. OpenFX tracks pending (EUR deliveries in flight) and held (EUR committed to active payouts) based on its own internal state machine. The available bucket comes from Schuman. The rest is OpenFX's overlay.

Polling Cadence for Balance Refresh#

TBD during sandbox testing. Balance updates from Schuman may have latency. Initial assumption: poll on demand (when needed for a payment check) rather than continuously.

Polling Service

Schuman has no webhooks. OpenFX must poll for two things: balance arrival after EUR delivery, and transaction status after payout submission. Both loops are designed as a shared polling service.

Balance Polling (Post-EUR-Delivery)#

Endpoint: GET /1p-3p/balance/{customerId}

Purpose: Detect when EUR lands in the client's Schuman IBAN after FX conversion and delivery.

WindowFrequency
First 5 minutesEvery 30 seconds
Minutes 5-60Every 2 minutes
After 60 minutesEvery 10 minutes
Timeout4 hours - escalate to ops if EUR has not arrived

Transaction Status Polling (Post-Payout-Submission)#

Endpoint: GET /1p-3p/transactions/{customerId}/{transactionId}

Purpose: Track payout through to terminal state.

WindowFrequency
First 5 minutesEvery 30 seconds
After 5 minutesEvery 2 minutes
Timeout24 hours (SEPA Credit Transfer settles D+1)

Shared Polling Service Design#

Design both polling loops as a shared polling service rather than ad-hoc per-payment logic.

Responsibilities:

  • Manage poll schedules per payment (register, tick, deregister on terminal state)
  • Handle Schuman rate limits (back off if rate-limited)
  • Emit internal events when state changes are detected
  • Internal events trigger webhook delivery to clients via Tyler's webhook infrastructure
  • Track and log all poll attempts for debugging
The polling service is the bridge between Schuman's pull-based model and Tyler's push-based webhook model. When OpenFX moves to owned infrastructure, the polling service may be replaced by direct event feeds if webhooks are available.

Reconciliation Linkage

Every EUR payout has up to three legs. All three must be traceable from the pmt_ object. This section defines the identifier chain and how to reconcile against Schuman.

Three-Leg Tracing#

LegIdentifierSourceExposed Via
FX Conversioncnv_OpenFX trade engineexchangeRate.conversionId on payment object
EUR Fundingtxn_OpenFX ledger entryTimeline event with txnId
Schuman PayoutSchuman transactionIdGET /1p-3p/transactionstrackingIdentifiers.providerTransactionId on payment object
SEPA End-to-EndendToEndIdSEPA credit transfer referencetrackingIdentifiers.endToEndId on payment object
Collection-funded payouts: FX Conversion leg is absent. exchangeRate is null. Only two legs exist (EUR Funding + Schuman Payout), or just one (Schuman Payout) if EUR was already sitting in the IBAN.

Cross-Reconciliation Against Schuman#

Schuman EndpointReconciles Against
GET /transfer/{customerId}/bank-transfersEUR funding txn_ entries in OpenFX's ledger
GET /1p-3p/transactions/{customerId}pmt_ records using providerTransactionId

Fee Passthrough

Fees are determined at quote time and locked into the payment object. All three fee components appear in the feeComponents array.

Fee Component Sources#

Fee ComponentSourceDetermined AtDescription
fx_markup OpenFX spread (bps) Quote time (POST /payments/intents) FX margin on the currency conversion
processing_fee OpenFX platform fee Quote time OpenFX's per-transaction processing fee
sepa_fee Schuman payout fee Quote time (fixed per-transaction, TBC) Schuman's fee for executing the SEPA credit transfer

Total Debit Calculation#

totalDebitAmount = source amount + fx_markup + processing_fee + sepa_fee
Schuman's per-transaction payout fee is not yet confirmed. This must be locked before go-live pricing can be finalized. See Open Items.

Migration Path

When OpenFX's NL PI licence transfers via the Embed acquisition, EUR volume migrates off Schuman. The provider abstraction makes this a backend swap, not a client migration.

What Stays the Same#

  • Client-facing API: Zero changes. Same POST /payments, same state machine, same webhooks, same balance model.
  • Reconciliation: Three-leg model stays. "Schuman Payout" leg becomes "OpenFX Payout" with direct SEPA access.
  • Fee structure: Same feeComponents array, same fields. Underlying cost may change.

What Changes#

ComponentChange
Provider layer Schuman integration swapped for owned-infrastructure integration. All Schuman-specific code is behind the provider interface.
Account migration Client EUR IBANs move from Schuman VIBANs to OpenFX's own banking partner. acc_ IDs may change (or be remapped). Clients must be notified.
Polling service May be replaced by direct event feeds from owned infrastructure (no more polling if webhooks are available).
Balance proxy May simplify if owned infrastructure provides multi-bucket balances natively.

Design Implication#

Every piece of Schuman-specific logic (API calls, status mappings, polling cadences, balance construction) must be isolated behind the provider interface. No Schuman assumptions should be baked into the orchestration layer or the client-facing API. This is not optional - it is the enabling condition for a zero-client-impact migration.

Schuman is an explicitly interim partner, not a long-term dependency. Design accordingly from day one.

Sandbox Validation

Week 1 items to confirm during Schuman sandbox testing. These are blockers for the build phase - each item affects implementation decisions.

The Schuman 1P/3P module is currently POC status. Sandbox access is available. All items below must be resolved before Week 2-3 build work begins.

Week 1 Items#

#ItemWhy It Matters
1 Confirm Schuman's exact transaction status values from GET /1p-3p/transactions Needed to finalize the internal state machine mapping (currently using assumed values from POC observations)
2 Confirm balance API latency after EUR delivery Determines polling cadence for balance checks. If latency is high, clients wait longer in pending_funding state.
3 Confirm Schuman's compliance hold behavior and how it surfaces in the API Needed to implement in_review client state. Currently unconfirmed whether GET /1p-3p/transactions exposes a compliance hold status.
4 Confirm rate limits on balance and transaction polling endpoints Required for polling service design. No batch endpoint exists - burst rate determines max concurrent clients supported.
5 Confirm Schuman's endToEndId generation and format Required for the SEPA end-to-end tracing model. If Schuman generates the endToEndId, OpenFX must capture it from the payout response.
6 Confirm per-transaction payout fee from Schuman Blocks go-live pricing. The sepa_fee component in feeComponents is currently TBC.
7 Confirm SEPA Instant availability on VIBANs, how to request instant vs SCT via the API, and any fee differential or amount caps Determines whether sepaType: instant is launchable in v1 or deferred. Affects API design for the sepaType parameter mapping to Schuman.

Full Open Questions (from PRD)#

ItemPriorityOwnerNotes
Schuman 1P/3P production timelineP0Rushil + SchumanModule is POC. When GA? Breaking changes? SLA?
EUR delivery mechanismP0Eng + RushilHow does EUR get from OpenFX to client's IBAN? SEPA transfer? Does OpenFX open its own Schuman account for internal book transfer?
Schuman API rate limitsP0Eng (sandbox test)No batch endpoint. What's the max burst rate for single payout calls?
Tripartite agreementP0Katherine / LegalClient-OpenFX-Schuman agreement. Not yet drafted. Blocks client activation. INR precedent exists.
Schuman 3P payout pricingP0Rushil + DanDoes EUROP fee schedule apply, or separate? Negotiate combined volume.
Ledger dependency for reconciliationP0Rushil + NathanOpenFX does not currently have a ledger. Interim per-transaction reference model works for MVP. Embed acquisition and multi-currency ledger effort addressing separately. Confirm timeline and ensure Schuman integration is ledger-compatible when it lands.
Webhook support from SchumanP1Rushil + SchumanCurrent API has no push notifications. Can they add?
Mint-back on IBANsP1Rushil + SchumanAuto-sweep to EUROP could drain payout balances. How to exclude? Less critical with just-in-time funding model.
FX rate lock durationP1Rushil + Alex RowlesWhat quote validity window does the trading engine support?
Compliance hold and RFI via APIP1Rushil + SchumanDoes GET /1p-3p/transactions return a status indicating compliance hold? Can Schuman expose an endpoint for submitting RFI responses? Needed to support in_review and requires_action states.
SEPA Instant via Banking CircleP1Rushil + SchumanConfirm: (1) is SEPA Instant available on client VIBANs, (2) is there a fee differential, (3) any amount caps or country restrictions on instant.
On this page