API & Data API Reference Drop Backend API Reference Auto-generated from source code analysis. All file references are relative to src/drop-app/src/ . Overview Drop uses Next.js App Router API routes ( app/api/ ). All responses use a consistent JSON envelope: { "data": { ... } } // Success { "error": "code", "message": "...", "details": [...] } // Error Authentication is via httpOnly cookie ( drop_token ) containing a signed JWT (HS256, 24h expiry). Pass-Through Model: Drop uses a PSD2 pass-through model — it NEVER holds customer money. There is no wallet, no balance, no top-up. User funds remain in their bank account at all times. Drop uses: AISP (Account Information Service Provider) — reads bank balance via Open Banking PISP (Payment Initiation Service Provider) — initiates transfers directly from user's bank account The bank_accounts.balance field stores the last AISP-read balance from the user's real bank (cached for display) — NOT a Drop-held balance. Authentication POST /api/auth/register Create a new user account. Field Source File app/api/auth/register/route.ts Auth None Rate Limit 10 req/min per IP Request Body: Field Type Required Validation email string Yes RFC-like regex, unique password string Yes Min 8 chars, must contain letters + digits firstName string Yes validateName() — 1-100 chars, at least one letter, no HTML/script lastName string Yes Same as firstName phone string No International format +XXXXXXXXXXXX (8-15 digits) dateOfBirth string Yes ISO date string, must be >= 18 years old Success Response (201): { "data": { "id": "usr_...", "email": "user@example.com", "firstName": "...", "lastName": "...", "dateOfBirth": "...", "kycStatus": "pending", "createdAt": "2026-..." } } Error Responses: Status Code Condition 400 bad_request Invalid JSON body 409 conflict Email already registered 422 validation_error Field validation failures (returned in details array) 429 rate_limited Too many requests POST /api/auth/login Authenticate with email and password. Field Source File app/api/auth/login/route.ts Auth None Rate Limit 10 req/min per IP Request Body: Field Type Required email string Yes password string Yes Success Response (200): { "data": { "id": "usr_...", "email": "...", "firstName": "...", "lastName": "...", "kycStatus": "approved" } } Error Responses: Status Code Condition 400 bad_request Missing email or password 401 unauthorized Invalid credentials 429 rate_limited Too many requests GET /api/auth/me Get current authenticated user with bank accounts. Field Source File app/api/auth/me/route.ts Auth Required (cookie) Success Response (200): { "data": { "id": "usr_...", "email": "...", "firstName": "...", "lastName": "...", "totalBalance": 58030.0, "bankAccounts": [ { "id": "ba_1", "bankName": "DNB", "accountNumber": "1234.56.78901", "balance": 45230.0, "currency": "NOK", "isPrimary": true } ], "kycStatus": "approved", "createdAt": "..." } } POST /api/auth/logout Logout and revoke all sessions. Field Source File app/api/auth/logout/route.ts Auth Required (cookie) Calls revokeAllSessions() to invalidate all session records, then clears the auth cookie. Success Response (200): { "message": "Logged out" } POST /api/auth/refresh Refresh the authentication token (issue new JWT, create new session record). Field Source File app/api/auth/refresh/route.ts Auth Required (cookie) Success Response (200): { "data": { "userId": "usr_...", "email": "...", "role": "user" } } Transactions GET /api/transactions List user's transactions with pagination and filtering. Field Source File app/api/transactions/route.ts Auth Required Query Parameters: Param Type Default Notes page int 1 Min 1 limit int 20 Min 1, Max 50 type string - remittance or qr_payment status string - processing , completed , or failed Success Response (200): { "data": [ { "id": "tx_rem_1", "type": "remittance", "status": "completed", "amount": -2000, "currency": "NOK", "recipientName": "Mama Jasmina", "createdAt": "..." } ], "pagination": { "page": 1, "limit": 20, "total": 3 } } Note: amount is negated in the response (always shown as outgoing). GET /api/transactions/[id] Get single transaction details with exchange rate info. Field Source File app/api/transactions/[id]/route.ts Auth Required Success Response (200): { "data": { "id": "tx_rem_1", "type": "remittance", "status": "completed", "sendAmount": 2000, "sendCurrency": "NOK", "receiveAmount": 23400, "receiveCurrency": "RSD", "exchangeRate": 11.7, "fee": 10, "total": 2010, "recipientName": "Mama Jasmina", "recipientCountry": "Serbia", "createdAt": "...", "completedAt": "..." } } GET /api/transactions/summary Get transaction summary statistics (all-time and this month). Field Source File app/api/transactions/summary/route.ts Auth Required Success Response (200): { "data": { "allTime": { "totalCount": 3, "totalSent": 5000, "totalPaid": 129, "remittanceCount": 2, "qrPaymentCount": 1 }, "thisMonth": { "..." } } } POST /api/transactions/remittance Create a remittance (international money transfer). Field Source File app/api/transactions/remittance/route.ts Auth Required Rate Limit 10 req/min per IP KYC Must be approved Request Body: Field Type Required Validation recipientId string Yes Must belong to user amount number Yes 100-50,000 NOK, max 2 decimal places currency string No Defaults to NOK bankAccountId string No Defaults to primary bank account Business Logic: Verify recipient belongs to user Look up exchange rate for recipient's currency Verify bank account exists and has sufficient balance Fee: 0.5% of amount Debit bank account (atomic transaction) Create transaction record with status processing Success Response (201): { "data": { "id": "tx_rem_...", "type": "remittance", "status": "processing", "sendAmount": 2000, "sendCurrency": "NOK", "receiveAmount": 23400, "receiveCurrency": "RSD", "exchangeRate": 11.7, "fee": 10, "feePercent": 0.5, "total": 2010, "recipientName": "...", "recipientCountry": "Serbia", "fromAccount": "DNB", "eta": "1-2 business days", "createdAt": "..." } } Error Responses: Status Code Condition 400 bad_request Missing/invalid fields 400 no_bank_account No linked bank account 402 insufficient_balance Bank account balance too low 403 kyc_required KYC not approved 404 not_found Recipient not found 422 validation_error Unsupported currency corridor POST /api/transactions/qr-payment Create a QR payment to a merchant. Field Source File app/api/transactions/qr-payment/route.ts Auth Required Rate Limit 10 req/min per IP Request Body: Field Type Required Validation merchantId string Yes Must exist amount number Yes 1-100,000 NOK, max 2 decimal places Business Logic: Verify merchant exists Get user's primary bank account Fee: 1% of amount Debit bank account (atomic transaction) Create transaction with status completed (instant) Success Response (201): { "data": { "id": "tx_qr_...", "type": "qr_payment", "status": "completed", "amount": 129, "currency": "NOK", "fee": 1.29, "feePercent": 1, "merchantName": "Ahmetov Kebab", "merchantId": "mer_1", "fromAccount": "DNB", "createdAt": "..." } } Recipients GET /api/recipients List user's recipients with pagination. Field Source File app/api/recipients/route.ts Auth Required Query Parameters: page (default 1), limit (default 20, max 50) Bank account numbers are masked in response (e.g., *****5678 ). Supported Countries: RS (Serbia), BA (Bosnia), PL (Poland), PK (Pakistan), TR (Turkey) POST /api/recipients Add a new recipient. Field Source File app/api/recipients/route.ts Auth Required Request Body: Field Type Required Validation name string Yes validateName() country string Yes Must be in supported list currency string Yes - bankAccount string Yes - bankName string No Sanitized to 200 chars DELETE /api/recipients/[id] Delete a recipient. Field Source File app/api/recipients/[id]/route.ts Auth Required Returns 204 No Content on success. Returns 404 if recipient not found or not owned by user. Cards (FUTURE — feature-flagged, all flags default to false ) Note: The entire Cards section is a FUTURE feature, gated behind feature flags. All card-related feature flags default to false . These endpoints exist in code but return 404 when flags are disabled. Cards require a card issuing partner (e.g., Stripe Issuing) before activation. GET /api/cards List user's cards (excludes cancelled). Field Source File app/api/cards/route.ts Auth Required POST /api/cards Create a new card (virtual or physical). Field Source File app/api/cards/route.ts Auth Required Request Body: Field Type Required Notes type string No virtual (default) or physical GET /api/cards/[id] Get card details. Card number is masked ( ---- ---- ---- XXXX ), CVV is hidden ( --- ). Field Source File app/api/cards/[id]/route.ts Auth Required PCI-DSS compliant: never exposes full card number or CVV. PATCH /api/cards/[id] Freeze or unfreeze a card. Field Source File app/api/cards/[id]/route.ts Auth Required Request Body: { "status": "active" | "frozen" } DELETE /api/cards/[id] Cancel a card (soft delete — sets status to cancelled ). Field Source File app/api/cards/[id]/route.ts Auth Required POST /api/cards/[id]/physical Order physical version of a virtual card. Field Source File app/api/cards/[id]/physical/route.ts Auth Required Feature Flag physicalCards (returns 404 if disabled) Request Body: { "address": "..." } (min 10 chars) POST /api/cards/[id]/pin Set PIN for a card. Field Source File app/api/cards/[id]/pin/route.ts Auth Required Feature Flag cardPin (returns 404 if disabled) Request Body: { "pin": "1234" } (exactly 4 digits) PIN is hashed with bcrypt before storage. GET /api/cards/[id]/limits Get spending limits for a card. Field Source File app/api/cards/[id]/limits/route.ts Auth Required Feature Flag spendingLimits (returns 404 if disabled) PUT /api/cards/[id]/limits Set a spending limit for a card. Field Source File app/api/cards/[id]/limits/route.ts Auth Required Feature Flag spendingLimits (returns 404 if disabled) Request Body: Field Type Required Validation limitType string Yes daily , weekly , monthly , or transaction amount number Yes Must be positive Replaces any existing limit of the same type. Exchange Rates See also: Currency Rates for provider source, cache/freshness behavior, fallback, and circuit-breaker verification. GET /api/rates Get all persisted exchange rates from NOK. This endpoint reads exchange_rates ; it does not call the external FX providers directly. Field Source File apps/drop-api/src/routes/rates.ts Auth None Rate Limit 120 req/min per IP Success Response (200): { "data": { "baseCurrency": "NOK", "rates": { "RSD": 10.17, "BAM": 0.17, "PLN": 0.374, "PKR": 26.5, "TRY": 3.39, "EUR": 0.087 }, "updatedAt": "...", "stale": false } } GET /api/rates/:currency Get one persisted NOK → target-currency rate. Field Source File apps/drop-api/src/routes/rates.ts Auth None Rate Limit 120 req/min per IP Response includes fee: 0.005 (0.5% remittance fee) and stale freshness status. Notifications GET /api/notifications List all notifications for user. Field Source File app/api/notifications/route.ts Auth Required Feature Flag notifications (default: enabled) PATCH /api/notifications Mark notifications as read. Field Source File app/api/notifications/route.ts Auth Required Feature Flag notifications Request Body: { "notificationIds": ["noti_..."] } Max 100 IDs per request IDs validated against format ^[a-z]+_[a-f0-9]{16}$ Settings GET /api/settings Get user settings (creates defaults if none exist). Field Source File app/api/settings/route.ts Auth Required Defaults: currency=NOK, language=nb, pushEnabled=true, emailEnabled=true PATCH /api/settings Update user settings. Field Source File app/api/settings/route.ts Auth Required Request Body (all optional): Field Type Validation currency string Whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR language string Whitelist: nb, en, bs, sq pushEnabled boolean - emailEnabled boolean - Merchants POST /api/merchants/register Register as a merchant. Field Source File app/api/merchants/register/route.ts Auth Required Request Body: Field Type Required Validation businessName string Yes validateName() orgNumber string Yes Exactly 9 digits, unique address string No Sanitized to 300 chars bankAccount string Yes Payout account Upgrades user role to merchant . Returns a QR code URI ( drop://pay/{merchantId} ). GET /api/merchants/dashboard Get merchant dashboard stats. Field Source File app/api/merchants/dashboard/route.ts Auth Required (merchant role) Query Parameters: period — today (default), week , month Returns: revenue, transactionCount, fees, netRevenue, nextPayout, payoutTime. GET /api/merchants/qr Get merchant QR code data. Field Source File app/api/merchants/qr/route.ts Auth Required (merchant role) Returns: merchantId, businessName, qrValue ( drop://pay/{id} ), address. GET /api/merchants/transactions List merchant's QR payment transactions with pagination. Field Source File app/api/merchants/transactions/route.ts Auth Required (merchant role) Query Parameters: page , limit Customer names are partially anonymized (first name + last initial). GDPR & Compliance GET /api/user/data-export Export all user data (GDPR right to data portability). Field Source File app/api/user/data-export/route.ts Auth Required Creates a data_access_request record with type export and status completed . Success Response (200): { "data": { "user": { "id": "usr_...", "email": "...", "first_name": "...", "last_name": "...", "phone": "+47...", "date_of_birth": "1995-03-15", "kyc_status": "approved", "role": "user", "created_at": "..." }, "transactions": [ {...}, {...} ], "recipients": [ {...}, {...} ], "bankAccounts": [ {...} ], "settings": { "currency": "NOK", "language": "nb", ... }, "consents": [ {...}, {...} ] }, "exportedAt": "2026-02-17T..." } DELETE /api/user/account Request account deletion (GDPR right to erasure). Field Source File app/api/user/account/route.ts Auth Required Behavior: Soft-deletes user (sets deleted_at timestamp) Revokes all active sessions Creates data_access_request with type erasure and status completed Important: Data retained for 5 years per AML/KYC legal requirements (hvitvaskingsloven) Success Response (200): { "message": "Account scheduled for deletion", "retentionNote": "Data retained for 5 years per AML requirements" } GET /api/consents List user's GDPR consents. Field Source File app/api/consents/route.ts Auth Required Success Response (200): { "data": [ { "id": "con_...", "user_id": "usr_...", "consent_type": "terms", "granted": 1, "granted_at": "2026-02-17T...", "withdrawn_at": null, "ip_address": "192.0.2.1" } ] } POST /api/consents Grant or withdraw a consent. Field Source File app/api/consents/route.ts Auth Required Request Body: Field Type Required Validation consentType string Yes Must be one of: terms , privacy , marketing , cookies_analytics , cookies_marketing granted boolean Yes true to grant, false to withdraw Behavior: If consent exists: updates granted field and sets either granted_at or withdrawn_at If consent doesn't exist: creates new consent record Records user's IP address with consent action Success Response (200 for update, 201 for new): { "data": { "id": "con_...", "consent_type": "marketing", "granted": 1, "granted_at": "2026-02-17T...", "withdrawn_at": null, "ip_address": "192.0.2.1" } } Error Responses: Status Code Condition 400 bad_request Invalid consent type or missing fields GET /api/complaints List user's complaints. Field Source File app/api/complaints/route.ts Auth Required Query Parameters: Param Type Default Notes page int 1 Pagination page number limit int 10 Items per page, max 100 Success Response (200): { "data": [ { "id": "cmp_...", "category": "transaction", "subject": "Transaction delayed", "description": "My remittance to Serbia is delayed...", "status": "received", "resolution": null, "created_at": "2026-02-17T...", "resolved_at": null } ], "pagination": { "page": 1, "limit": 10, "total": 3, "totalPages": 1 } } POST /api/complaints Submit a complaint (Finansavtaleloven §3-53 compliance). Field Source File app/api/complaints/route.ts Auth Required Request Body: Field Type Required Validation category string Yes Must be one of: transaction , service , fees , privacy , technical , other subject string Yes Max 200 chars, sanitized description string Yes Max 2000 chars, sanitized Success Response (201): { "data": { "id": "cmp_...", "category": "fees", "subject": "High transfer fee", "description": "...", "status": "received", "created_at": "2026-02-17T..." }, "commitmentNote": "We will review and respond to your complaint within 15 business days per Finansavtaleloven §3-53" } Error Responses: Status Code Condition 400 bad_request Invalid category or empty fields POST /api/transactions/disclosure Get full transaction fee and exchange rate disclosure before initiating payment. Field Source File app/api/transactions/disclosure/route.ts Auth Required Request Body: Field Type Required Notes type string Yes remittance or qr_payment amount number Yes Must be positive currency string No Defaults to NOK recipientId string Conditional Required for remittance Success Response (200): { "amount": 2000, "fee": 10, "feePercentage": 0.5, "exchangeRate": 10.17, "receiveAmount": 20340, "receiveCurrency": "RSD", "estimatedDelivery": "1-2 business days", "totalCost": 2010 } Fee Calculation: Remittance: 0.5% of amount QR payment: 1.0% of amount Delivery Time: QR payment: "Instant" Remittance (EEA): "1-2 business days" Remittance (non-EEA): "2-4 business days" GET /api/transactions/[id]/receipt Get transaction receipt with full details. Field Source File app/api/transactions/[id]/receipt/route.ts Auth Required Success Response (200): { "data": { "transactionId": "tx_rem_1", "date": "2026-02-17T...", "type": "remittance", "amount": 2000, "currency": "NOK", "fee": 10, "exchangeRate": 10.17, "receiveAmount": 20340, "receiveCurrency": "RSD", "recipient": { "name": "Mama Jasmina", "country": "RS" }, "reference": "tx_rem_1", "status": "completed", "estimatedCompletion": null, "completedAt": "2026-02-17T..." } } Error Responses: Status Code Condition 404 not_found Transaction not found or not owned by user Health Check GET /api/health System health check (no auth required). Field Source File app/api/health/route.ts Auth None Success Response (200): { "status": "ok", "version": "0.1.0", "uptime": 3600, "db": "connected", "dbLatencyMs": 1, "timestamp": "..." } Returns 503 with status: "error" if database is unreachable. Authentication Drop Authentication System Sources: src/drop-app/src/app/api/auth/bankid/ , src/drop-api/src/lib/bankid.ts , src/drop-api/src/routes/auth.ts Overview Drop uses BankID OIDC as the sole authentication method. Email/password login has been removed to comply with PSD2/SCA requirements. Auth method: BankID OIDC (Norwegian eID) JWT Algorithm: HS256 (HMAC-SHA256), RS256 opt-in Library: jose ( SignJWT / jwtVerify ) Token lifetime: 24h (web cookie), 7d (mobile Bearer token) Web cookie: drop_token (httpOnly, secure, sameSite=strict) Mobile: Bearer token in Authorization header Phase 2 (planned) Vipps Login — same OIDC pattern, user dedup by national_id_hash Idura aggregator optional (single integration point for BankID + Vipps) Authentication Flow BankID Login (Web) Browser Next.js BFF BankID OIDC | | | | GET /api/auth/bankid | | |--------------------------->| | | | 1. Rate limit check | | | 2. Generate state + nonce | | | 3. Set bankid_state cookie | | { redirectUrl } | | |<---------------------------| | | | | | Browser redirects to BankID authorize URL | |---------------------------------------------------------->| | | | | User authenticates with BankID | | | | | BankID redirects to /api/auth/bankid/callback?code=&state= |<----------------------------------------------------------| | | | | GET /callback?code&state | | |--------------------------->| | | | 4. Verify state vs cookie | | | 5. Exchange code for tokens | | |----------------------------->| | | { id_token, access_token } | | |<-----------------------------| | | 6. Verify ID token (JWKS) | | | 7. Parse pid, verify age | | | 8. Find/create user | | | 9. Create session + cookie | | 302 /dashboard | | |<---------------------------| | BankID Login (Mobile) Mobile App Hono API BankID OIDC | | | | GET /v1/auth/bankid/initiate?platform=mobile | |--------------------------->| | | { redirectUrl, state } | | |<---------------------------| | | | | | Open BankID in secure browser (expo-web-browser) | |---------------------------------------------------------->| | | | | User authenticates with BankID | | | | | Redirect to drop://auth/callback?code=&state= | |<----------------------------------------------------------| | | | | POST /v1/auth/bankid/callback | | { code, state, platform } | |--------------------------->| | | | 1. Exchange code for tokens | | |----------------------------->| | | { id_token } | | |<-----------------------------| | | 2. Verify ID token (JWKS) | | | 3. Parse pid, verify age | | | 4. Find/create user | | | 5. Create session | | { token, data } | | |<---------------------------| | | | | | Store token in AsyncStorage | User Creation BankID login automatically creates user accounts: Parse pid from BankID ID token (Norwegian national ID, 11 digits) Hash pid with SHA-256 for storage ( national_id_hash column) Check existing user by national_id_hash If new: Create user with: kyc_status = 'approved' (BankID = verified identity) kyc_method = 'bankid' auth_provider = 'bankid' password_hash = 'EIDONLY' (sentinel — no password auth) Age check: Must be >= 18 (parsed from pid birthdate) JWT Structure Payload interface JwtPayload { userId: string; // e.g., "usr_a1b2c3d4e5f6g7h8" email: string; // e.g., "usr_xxx@bankid.drop.local" role: string; // "user" or "merchant" } Claims Claim Value exp Current time + 24h (web) / 7d (mobile) iat Current time iss drop-api (Hono) / none (Next.js) aud drop (Hono) / none (Next.js) Session Revocation On login: sessions record created with SHA-256 hash of JWT On each request: Verify session not revoked + not expired On logout: All user sessions marked revoked = 1 CSRF Protection Web: State parameter in BankID OIDC flow (stored in httpOnly cookie) API: Origin header validation against allowed origins Mobile: N/A (Bearer token, no cookies) Rate Limiting Endpoint Limit BankID initiate 10/min per IP BankID callback 10/min per IP Auth me/logout/refresh No additional limit (auth required) Authorization Role-Based Access Two roles: user and merchant . Route Auth Role GET /auth/bankid/initiate None - POST /auth/bankid/callback None - GET /auth/me Required Any POST /auth/logout Required Any POST /auth/refresh Required Any POST /merchants/register Required Any (upgrades to merchant) GET /merchants/dashboard Required Merchant Deprecated Endpoints These endpoints return 410 Gone : Endpoint Replacement POST /auth/login BankID OIDC flow POST /auth/register Automatic via BankID login POST /auth/verify-otp Not needed (BankID replaces OTP) Environment Variables Required (Production) BANKID_CLIENT_ID # BankID OIDC client ID BANKID_CLIENT_SECRET # BankID OIDC client secret BANKID_CALLBACK_URL # Web callback URL (e.g., https://getdrop.no/api/auth/bankid/callback) BANKID_CALLBACK_URL_MOBILE # Mobile deep link (e.g., drop://auth/callback) JWT_SECRET # JWT signing secret (min 32 chars) Optional BANKID_AUTHORIZE_URL # Default: BankID prod authorize endpoint BANKID_TOKEN_URL # Default: BankID prod token endpoint BANKID_JWKS_URL # Default: BankID prod JWKS endpoint BANKID_ISSUER # Default: BankID prod issuer BANKID_MOCK=true # Dev mode: mock OIDC flow (no real BankID needed) JWT_ALGORITHM # "HS256" (default) or "RS256" JWT_EXPIRY # Default: "24h" Merchant Flow Merchants use the same BankID login as regular users. After logging in: Navigate to merchant registration Fill in business details (business name, org number, bank account) POST /merchants/register with auth token User role upgraded from user to merchant Merchant dashboard becomes accessible