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 the TRADE-failed trigger.
  • 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:

  • ResolveInboundScreeningOnRequested and ResolveOutboundScreeningOnReady deliver the screening
    verdict (rejected when the payment reference carries PAYMENT-rejected-compliance, accepted
    otherwise).
  • AcknowledgeOutboundPaymentOnInitiated delivers 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-payments drives the real inbound-payment command chain the upstream
    webhook would trigger, so sandbox accounts can be funded.
  • POST /simulations/fx-trades/{id}/reject rejects 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

  1. POST /payments creates 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.
  2. The balance check runs against the real ledger; an unfunded payment stops as AwaitingFunds.
  3. Screening is requested and the simulated verdict arrives: rejected (trigger) ends the payment
    Rejected; accepted re-drives the lifecycle.
  4. Initiation reaches the payment-processor simulator, the payment moves to processing, and the
    simulated connector callbacks walk it to Sent (default) or the triggered outcome. On Sent
    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

  1. New leaf behaviour: replace the throwing stub with a real simulator, register it in
    Integrations.Simulators/DependencyInjection.cs with a comment naming its consumer, and pin the
    behaviour with ClientApi feature scenarios. The feature scenarios are the simulator's spec.
  2. New failure or delay outcome: add a trigger constant to SandboxOutcomeTriggers.cs and 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.
  3. 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) and api.ibanq.com (production),
    as published in the OpenAPI spec. The services-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-001 with opening balances) on every deploy. API credentials for it
    (Auth0 application, auth-group credential, signing key) are a manual setup step.

Did this page help you?