How the Client API Sandbox Simulator Works
The one setting that makes an environment a sandbox
Every host reads a single platform setting, IntegrationsMode, with three values:
- Real: production integrations (Numeral, the FX broker via the FIX connector, the screening
service, Auth0, SendGrid). - Mocked: the shipped inert baseline used by local development and as a fallback for hosts whose
simulated composition is not complete yet. - Simulated: the sandbox. Real simulators replace the external counterparties; everything a
client can observe still runs through the genuine domain code.
The sandbox environment (sbx), dev, and staging run ClientApi and the OutboxProcessor in
Simulated. A missing or invalid mode fails startup; nothing ever silently falls back.
What "simulated" means
The composition lives in src/Integrations.Simulators and works through three mechanisms:
1. Leaf simulators
Deterministic replacements for the outbound calls the domain makes:
- FX: a fixed rate table (GBPUSD 1.25, GBPEUR 1.15, EURUSD 1.10 plus derived inverses), quotes
issued by a seeded "Sandbox Broker" valid for 30 seconds, and a trade executor that accepts every
order unless the ClientApi internal reference carries theTRADE-failedtrigger. - Screening: inbound and outbound screening requests are acknowledged with a case id, exactly
like the real compliance system, with the verdict arriving asynchronously (mechanism 2). - Payment processor: payment initiation is accepted and issued simulator ids; the connector
status callbacks arrive asynchronously (mechanism 2). - Confirmation of Payee: deterministic answers driven by account-holder-name triggers
(COP-rejected*), results stored in the session cache so any replica can resolve them.
Integrations with no simulator yet (treasury payments, payment returns, Auth0
management operations, email, the reconciliation and fee publishers) are throwing stubs: they
fail loudly with NotSupportedException rather than silently pretending to work. When a simulated
lifecycle brushes one of these (for example the reconciliation publish after a payment settles), the
event dispatcher isolates that handler failure as a dead letter and the lifecycle continues. These
dead letters are expected, accepted sandbox noise.
2. Async verdict and acknowledgement handlers
In production, screening verdicts and payment-connector status updates arrive as service-bus
messages consumed by dedicated consumers. The sandbox has no service bus, so simulator-registered
domain event handlers stand in for those consumers and dispatch the same domain commands the
real consumers send:
ResolveInboundScreeningOnRequestedandResolveOutboundScreeningOnReadydeliver the screening
verdict (rejected when the payment reference carriesPAYMENT-rejected-compliance, accepted
otherwise).AcknowledgeOutboundPaymentOnInitiateddelivers the connector callbacks: sent-to-bank then
executed by default, or the outcome the reference trigger asks for (PAYMENT-rejected-bank,
PAYMENT-returned,PAYMENT-held).
Because these handlers drive the real command handlers, statuses, guards, ledger entries and fees
come from the production code paths, never from hand-written rows.
3. Scenario services and sandbox-only endpoints
Some things a client needs to simulate are not reactions to their own API calls but upstream events
(money arriving; a broker rejecting a booked order). Each of those is a scenario service plus one
endpoint under /v1/simulations/:
POST /simulations/inbound-paymentsdrives the real inbound-payment command chain the upstream
webhook would trigger, so sandbox accounts can be funded.POST /simulations/fx-trades/{id}/rejectrejects a booked trade the way a broker fill rejection
would.
The endpoints exist in every environment, but the scenario services are registered only in
Simulated mode; outside the sandbox the endpoint cannot resolve its scenario and returns 404 for
every request. There is deliberately no runtime mode check in the endpoints.
What a sandbox payment lifecycle looks like
POST /paymentscreates the payment directly in compliance approval (the Client API skips the
Portal approval stages) and the value-date gate runs: a future-dated payment parks as
Scheduled.- The balance check runs against the real ledger; an unfunded payment stops as
AwaitingFunds. - Screening is requested and the simulated verdict arrives: rejected (trigger) ends the payment
Rejected; accepted re-drives the lifecycle. - Initiation reaches the payment-processor simulator, the payment moves to processing, and the
simulated connector callbacks walk it toSent(default) or the triggered outcome. OnSent
the account is genuinely debited in the ledger.
In deployed environments the chain runs asynchronously through the outbox processor, so clients
poll the read endpoints; in the feature-test composition the same chain runs synchronously
in-memory.
Where behaviour is pinned
- The ClientApi feature tests boot the genuine Simulated composition (only messaging is swapped
to run in-memory), so every trigger and lifecycle outcome is pinned end to end in
tests/FeatureTests/Tests/ClientApi/. - The DI matrix (
tests/UnitTests/ArchitectureTests/DependencyInjectionTests.cs) proves the
simulated composition resolves completely for ClientApi and the OutboxProcessor, so a new
integration seam without a simulator or stub fails the build, not the sandbox.
How to extend the sandbox
- New leaf behaviour: replace the throwing stub with a real simulator, register it in
Integrations.Simulators/DependencyInjection.cswith a comment naming its consumer, and pin the
behaviour with ClientApi feature scenarios. The feature scenarios are the simulator's spec. - New failure or delay outcome: add a trigger constant to
SandboxOutcomeTriggers.csand act
on it in the relevant simulator or verdict handler. Triggers are deliberate, documented,
client-facing sandbox features; nothing test-shaped goes into the simulators. - New upstream event: one scenario service plus one endpoint, copying the inbound-payment
template (tenant scoping inside the scenario, structural 404 outside sandbox mode).
The authoritative engineering reference, kept in step with the code, is
ai-docs/patterns/integration-simulators.md.
Environments and access
- Public client-facing hosts:
api.sandbox.ibanq.com(sandbox) andapi.ibanq.com(production),
as published in the OpenAPI spec. Theservices-int.*hosts are internal. - Authentication, DPoP and request signing behave exactly as in production; only the simulation
endpoints skip request signing. - The sandbox database seeds a demo client ("Sandbox Demo Ltd", accounts
SBX-GBP-001,
SBX-EUR-001,SBX-USD-001with opening balances) on every deploy. API credentials for it
(Auth0 application, auth-group credential, signing key) are a manual setup step.
Updated 10 days ago