API Reference
Drop Backend API Reference
Auto-generatedProject:from{{PROJECT_NAME}}sourceVersion:code{{VERSION}}analysis.Date:All{{DATE}}fileAuthor:references{{AUTHOR}}areStatus:relativeDraftto|src/drop-app/src/.In Review | Approved Reviewers: {{REVIEWERS}}
OverviewDocument History
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 BankingPISP(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.
| Author | Changes | ||
|---|---|---|---|
| |||
1. API Overview & Conventions
RequestAPI Body:style: {{RESTful HTTP/JSON | GraphQL | gRPC}}
API version strategy: {{URL versioning: /v1/ | Header versioning}}
OpenAPI spec: {{https://api.domain.com/docs/openapi.json}}
Interactive docs: {{https://api.domain.com/docs}}
Design conventions:
- Resources named as plural nouns:
/users,/orders,/products - HTTP methods map to CRUD: GET (read), POST (create), PUT (replace), PATCH (update), DELETE (remove)
- Response format: always JSON
- Timestamps: ISO 8601 UTC (
2024-01-15T10:30:00Z) - IDs: UUID v4 strings
- Booleans:
true/false(never1/0) - Empty collections:
[](nevernull) - Missing optional fields: omitted (never
nullunless semantically null)
2. Base URLs
http://localhost:4000/v1 |
|||
https://api-staging.{{domain.com}}/v1 |
|||
| |||
| |||
3. Authentication
SuccessMethod: Bearer Token (JWT)
Obtain tokens: POST /auth/login (see Auth section below)
Include in requests:
Authorization: Bearer <access_token>
Token lifetimes:
- Access token: 15 minutes
- Refresh token: 30 days (rotate on use)
Refresh tokens: POST /auth/refresh with { "refreshToken": "..." } in body.
API Key authentication (machine-to-machine):
X-API-Key: <api_key>
API keys are scoped and managed at {{https://dashboard.domain.com/api-keys}}.
4. Common Request/Response Headers
4.1 Request Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes (auth routes) | Bearer <token> |
Content-Type |
Yes (POST/PUT/PATCH) | application/json |
Accept |
No | application/json (default) |
X-Request-ID |
No | Client-provided idempotency ID |
Accept-Language |
No | en, nb, etc. — affects response locale |
4.2 Response Headers
| Header | Description |
|---|---|
Content-Type |
application/json; charset=utf-8 |
X-Request-ID |
Echo of client request ID ( |
X-RateLimit-Limit |
Total requests allowed in window |
X-RateLimit-Remaining |
Remaining requests in current window |
X-RateLimit-Reset |
Unix timestamp when window resets |
Retry-After |
Seconds to wait (set when 429 returned) |
5. Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"code": "INVALID_FORMAT"
}
],
"requestId": "req_7f3a2b1c",
"timestamp": "2024-01-15T10:30:00Z"
}
}
Standard error codes:
| HTTP Status | Error Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR |
Request body / params failed validation |
| 400 | BAD_REQUEST |
Malformed request |
| 401 | UNAUTHORIZED |
Missing or invalid authentication |
| 401 | TOKEN_EXPIRED |
JWT has expired — refresh required |
| 403 | FORBIDDEN |
Authenticated but lacks permission |
| 404 | NOT_FOUND |
Resource does not exist |
| 409 | CONFLICT |
Resource already exists / version conflict |
| 422 | UNPROCESSABLE |
Valid format but business rule violation |
| 429 | RATE_LIMITED |
Too many requests |
| 500 | INTERNAL_ERROR |
Unexpected server error |
| 503 | SERVICE_UNAVAILABLE |
Temporary downtime |
6. Resources
6.1 Authentication
POST /auth/login
Authenticate user and receive token pair.
Auth required: No
Request body:
{
"email": "[email protected]",
"password": "{{password}}"
}
Response 200 OK:
{
"data"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g...",
"expiresIn": 900,
"user": {
"id": "usr_.usr_01HX7...",
"email": "[email protected]",
"firstName"name": "..."John Doe",
"lastName"role": "...",
"dateOfBirth": "...",
"kycStatus": "pending",
"createdAt": "2026-..."user"
}
}
Error Responses:responses:
| Status | Code | Condition |
|---|---|---|
| ||
POST /api/auth/login
Authenticate with email and password.
|
|
Request Body:
Success Response (200):
{
"data": {
"id": "usr_...",
"email": "...",
"firstName": "...",
"lastName": "...",
"kycStatus": "approved"
}
}
Error Responses:
| 429 | RATE_LIMITED |
GETPOST /api/auth/merefresh
Rotate access and refresh tokens.
Auth required: No
Request body:
{ "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g..." }
Response 200 OK: Same as login response.
POST /auth/logout
Revoke refresh token.
Auth required: Yes (Bearer)
Request body:
{ "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g..." }
Response 204 No Content
6.2 Users
Get current authenticated user with bank accounts.Endpoints:
| Description | Auth | ||
|---|---|---|---|
GET |
|
List users (paginated) | Admin |
GET |
/users/:id |
Get user by ID | Self or Admin |
POST |
/users |
Create user | Admin |
PATCH |
/users/:id |
Update user fields | Self or Admin |
DELETE |
/users/:id |
Delete user ( |
Admin |
GET |
/users/me |
Get current user | Authenticated |
PATCH |
/users/me |
Update current user | Authenticated |
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": "..."
}
}
POSTGET /api/auth/logout
users
LogoutList users with pagination and revokefiltering.
Auth sessions.required: Admin
Query parameters:
| Default | Description | ||
|---|---|---|---|
page |
integer | |
Page number |
pageSize |
integer | 25 |
Items per page (max: 100) |
search |
string | — | Search name or email (min 2 chars) |
role |
string | — | Filter by role: admin, user, viewer |
status |
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).
|
Filter by status: active, inactive, all |
|
sort |
Success Response (200):
{
"data": {
"userId": "usr_...",
"email": "...",
"role": "user"
}
}
Transactions
GET /api/transactions
List user's transactions with pagination and filtering.
|
Sort field | |
Query Parameters:
dir |
string | desc |
Sort |
, |
Success Response (200)200 OK:
{
"data": [
{
"id": "tx_rem_1"usr_01HX7...",
"type"email": "remittance"[email protected]",
"name": "Jane Doe",
"role": "user",
"status": "completed",
"amount": -2000,
"currency": "NOK",
"recipientName": "Mama Jasmina"active",
"createdAt": "...2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"page": 1,
"limit"pageSize": 20,25,
"total": 3142,
"totalPages": 6
}
}
Note: amount is negated in the response (always shown as outgoing).
GET /api/transactions/[id]
users/:id
GetAuth singlerequired: transactionSelf detailsor withAdmin
Path rate info.parameters:
| Description | ||
|---|---|---|
id |
| |
Success Response (200)200 OK:
{
"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).
| |
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).
| |
|
Request Body:
| |||
Business Logic:
Verify recipient belongs to userLook up exchange rate for recipient's currencyVerify bank account exists and has sufficient balanceFee: 0.5% of amountDebit bank account (atomic transaction)Create transaction record with statusprocessing
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:
POST /api/transactions/qr-payment
Create a QR payment to a merchant.
| |
Request Body:
Business Logic:
Verify merchant existsGet user's primary bank accountFee: 1% of amountDebit bank account (atomic transaction)Create transaction with statuscompleted(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.
| |
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.
| |
Request Body:
| |||
DELETE /api/recipients/[id]
Delete a recipient.
| |
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 tofalse. 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).
| |
POST /api/cards
Create a new card (virtual or physical).
| |
Request Body:
|
GET /api/cards/[id]
Get card details. Card number is masked (---- ---- ---- XXXX), CVV is hidden (---).
| |
PCI-DSS compliant: never exposes full card number or CVV.
PATCH /api/cards/[id]
Freeze or unfreeze a card.
| |
Request Body: { "status": "active" | "frozen" }
DELETE /api/cards/[id]
Cancel a card (soft delete — sets status to cancelled).
| |
POST /api/cards/[id]/physical
Order physical version of a virtual card.
| |
|
Request Body: { "address": "..." } (min 10 chars)
POST /api/cards/[id]/pin
Set PIN for a card.
| |
|
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.
| |
|
PUT /api/cards/[id]/limits
Set a spending limit for a card.
| |
|
Request Body:
| |||
Replaces any existing limit of the same type.
Exchange Rates
GET /api/rates
Get all exchange rates from NOK.
| |
Success Response (200):
{
"data": {
"baseCurrency": "NOK",
"rates": { "RSD": 11.7, "BAM": 1.04, "PLN": 0.41, "PKR": 26.8, "TRY": 3.45, "EUR": 0.089 },
"updatedAt": "..."
}
}
GET /api/rates/[currency]
Get rate for a specific currency pair.
| |
Response includes fee: 0.005 (0.5% remittance fee).
Notifications
GET /api/notifications
List all notifications for user.
| |
|
PATCH /api/notifications
Mark notifications as read.
| |
|
Request Body: { "notificationIds": ["noti_..."] }
Max 100 IDs per requestIDs validated against format^[a-z]+_[a-f0-9]{16}$
Settings
GET /api/settings
Get user settings (creates defaults if none exist).
| |
Defaults: currency=NOK, language=nb, pushEnabled=true, emailEnabled=true
PATCH /api/settings
Update user settings.
| |
Request Body (all optional):
Merchants
POST /api/merchants/register
Register as a merchant.
| |
Request Body:
| |||
Upgrades user role to merchant. Returns a QR code URI (drop://pay/{merchantId}).
GET /api/merchants/dashboard
Get merchant dashboard stats.
| |
Query Parameters: period — today (default), week, month
Returns: revenue, transactionCount, fees, netRevenue, nextPayout, payoutTime.
GET /api/merchants/qr
Get merchant QR code data.
| |
Returns: merchantId, businessName, qrValue (drop://pay/{id}), address.
GET /api/merchants/transactions
List merchant's QR payment transactions with pagination.
| |
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).
| |
Creates a data_access_request record with type export and status completed.
Success Response (200):
{
"data": {
"user": {
"id": "usr_.usr_01HX7...",
"email": "..."[email protected]",
"first_name"name": "...",Jane "last_name": "...",
"phone": "+47...",
"date_of_birth": "1995-03-15",
"kyc_status": "approved"Doe",
"role": "user",
"created_at"status": "..."
}active",
"transactions": [ {...}, {...} ],
"recipients": [ {...}, {...} ],
"bankAccounts": [ {...} ],
"settings"profile": {
"currency"avatarUrl": "NOK", "language": "nb", https://cdn.domain.com/avatars/... },
"consents": [ {...}, {...} ]
},
"exportedAt": "2026-02-17T..."
}
DELETE /api/user/account
Request account deletion (GDPR right to erasure).
| |
Behavior:
Soft-deletes user (setsdeleted_attimestamp)Revokes all active sessionsCreatesdata_access_requestwith typeerasureand statuscompletedImportant: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.
| |
Success Response (200):
{
"data": [
{
"id": "con_...",
"user_id"bio": "usr_..."Software developer"
},
"consent_type"createdAt": "terms"2024-01-15T10:30:00Z",
"granted": 1,
"granted_at"updatedAt": "2026-02-17T...",
"withdrawn_at": null,
"ip_address": "192.0.2.1"
}
]
}
POST /api/consents
Grant or withdraw a consent.
| |
Request Body:
| |||
|
Behavior:
If consent exists: updatesgrantedfield and sets eithergranted_atorwithdrawn_atIf consent doesn't exist: creates new consent recordRecords 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"
}2024-01-15T10:30:00Z"
}
Error Responses:
GET /api/complaints
List user's complaints.
| |
Query Parameters:
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).
| |
Request Body:
| |||
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:
POST /api/transactions/disclosure
Get full transaction fee and exchange rate disclosure before initiating payment.
| |
Request Body:
| |||
| |||
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 amountQR 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.
| |
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:responses:
| Status | Code | Condition |
|---|---|---|
| 404 | NOT_FOUND |
|
| 403 | FORBIDDEN |
Non-admin accessing another user |
HealthPOST Check
/users
Auth required: Admin
Request body:
{
"email": "[email protected]",
"name": "New User",
"role": "user",
"password": "{{password}}"
}
Response 201 Created: Full user object (same as GET /api/healthusers/:id)
PATCH /users/:id
Auth required: Self or Admin
Request body (all fields optional):
{
"name": "Updated Name",
"profile": {
"bio": "Updated bio"
}
}
Response 200 OK: Updated user object.
DELETE /users/:id
Soft-deletes user (sets deletedAt, anonymizes PII).
Auth required: Admin
Response 204 No Content
6.3 {{RESOURCE_NAME}}
System health check (no auth required).Endpoints:
| Description | Auth | ||
|---|---|---|---|
GET |
|
List {{resource}} |
{{Auth level}} |
GET |
/{{resource}}/:id |
Get by ID | {{Auth level}} |
POST |
/{{resource}} |
Create | {{Auth level}} |
PATCH |
/{{resource}}/:id |
Update | {{Auth level}} |
DELETE |
/{{resource}}/:id |
Delete | {{Auth level}} |
SuccessTODO: Document all endpoints for this resource following the pattern above.
7. Pagination Format
All list endpoints return the same pagination envelope:
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 25,
"total": 142,
"totalPages": 6,
"hasNextPage": true,
"hasPreviousPage": false
}
}
Cursor pagination (high-performance, for infinite scroll):
GET /feed?cursor=eyJpZCI6MTIzfQ&pageSize=20
Response includes nextCursor — pass as cursor in next request.
8. Filtering & Sorting Conventions
Filter parameters:
GET /orders?status=pending&createdAt[gte]=2024-01-01&total[lte]=1000
| Operator | Suffix | Example |
|---|---|---|
| Equals | ( |
?status=active |
| Greater than | [gt] |
?price[gt]=100 |
| Greater than or equal | [gte] |
?price[gte]=100 |
| Less than | [lt] |
?price[lt]=500 |
| Less than or equal | [lte] |
?price[lte]=500 |
| In list | [in] |
?status[in]=active,pending |
| Not in list | [nin] |
?status[nin]=deleted |
Sort: ?sort=createdAt&dir=desc (default: createdAt desc)
9. Webhooks Documentation
Webhook endpoint: Configured per-account at {{https://dashboard.domain.com/webhooks}}
Delivery: HTTP POST with JSON body, signed with HMAC-SHA256.
Signature verification:
const signature = req.headers['x-webhook-signature'];
const computed = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody)
.digest('hex');
const valid = crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(computed)
);
Event envelope:
{
"id": "evt_01HX7...",
"type": "user.created",
"data": { /* resource object */ },
"timestamp": "2024-01-15T10:30:00Z",
"version": "1"
}
Available events:
| Event | Trigger |
|---|---|
user.created |
New user registered |
user.updated |
User profile changed |
user.deleted |
User account deleted |
{{resource}}.{{action}} |
{{Description}} |
Retry policy: 5 retries with exponential backoff. Undeliverable after 24 hours → marked as failed.
10. Rate Limiting
| Endpoint Group | Limit | Window |
|---|---|---|
| Public endpoints | 100 req | 1 minute |
| Authenticated endpoints | 1000 req | 1 minute |
| Admin endpoints | 5000 req | 1 minute |
| Auth endpoints (login) | 5 req | 15 minutes |
| Webhook delivery | N/A | — |
Response when rate limited (429 Too Many Requests):
{
"status"error": {
"code": "ok"RATE_LIMITED",
"version"message": "0.1.0"Too many requests. Please retry after 60 seconds.",
"uptime"retryAfter": 3600,60
"db": "connected",
"dbLatencyMs": 1,
"timestamp": "..."}
}
Returns
11. Code Examples
cURL
# Login
curl -X POST https://api.domain.com/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "secret"}'
# Get users (with status:token)
curl -X GET "error"https://api.domain.com/v1/users?page=1&pageSize=10" \
-H "Authorization: Bearer <access_token>"
JavaScript (fetch)
const response = await fetch('https://api.domain.com/v1/users', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
if database(!response.ok) is{
unreachable.const error = await response.json();
throw new Error(error.error.message);
}
const { data, pagination } = await response.json();
Python
import httpx
client = httpx.Client(
base_url="https://api.domain.com/v1",
headers={"Authorization": f"Bearer {access_token}"}
)
response = client.get("/users", params={"page": 1, "pageSize": 10})
response.raise_for_status()
result = response.json()
12. SDK Availability
| Language | Package | Repository | Status |
|---|---|---|---|
| TypeScript / JavaScript | @{{company}}/api-client |
{{URL}} |
{{Available/Planned}} |
| Python | {{company}}-python |
{{URL}} |
{{Available/Planned}} |
| Go | {{company}}-go |
{{URL}} |
{{Planned}} |
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | |||
| Backend Lead | |||
| Tech Lead | |||
| Product Owner |