Skip to content

Inter-module interface contracts

Synchronous (request/response) contracts between modules. These are the highest-coupling interfaces in the system — a schema mismatch here breaks the critical payment and onboarding paths.

Architecture constraints (from ADR-001, ADR-029 (superseded by ADR-051 — see ADR-051 for current EventBridge bus naming convention), ADR-036):

  • All synchronous calls are Lambda-to-Lambda invocations within the same VPC, mediated by MOD-075 (internal API gateway) for cross-domain calls.
  • Intra-domain calls (same system domain, same repo) may invoke directly without the API gateway.
  • JWT validation via MOD-044 is required on every call crossing a trust boundary. Internal Lambda-to-Lambda calls within a domain use IAM role trust; no JWT required.
  • All money fields are fixed-point decimal strings — never floats.
  • Every request must carry an idempotency_key. Callee is responsible for deduplication.
  • Error responses follow a standard envelope (see Error envelope).

Related: Event catalogue · Data contracts · ADR-029 (superseded by ADR-051 — see ADR-051 for current EventBridge bus naming convention) · ADR-036


Error envelope

All error responses across all interfaces return this structure. HTTP status follows the semantic of the error type.

{
  "error_code": "SANCTIONS_HOLD",
  "error_message": "Payment blocked — active sanctions match on source account.",
  "request_id": "b12c3d4e-...",
  "idempotency_key": "pay-...",
  "retryable": false
}
Field Type Notes
error_code string Stable machine key — used for routing and analytics
error_message string Human-readable operator message
request_id uuid Matches the inbound request
idempotency_key string Matches the inbound idempotency key
retryable boolean false for validation failures; true for transient infra errors

SD01 ↔ SD04: Core Banking and Payments

MOD-020 → MOD-001: payment validation result to posting request

Direction: Synchronous. MOD-020 (pre-payment validation) calls MOD-001 (posting engine) once all validation checks pass. No posting occurs without a prior validated payment. Protocol: Lambda invocation — SD04 calls SD01 via MOD-075 (internal API gateway). SLA: p99 ≤ 10ms for posting commit (NFR-012). Combined validation + posting must complete within p99 ≤ 500ms to meet payment initiation SLA. Idempotency: MOD-001 deduplicates on idempotency_key. A second call with the same key returns the original posting result without re-posting.

Request (MOD-020 → MOD-001)

POST /internal/v1/postings
Field Type Required Description
idempotency_key string Caller-supplied; tied to the payment_id
payment_id uuid Source payment reference
posting_type string PAYMENT / FX_CONVERSION / ADJUSTMENT / REVERSAL
entries array Balanced debit/credit pair; see entry shape below
validation_reference uuid MOD-020 validation run ID — recorded in audit
requested_at ISO8601

Entry shape:

Field Type Required Description
account_id uuid
direction string DEBIT or CREDIT
amount string Fixed-point decimal
currency string ISO 4217
gl_account_code string Chart-of-accounts code for GL classification

Response (MOD-001 → MOD-020)

HTTP 201 on success.

Field Type Description
posting_id uuid Stable posting reference — used in event and audit
committed_at ISO8601 Ledger commit timestamp
ledger_balance_after string Fixed-point decimal
available_balance_after string Fixed-point decimal
idempotency_key string Echoed from request

HTTP 409 if duplicate idempotency_key (returns original 201 body). HTTP 422 if entries are unbalanced or account not found. HTTP 503 retryable — transient Postgres error.


MOD-071 → MOD-020: payment initiation to pre-payment validation

Direction: Synchronous. MOD-071 (payment initiation UI module, SD08) calls MOD-020 (pre-payment validation, SD04) as the first step of every payment submission. Protocol: Cross-domain Lambda invocation via MOD-075. JWT validated by MOD-044 at the API gateway layer. SLA: MOD-020 must return within p99 ≤ 300ms. This is a customer-facing blocking call on the payment confirmation screen. Hard gate: A payment that does not pass MOD-020 must never reach MOD-001. This is enforced architecturally — MOD-001 refuses postings without a validation_reference.

Request (MOD-071 → MOD-020)

POST /internal/v1/payments/validate
Field Type Required Description
idempotency_key string Client-generated; stable across retries
customer_id uuid
source_account_id uuid
amount string Fixed-point decimal
currency string ISO 4217
payment_type string INTERNAL / DOMESTIC / INTERNATIONAL / FX
destination object See destination shape below
channel string APP / API / OPEN_BANKING / AGENT
session_id uuid From MOD-068 — used by fraud scorer
device_fingerprint_id uuid From MOD-024; required for APP channel
requested_at ISO8601

Destination shape:

Field Type Required Description
type string INTERNAL_ACCOUNT / DOMESTIC_BSB / DOMESTIC_SORT / SWIFT_BIC
account_id uuid Present when type is INTERNAL_ACCOUNT
bsb string AU domestic; 6 digits
account_number string AU domestic
sort_code string NZ domestic; 6 digits
swift_bic string International
beneficiary_name string Always required for display and sanctions screening
reference string Free-text payment reference

Response (MOD-020 → MOD-071)

HTTP 200 on pass.

Field Type Description
validation_reference uuid Passed to MOD-001 posting request
validation_status string PASS — only value in 200 response
checks_performed array of string e.g. ["BALANCE", "LIMITS", "SANCTIONS", "FRAUD", "ACCOUNT_STATUS"]
fraud_score string Decimal string — for transparency logging
fx_required boolean True when currency conversion is needed
fx_lock_required boolean True when caller must obtain an FX rate lock before posting
expires_at ISO8601 Validation result is valid until this time (default: 30s)
idempotency_key string Echoed

HTTP 422 on validation failure:

Field Type Description
validation_status string FAIL
failure_code string Stable key e.g. INSUFFICIENT_FUNDS, SANCTIONS_HOLD, DAILY_LIMIT_EXCEEDED, ACCOUNT_RESTRICTED, FRAUD_BLOCK
failure_message string Human-readable
retryable boolean

MOD-025 → MOD-001: FX conversion posting

Direction: Synchronous. MOD-025 (FX rate lock & conversion) calls MOD-001 to post a balanced FX conversion pair (debit source currency / credit target currency) atomically. Protocol: Intra-domain within SD04 for the rate lock; cross-domain to SD01 for posting. SLA: Must complete within the rate-lock window (30–60 seconds from bank.payments.fx_rate_locked event). If the lock expires, MOD-025 must not call MOD-001 and must return an error to MOD-071.

Request (MOD-025 → MOD-001)

POST /internal/v1/postings

Uses the same posting request schema as MOD-020 → MOD-001, with posting_type: "FX_CONVERSION" and exactly four entries:

  1. Debit source currency account (customer)
  2. Credit FX suspense account (source currency)
  3. Debit FX suspense account (target currency)
  4. Credit target currency account (customer)

Additional field in request:

Field Type Required Description
fx_lock_id uuid References bank.payments.fx_rate_locked event
rate_applied string Fixed-point decimal — must match locked rate
lock_expires_at ISO8601 MOD-001 rejects if now > lock_expires_at

SD02 internal: KYC Platform

MOD-009 → MOD-010: eIDV result to CDD tier assignment

Direction: Synchronous. MOD-009 calls MOD-010 upon a PASS or REFER verification result. MOD-010 responds with a CDD tier and account activation decision. Protocol: Lambda invocation — intra-domain (both SD02, bank-kyc repo). SLA: Must complete within the onboarding flow. NFR-002: total onboarding p90 ≤ 5 minutes. Note: MOD-009 also emits bank.kyc.identity_verified on EventBridge. MOD-010 may consume either the sync call or the event. In the onboarding hot path, the sync call is used to keep the activation decision within the same request scope. The event is emitted regardless for audit and downstream consumers.

Request (MOD-009 → MOD-010)

POST /internal/v1/kyc/cdd-assignment
Field Type Required Description
idempotency_key string Tied to verification_id
customer_id uuid
verification_id uuid Stable MOD-009 record
verification_status string PASS / REFER
document_type string PASSPORT / DRIVERS_LICENCE / NATIONAL_ID
confidence_score string Decimal string "0.00""1.00"
jurisdiction string NZ / AU
edd_required boolean True if confidence below 0.85 auto-accept threshold
verified_at ISO8601

Response (MOD-010 → MOD-009)

HTTP 200.

Field Type Description
cdd_tier string LOW / MEDIUM / HIGH / ENHANCED
tier_rationale string Human-readable reason
sanctions_check_status string CLEAR / MATCH_FOUND / PENDING
account_activation_permitted boolean False if sanctions match found or EDD incomplete
edd_tasks array of string Required EDD steps if cdd_tier is ENHANCED; empty otherwise
idempotency_key string Echoed

HTTP 422 if verification_status is FAIL — MOD-009 should not call MOD-010 on FAIL; use the event path instead.


MOD-013: sanctions screener — synchronous callers

Direction: Synchronous. MOD-013 is called by MOD-020 (pre-payment validation) and MOD-009 (eIDV) at the point where a screen must block progress if a match is found. Protocol: Lambda invocation — cross-domain calls use MOD-075; intra-domain calls are direct. SLA: p99 ≤ 150ms (within MOD-020's 300ms total budget). Hard gate: A positive (non-false-positive) match must immediately stop the caller from proceeding. MOD-013 never returns a CLEAR until all lists are checked.

Request (any caller → MOD-013)

POST /internal/v1/sanctions/screen
Field Type Required Description
idempotency_key string
entity_type string CUSTOMER / COUNTERPARTY
entity_id uuid
full_name string
date_of_birth string YYYY-MM-DD — improves match precision
nationality string ISO 3166-1 alpha-2
country_of_residence string ISO 3166-1 alpha-2
triggering_context string ONBOARDING / PAYMENT / PERIODIC_REVIEW / LIST_UPDATE
lists array of string If omitted, all active lists are screened

Response (MOD-013 → caller)

HTTP 200.

Field Type Description
screening_id uuid Stable record reference
result string CLEAR / MATCH_FOUND / PENDING
match_score string Highest match score across lists; decimal string "0.00""1.00"
match_type string EXACT / FUZZY / ALIAS — present when result is MATCH_FOUND
list_source string List that produced the match — present when result is MATCH_FOUND
lists_checked array of string All lists screened in this call
screened_at ISO8601
idempotency_key string Echoed

A PENDING result means list loading is in progress (rare). Callers must treat PENDING as a block — do not proceed.


SD04 internal: Payments

MOD-020 → MOD-021: velocity check request

Direction: Synchronous. MOD-020 calls MOD-021 (payment limit & velocity controller) as one of its validation sub-checks. Protocol: Lambda invocation — intra-domain (both SD04, bank-payments repo). Direct invocation; no API gateway. SLA: p99 ≤ 20ms. This call is on the hot payment path inside MOD-020's 300ms budget.

Request (MOD-020 → MOD-021)

POST /internal/v1/limits/check
Field Type Required Description
idempotency_key string Tied to the payment_id
customer_id uuid
account_id uuid Source account
amount string Fixed-point decimal
currency string ISO 4217
payment_type string INTERNAL / DOMESTIC / INTERNATIONAL / FX
channel string APP / API / OPEN_BANKING / AGENT

Response (MOD-021 → MOD-020)

HTTP 200.

Field Type Description
result string PASS / FAIL
daily_spent_today string Fixed-point decimal — running total
daily_limit string Fixed-point decimal
per_transaction_limit string Fixed-point decimal
breach_type string DAILY_VALUE / PER_TRANSACTION / DAILY_COUNT — present when result is FAIL
idempotency_key string Echoed

MOD-071 → MOD-025: FX rate lock request

Direction: Synchronous. MOD-071 (payment initiation) calls MOD-025 to obtain a rate lock before displaying the confirmed FX amount to the customer. The rate lock must be obtained before MOD-020 validation when fx_lock_required is true in the pre-validation response. Protocol: Cross-domain Lambda invocation via MOD-075. SLA: p99 ≤ 200ms. Customer is waiting on the FX confirmation screen. Lock window: 30–60 seconds. MOD-071 must submit the posting within this window or request a new lock.

Request (MOD-071 → MOD-025)

POST /internal/v1/fx/rate-lock
Field Type Required Description
idempotency_key string Tied to payment_id
payment_id uuid
source_currency string ISO 4217
target_currency string ISO 4217
source_amount string Fixed-point decimal — amount customer is sending
customer_id uuid
requested_at ISO8601

Response (MOD-025 → MOD-071)

HTTP 200.

Field Type Description
lock_id uuid Pass to MOD-001 posting request via MOD-025
rate string Fixed-point decimal — locked exchange rate
inverse_rate string For customer display
source_amount string Fixed-point decimal
target_amount string Fixed-point decimal
fee_amount string Fixed-point decimal
fee_currency string ISO 4217
lock_expires_at ISO8601 Customer must confirm before this time
idempotency_key string Echoed

HTTP 422 if currency pair not supported or amount below minimum. HTTP 503 retryable — liquidity provider timeout.


SD05 internal: Credit Decisioning

MOD-027 + MOD-028: affordability and credit score combined assessment

Direction: Synchronous. MOD-029 (pre-approval engine) orchestrates a combined credit assessment by calling MOD-027 (affordability calculator) and MOD-028 (credit score & risk rating) in sequence. MOD-028 consumes the MOD-027 output as part of its scoring inputs. Protocol: Lambda invocation — intra-domain (all SD05, bank-credit repo). SLA: Combined affordability + scoring p99 ≤ 2s for automated decisions.

Step 1 — Request (MOD-029 → MOD-027)

POST /internal/v1/credit/affordability
Field Type Required Description
idempotency_key string Tied to application_id
application_id uuid
customer_id uuid
declared_income string Fixed-point decimal annual gross
income_currency string ISO 4217
income_verification_source string CDR_BANK_DATA / PAYSLIP / TAX_RETURN / SELF_DECLARED
declared_expenses string Fixed-point decimal annual
existing_debt_obligations string Fixed-point decimal annual repayment total
requested_amount string Fixed-point decimal
term_months integer
product_id string
jurisdiction string NZ / AU

Step 1 — Response (MOD-027 → MOD-029)

HTTP 200.

Field Type Description
affordability_id uuid Passed to MOD-028
net_disposable_income string Fixed-point decimal monthly
proposed_repayment string Fixed-point decimal monthly
dti_ratio string Debt-to-income as decimal string
affordability_outcome string PASS / FAIL / MARGINAL
hem_applied boolean True when Household Expenditure Measure applied (AU)
regulatory_framework string CCCFA (NZ) / NCCP (AU)
idempotency_key string Echoed

Step 2 — Request (MOD-029 → MOD-028)

POST /internal/v1/credit/score
Field Type Required Description
idempotency_key string Same application_id tie
application_id uuid
customer_id uuid
affordability_id uuid From MOD-027 response
affordability_outcome string From MOD-027
net_disposable_income string From MOD-027
dti_ratio string From MOD-027
bureau_data_consent boolean Must be true — blocks if false
jurisdiction string NZ / AU

Step 2 — Response (MOD-028 → MOD-029)

HTTP 200.

Field Type Description
scoring_id uuid Stable reference
credit_score integer Internal scorecard score
risk_rating string Basel-aligned rating e.g. AAA, BBB+, CCC
decision string APPROVE / DECLINE / REFER
approved_amount string Fixed-point decimal — present on APPROVE
approved_rate string Decimal string — present on APPROVE
decline_reason_codes array of string Present on DECLINE
model_version string Scorecard version for audit
idempotency_key string Echoed

MOD-029: pre-approval write-back via ADR-036

Direction: Asynchronous write-back. MOD-079 (Snowflake decision publication service, SD07) writes nightly batch pre-approval decisions from Snowflake into the Neon decision_* schema. MOD-029 reads from the decision_inbox table; it does not call MOD-079 directly. Protocol: Snowflake → Neon decision publication contract (ADR-036). MOD-079 POST endpoint receives the batch from Snowflake and upserts into Neon. SLA: Write-back latency ≤ 60s from Snowflake decision (NFR-014).

MOD-079 receives the following payload from Snowflake for each pre-approval decision (see MOD-079 → decision inbox for the full contract).

MOD-029 reads from decision_inbox by polling for decision_type = 'PRE_APPROVAL' with applied = false. On reading:

  1. Validates schema_version and idempotency_key.
  2. Writes pre-approval offer to credit.pre_approvals table.
  3. Marks decision_inbox row applied = true.
  4. Emits bank.platform.decision_applied event.

SD07 Platform: Data Platform

MOD-044: JWT validation

Direction: Synchronous. Every module that exposes an endpoint accessible from outside its own domain calls MOD-044 to validate the caller's JWT before processing the request. MOD-044 is exposed via MOD-075 (internal API gateway) as a shared authoriser. Protocol: Lambda authoriser invoked by MOD-075 on every inbound cross-domain request. The calling module never calls MOD-044 directly; the gateway enforces it. SLA: p99 ≤ 5ms. Results are cached for 300 seconds per token by the API gateway.

Request (API gateway → MOD-044)

Field Type Required Description
token string Bearer token from Authorization header
required_scope string The scope required for the requested operation
resource_arn string AWS resource ARN being accessed

Response (MOD-044 → API gateway)

IAM policy document (AWS Lambda authoriser format):

{
  "principalId": "customer|agent|system:<subject_id>",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [{
      "Action": "execute-api:Invoke",
      "Effect": "Allow",
      "Resource": "<resource_arn>"
    }]
  },
  "context": {
    "subject_id": "uuid",
    "subject_type": "CUSTOMER | AGENT | SERVICE",
    "roles": "csv of roles",
    "jurisdiction": "NZ | AU | NZ + AU",
    "session_id": "uuid",
    "token_expiry": "ISO8601"
  }
}

Effect: Deny is returned when the token is invalid, expired, or lacks the required scope. The API gateway returns HTTP 403 to the caller without invoking the downstream Lambda.


MOD-079: decision inbox — Snowflake write-back

Direction: Snowflake pushes decisions to MOD-079 (Snowflake decision publication service). MOD-079 validates, deduplicates, and writes to the Neon decision_* schema. Protocol: HTTPS POST from Snowflake external function / task to MOD-079 API, authenticated via Snowflake-managed secret (ADR-036, ADR-030). SLA: MOD-079 must acknowledge within 10s. Write-back from Snowflake commit to Neon availability ≤ 60s (NFR-014).

Request (Snowflake → MOD-079)

POST /internal/v1/decisions

Matches the decision publication contract defined in data-contracts.md. Key fields:

Field Type Required Description
decision_id uuid Snowflake-generated stable ID
entity_type string APPLICATION / CUSTOMER / PAYMENT
entity_id string
decision_type string ONBOARDING / CREDIT / RISK_SCORE / PRE_APPROVAL / FRAUD
decision_status string ACCEPT / REFER / REJECT / HOLD / CLEAR
decision_summary string One-line plain-language summary
score_summary object { risk_score, risk_tier, fraud_score? }
reasons array [{ reason_code, reason_label, reason_explanation }]
policy_refs array of string e.g. ["AML-011"]
produced_by string Snowflake component identifier
model_version string
idempotency_key string
schema_version string

Response (MOD-079 → Snowflake)

HTTP 202 Accepted — write queued. HTTP 200 — already processed (idempotent replay). HTTP 422 — schema validation failure; Snowflake must not retry. HTTP 503 — transient; Snowflake retries with exponential backoff up to 3 times.

After writing to Neon, MOD-079 emits bank.platform.decision_published on the bank-platform EventBridge bus.


SD08 ↔ SD04 ↔ SD01: Full payment submission chain

This section documents the end-to-end synchronous chain triggered when a customer submits a payment in the app. All steps are synchronous from the customer's perspective; EventBridge events are emitted at each stage for audit and async consumers.

MOD-071 (payment initiation, SD08)
  ├─► MOD-025 (FX rate lock, SD04)        [if fx_lock_required]
  │     └── returns lock_id, target_amount
  ├─► MOD-020 (pre-payment validation, SD04)
  │     ├─► MOD-021 (limit & velocity check)    [sync sub-call]
  │     ├─► MOD-013 (sanctions screener)         [sync sub-call]
  │     ├─► MOD-023 (fraud scorer)               [sync sub-call, ≤200ms]
  │     └── returns validation_reference or failure
  └─► MOD-001 (posting engine, SD01)      [only if validation passes]
        └── returns posting_id, balances

Step 1 — FX lock (conditional). If the destination currency differs from the source account currency, MOD-071 calls MOD-025 to lock a rate. The lock_id and lock_expires_at are stored client-side for the session. If the customer does not confirm within lock_expires_at, MOD-071 must request a new lock.

Step 2 — Validation. MOD-071 posts to POST /internal/v1/payments/validate (MOD-020). MOD-020 runs its sub-checks in parallel where dependencies allow: - Balance check (reads Postgres directly) - Limit & velocity check (calls MOD-021 sync) - Sanctions check (calls MOD-013 sync) - Fraud score (calls MOD-023 sync — must return within 200ms per NFR-020) - Account status check (reads Postgres)

MOD-020 returns a single PASS or FAIL. On FAIL, MOD-071 surfaces the failure_message to the customer and the chain stops.

Step 3 — Posting. On PASS, MOD-071 passes validation_reference and (if FX) lock_id to MOD-001 via POST /internal/v1/postings. MOD-001 commits the ledger entry and returns posting_id.

Step 4 — Events. MOD-001 emits bank.core.posting_completed. MOD-022 consumes this to close its audit record. MOD-020 (via MOD-026 for cross-border) checks IFTI threshold and emits bank.payments.ifti_threshold_crossed if applicable.

Failure handling. If MOD-001 returns HTTP 503, MOD-071 may retry using the same idempotency_key. If MOD-001 returns HTTP 409 (duplicate), MOD-071 should treat the original response as the canonical result. MOD-071 must never submit a second posting with a different idempotency_key for the same customer-initiated payment.