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)¶
| 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)¶
| 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)¶
Uses the same posting request schema as MOD-020 → MOD-001,
with posting_type: "FX_CONVERSION" and exactly four entries:
- Debit source currency account (customer)
- Credit FX suspense account (source currency)
- Debit FX suspense account (target currency)
- 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)¶
| 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)¶
| 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)¶
| 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)¶
| 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)¶
| 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)¶
| 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:
- Validates
schema_versionandidempotency_key. - Writes pre-approval offer to
credit.pre_approvalstable. - Marks
decision_inboxrowapplied = true. - Emits
bank.platform.decision_appliedevent.
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)¶
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.