Remittance Flow
Low-Level Design: Remittance Flow
Version: 1.0
Date: 2026-02-21
Author: Banking Architecture Team
Status: Approved
Applies to: Drop — Cross-Border Money Transfer (PISP)
1. Overview
Remittance is Drop's core feature — sending money from a Norwegian bank account to a recipient abroad. The flow has 4 user-facing steps:
Select recipient (or add new)
Enter amount (see FX rate + fee in real-time)
Review (PSD2 Art. 45 pre-payment disclosure)
Confirm (SCA via BankID at user's bank)
Drop uses PISP (Payment Initiation Service) to initiate the transfer directly from the user's bank account. Drop never touches the money.
API endpoint: POST /api/transactions/remittance
Fee: 0.5% of send amount
Amount range: 100 - 50,000 NOK
KYC required: Yes ( kyc_status = 'approved' )
Supported corridors: Serbia (RSD), Bosnia (BAM), Poland (PLN), Pakistan (PKR), Turkey (TRY), EU (EUR)
2. End-to-End Remittance Flow
sequenceDiagram
participant U as User
participant UI as Drop App
(/send)
participant API as Drop API
participant DB as Drop DB
participant ASPSP as User's Bank
participant RB as Recipient Bank
Note over U,RB: Step 1 — Select Recipient
U->>UI: Navigate to Send Money
UI->>API: GET /api/recipients?page=1&limit=20
API->>DB: SELECT * FROM recipients
WHERE user_id = ? ORDER BY created_at DESC
DB-->>API: [{id: "rec_1", name: "Marko Petrovic",
country: "Serbia", currency: "RSD"}]
API-->>UI: Recipient list (bank accounts masked)
U->>UI: Select "Marko Petrovic"
Note over U,RB: Step 2 — Enter Amount
UI->>API: GET /api/rates/RSD
API->>DB: SELECT rate FROM exchange_rates
WHERE to_currency = 'RSD'
DB-->>API: {rate: 10.17}
API-->>UI: {rate: 10.17, fee: 0.005}
U->>UI: Enter 2000 NOK
UI->>UI: Live calculation:
Send: 2,000 NOK
Fee: 10 NOK (0.5%)
Rate: 1 NOK = 10.17 RSD
Receives: 20,340 RSD
Note over U,RB: Step 3 — Review (PSD2 Art. 45 Disclosure)
U->>UI: Tap "Neste" (Next)
UI->>API: POST /api/transactions/disclosure
{type: "remittance", amount: 2000,
recipientId: "rec_1"}
API->>DB: Lookup recipient currency, exchange rate
API->>API: Calculate fee (2000 * 0.005 = 10)
Calculate receive (2000 * 10.17 = 20340)
Determine delivery (non-EEA: 2-4 days)
API-->>UI: {amount: 2000, fee: 10, feePercentage: 0.5,
exchangeRate: 10.17, receiveAmount: 20340,
receiveCurrency: "RSD",
estimatedDelivery: "2-4 business days",
totalCost: 2010}
UI->>UI: Display disclosure screen:
"Du sender 2 000 kr til Marko Petrovic
Gebyr: 10 kr (0,5%)
Vekslingskurs: 1 NOK = 10,17 RSD
Marko mottar: 20 340 RSD
Total kostnad: 2 010 kr
Estimert levering: 2-4 virkedager"
Note over U,RB: Step 4 — Confirm & Payment Initiation
U->>UI: Tap "Bekreft og send" (Confirm and send)
UI->>API: POST /api/transactions/remittance
{recipientId: "rec_1", amount: 2000,
bankAccountId: "ba_1"}
API->>DB: Verify KYC: kyc_status = 'approved'
API->>DB: Verify recipient belongs to user
API->>DB: Verify bank account exists + balance >= 2010
API->>DB: Lookup exchange rate for RSD
API->>DB: Generate idempotency_key
BEGIN TRANSACTION
UPDATE bank_accounts SET balance = balance - 201000
INSERT INTO transactions (status: 'processing')
API->>DB: COMMIT
API->>ASPSP: POST /v1/payments/cross-border-credit-transfers
{debtorAccount: {iban: user_iban},
instructedAmount: {currency: "NOK", amount: "2010.00"},
creditorName: "Marko Petrovic",
creditorAccount: {bban: "265-1234567-89"},
remittanceInformationUnstructured: "Drop remittance tx_rem_xxx"}
ASPSP-->>API: {paymentId: "pay_xyz",
transactionStatus: "RCVD",
scaRedirect: "https://dnb.no/sca/pay/..."}
API-->>UI: {transactionId: "tx_rem_xxx",
scaRedirect: "https://dnb.no/sca/pay/..."}
Note over U,RB: SCA at Bank (Dynamic Linking)
UI->>U: Redirect to bank SCA page
U->>ASPSP: BankID authentication
(sees: "2 010 NOK to Marko Petrovic")
ASPSP-->>U: Redirect to Drop callback
U->>API: GET /api/payments/callback?paymentId=pay_xyz
API->>ASPSP: GET /v1/payments/pay_xyz/status
ASPSP-->>API: {transactionStatus: "ACCP"}
API->>DB: UPDATE transactions
SET status = 'completed',
completed_at = now
WHERE id = 'tx_rem_xxx'
API->>DB: INSERT INTO audit_log
(action: 'payment.completed')
API->>DB: INSERT INTO notifications
(title: 'Overfoering sendt',
body: '2 000 kr sendt til Marko Petrovic')
API-->>UI: {status: "completed"}
UI-->>U: Success screen:
"2 000 kr sendt til Marko Petrovic!
Estimert levering: 2-4 virkedager"
Note over ASPSP,RB: Settlement (Drop is not involved)
ASPSP->>RB: SWIFT gpi / correspondent banking
NOK converted to RSD
RB->>RB: Credit Marko's account: 20,340 RSD
3. Transaction States
stateDiagram-v2
[*] --> Draft: User on review screen
(disclosure shown, not yet confirmed)
Draft --> Initiated: User taps "Bekreft og send"
Transaction record created
(status: processing)
Draft --> Abandoned: User navigates away
(no record created)
Initiated --> ScaPending: ASPSP returns scaRedirect
User redirected to bank
Initiated --> Failed: ASPSP rejects initiation
(invalid IBAN, bank error)
ScaPending --> Completed: User completes BankID SCA
ASPSP status: ACCP/ACSC
ScaPending --> Failed: SCA timeout (5 min)
ScaPending --> Failed: User cancels SCA
ScaPending --> Failed: Bank rejects payment
(insufficient funds at bank)
Completed --> Settled: Funds credited to recipient
(tracked via ASPSP status polling)
Completed --> RefundPending: Settlement failed
(correspondent bank error)
Failed --> [*]: User sees error,
can retry from Step 1
RefundPending --> Refunded: Refund processed
Balance restored
Refunded --> [*]
Settled --> [*]
Abandoned --> [*]
Database status values ( transactions.status CHECK constraint):
processing — Transaction created, awaiting SCA or settlement
completed — ASPSP accepted payment, settlement in progress or done
failed — Payment rejected, SCA failed, or settlement error
4. Pre-Payment Disclosure (PSD2 Art. 45)
Before the user confirms a remittance, Drop must show a complete cost breakdown . This is a legal requirement under PSD2 Art. 45 (Betalingstjenesteloven in Norwegian law).
4.1 Disclosure Checklist
Item
PSD2 Reference
Drop Field
Example
Amount to be transferred
Art. 45(1)(a)
amount
2,000 NOK
All fees/charges
Art. 45(1)(b)
fee , feePercentage
10 NOK (0.5%)
Exchange rate used
Art. 45(1)(c)
exchangeRate
1 NOK = 10.17 RSD
Amount after conversion
Art. 45(1)(d)
receiveAmount , receiveCurrency
20,340 RSD
Total cost to payer
Art. 45(1)(e)
totalCost
2,010 NOK
Estimated delivery time
Art. 45(1)(f)
estimatedDelivery
2-4 business days
Currency of debit
Implicit
Send currency
NOK
Currency of credit
Implicit
Receive currency
RSD
4.2 Disclosure Screen Content (Norwegian)
Bekreft overfoering
Til: Marko Petrovic
Land: Serbia
Bankkonto: *****567-89 (Banca Intesa)
Du sender: 2 000,00 kr
Gebyr (0,5%): 10,00 kr
Totalt belop: 2 010,00 kr
Vekslingskurs: 1 NOK = 10,17 RSD
Marko mottar: 20 340,00 RSD
Estimert levering: 2-4 virkedager
Pengene trekkes fra: DNB Brukskonto
[Bekreft og send] [Avbryt]
5. FX Rate Lock & Expiry
5.1 Rate Lock Window
Event
Time
Action
User views disclosure
T+0
Rate displayed from exchange_rates table
User confirms payment
T+0 to T+15min
Rate locked in transactions.exchange_rate
SCA completes
T+0 to T+15min
Locked rate applies to settlement
SCA not completed
T+15min
Rate expires, transaction fails, user must re-quote
5.2 Rate Lock Implementation
When POST /api/transactions/remittance is called, the current rate is read from exchange_rates
The rate is stored in transactions.exchange_rate at insert time
If the SCA takes longer than 15 minutes, the reconciliation job detects the stale transaction and marks it failed
User is notified to retry (with a new, current rate)
6. Validation Rules
6.1 Pre-Flight Checks (Before Transaction Creation)
Check
Source
Error if Failed
User authenticated
JWT from cookie/header
401 unauthorized
KYC approved
users.kyc_status = 'approved'
403 kyc_required
Recipient exists
recipients.id WHERE user_id = ?
404 not_found
Recipient belongs to user
recipients.user_id = jwt.userId
404 not_found
Bank account exists
bank_accounts.id WHERE user_id = ?
400 no_bank_account
Amount in range
100 to 50,000 NOK
422 validation_error
Amount valid
Number.isFinite() , max 2 decimals
422 validation_error
Balance sufficient
bank_accounts.balance >= amount + fee
402 insufficient_balance
Currency corridor supported
exchange_rates.to_currency exists
422 validation_error
Not duplicate
idempotency_key unique
Return existing transaction
Rate limit
< 10 requests/min per IP
429 rate_limited
6.2 Amount Validation
Minimum: 100 NOK (protect against micro-transaction abuse)
Maximum: 50,000 NOK (regulatory limit for simplified CDD)
Decimals: max 2 (validated by validateAmount())
Type: Number.isFinite() (prevents NaN, Infinity injection)
7. Error Scenarios & User Messages
Scenario
API Response
User Message (Norwegian)
Next Step
KYC not approved
403 kyc_required
"Du maa fullfoere identitetsverifisering for aa sende penger."
Redirect to KYC flow
No linked bank account
400 no_bank_account
"Du har ingen tilkoblet bankkonto. Koble til en bank foerst."
Redirect to /accounts
Insufficient balance
402 insufficient_balance
"Ikke nok penger paa kontoen. Saldo: 1 200 kr, totalt belop: 2 010 kr."
Show balance, suggest lower amount
Unsupported corridor
422 validation_error
"Vi stoetter ikke overfoering til dette landet ennaa."
Show supported countries
Amount too low
422 validation_error
"Minimumsbelopet er 100 kr."
Adjust amount
Amount too high
422 validation_error
"Maksimumsbelopet er 50 000 kr."
Adjust amount
SCA timeout
(callback timeout)
"BankID-sesjonen utlop. Overforingen ble ikke gjennomfoert."
Retry button
SCA cancelled
(callback cancelled)
"Du avbrot betalingen. Ingen penger er trukket."
Retry button
Bank rejected
ASPSP RJCT
"Banken avviste overforingen. Kontakt banken din."
Show bank support info
Rate expired
(rate > 15min old)
"Vekslingskursen har utlopt. Vennligst bekreft ny kurs."
Re-show disclosure with new rate
Network error
502/503
"Teknisk feil. Proev igjen om noen minutter."
Retry after 30s
Duplicate detected
200 (existing tx)
"Denne overforingen er allerede registrert."
Show existing transaction
8. Post-Transaction
8.1 Confirmation Screen
After successful SCA:
Overfoering sendt!
2 000 kr sendt til Marko Petrovic
Marko mottar 20 340 RSD
Referanse: tx_rem_a1b2c3d4...
Status: Under behandling
Estimert levering: 2-4 virkedager
[Se detaljer] [Send til en annen]
8.2 Transaction Tracking
Users can track their remittance in the Transaction History ( /transactions ):
Status
Display
Icon
processing
"Under behandling"
Spinner
completed
"Fullfoert"
Green checkmark
failed
"Feilet"
Red X
8.3 Transaction Summary
GET /api/transactions/summary returns aggregated transaction statistics for the authenticated user (total sent, total fees, transaction count, breakdown by corridor).
8.4 Receipt
GET /api/transactions/{id}/receipt returns a detailed receipt:
{
"transactionId": "tx_rem_xxx",
"date": "2026-02-21T14:30:00Z",
"type": "remittance",
"amount": 2000,
"currency": "NOK",
"fee": 10,
"exchangeRate": 10.17,
"receiveAmount": 20340,
"receiveCurrency": "RSD",
"recipient": {"name": "Marko Petrovic", "country": "RS"},
"reference": "tx_rem_xxx",
"status": "completed",
"completedAt": "2026-02-21T14:35:00Z"
}
8.5 Notifications
On completion/failure, a notification is created:
Event
Notification Title
Notification Body
Payment sent
"Overfoering sendt"
"2 000 kr sendt til Marko Petrovic"
Payment completed
"Overfoering fullfoert"
"20 340 RSD mottatt av Marko Petrovic"
Payment failed
"Overfoering feilet"
"Overfoering til Marko Petrovic ble avvist. Kontakt oss for hjelp."
9. Refund Handling
If a remittance fails after funds were debited (e.g., correspondent bank rejects, recipient IBAN invalid):
Step
Action
Timeline
1
ASPSP reports RJCT or CANC status
1-5 business days
2
Drop detects via reconciliation job
Within 1 hour of status change
3
Drop creates refund record in audit_log
Immediate
4
ASPSP reverses the debit (automatic for SEPA)
1-3 business days
5
Drop updates bank_accounts.balance on next AISP sync
Next balance refresh
6
User notified via push notification
Immediate
Note: For SWIFT transfers, refund timing depends on correspondent banks and may take 5-10 business days. Drop sends a notification with estimated refund timeline.
10. AML/Compliance Checks
Each remittance triggers compliance checks before PISP initiation:
Check
Implementation
Action on Trigger
Velocity limit
> 5 remittances/hour or > 20/day
aml_alerts record (medium severity), continue
Structuring detection
Multiple amounts just below 25,000 NOK
aml_alerts record (high severity), review queue
High-risk corridor
FATF grey/black list country
Enhanced due diligence flag
Single large transfer
> 25,000 NOK
Enhanced monitoring
Total daily volume
> 100,000 NOK cumulative
aml_alerts record, may require manual approval
Sanctions screening
Recipient name vs sanctions lists
Block if match, screening_results record
11. Database Impact
11.1 Tables Written
Table
Operation
When
transactions
INSERT
Payment initiated (Step 4)
transactions
UPDATE
Status change (processing to completed/failed)
bank_accounts
UPDATE (balance)
Atomic debit during transaction creation
audit_log
INSERT
Every payment action
notifications
INSERT
Payment sent / completed / failed
aml_alerts
INSERT
If AML rule triggered
11.2 Tables Read
Table
Operation
When
users
SELECT (kyc_status)
Pre-flight KYC check
recipients
SELECT
Recipient lookup (Step 1)
bank_accounts
SELECT (balance)
Balance check (Step 4)
exchange_rates
SELECT (rate)
FX rate lookup (Step 2, 3, 4)
transactions
SELECT (idempotency_key)
Duplicate detection
12. Cross-References
Payment Processing: ../integration/payment-processing.md — SEPA/SWIFT settlement, FX, fees
Open Banking AISP/PISP: ../integration/open-banking-aisp-pisp.md — PISP API details
BankID OIDC: ../integration/bankid-oidc-integration.md — Drop authentication
Security Architecture: ../hld/security-architecture.md — Fraud detection, AML pipeline
Bank Account Linking: flow-bank-account-linking.md — Prerequisite: linked bank account
Database Schema: ../../backend/DATABASE-SCHEMA.md — transactions , recipients , exchange_rates tables
API Reference: ../../backend/API-REFERENCE.md — Remittance, disclosure, receipt, rate endpoints