# 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:

```json
{ "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{ "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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:**

1. Verify recipient belongs to user
2. Look up exchange rate for recipient's currency
3. Verify bank account exists and has sufficient balance
4. Fee: 0.5% of amount
5. Debit bank account (atomic transaction)
6. Create transaction record with status `processing`

**Success Response (201):**

```json
{
  "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:**

1. Verify merchant exists
2. Get user's primary bank account
3. Fee: 1% of amount
4. Debit bank account (atomic transaction)
5. Create transaction with status `completed` (instant)

**Success Response (201):**

```json
{
  "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](CURRENCY-RATES.md) 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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "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):**

```json
{
  "status": "ok",
  "version": "0.1.0",
  "uptime": 3600,
  "db": "connected",
  "dbLatencyMs": 1,
  "timestamp": "..."
}
```

Returns 503 with `status: "error"` if database is unreachable.