API Reference
API Reference
Project:
{{PROJECT_NAME}}Drop Version:{{VERSION}}0.1.0 Date:{{DATE}}2026-02-23 Author:{{AUTHOR}}Platform Architect (AI) Status:Draft |In Review| ApprovedReviewers:{{REVIEWERS}}Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 |
1.Overview
Drop uses Next.js App Router API Overviewroutes &(app/api/). ConventionsThe application
uses a APIPSD2 style:pass-through model money. {{RESTful— HTTP/JSONDrop |never GraphQLholds |customer gRPC}}APIAll versionfunds strategy:remain uses {{URLin versioning:users' /v1/bank |accounts; HeaderDrop 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,/productsHTTP methods mapAISP toCRUD:readGETbalances(read),andPOSTPISP(create),toPUTinitiate(replace),payments.PATCH (update), DELETE (remove)Response format: always JSONTimestamps: ISO 8601 UTC (2024-01-15T10:30:00Z)IDs: UUID v4 stringsBooleans:true/false(never1/0)Empty collections:[](nevernull)Missing optional fields: omitted (nevernullunless semantically null)
2. Base URLs
| Environment | Base URL |
|---|---|
(future: https://getdrop.no) |
|
| Staging | https:// |
|
Response Envelope
// Success
{ "data": {domain.com} ... } }
/v1/ Success (list)
{ "data": [...], "pagination": { "page": 1, "limit": 20, "total": 100 } }
// Error
{ "error": "error_code", "message": "Human-readable message", "details": [...] }
Authentication
Authentication is via httpOnly cookie (drop_token) containing a signed JWT (HS256, 24h expiry). BankID OIDC is the sole login method.
Mobile clients use Authorization: Bearer <token> (7d expiry, stored in AsyncStorage).
Rate Limits
| Endpoint Group | Limit |
|---|---|
| BankID initiate/callback | 10/min per IP |
| Transaction creation (remittance, qr-payment) | 10/min per IP |
| Exchange rates | 120/min per IP |
| All other endpoints | No IP-level limit (auth required) |
3.Authentication AuthenticationEndpoints
GET /api/auth/bankid
Initiate BankID OIDC login flow.
| Property | Value |
|---|---|
| Auth | None |
| Rate Limit | 10/min per IP |
Sets bankid_state httpOnly cookie. Returns { "redirectUrl": "https://bankid.no/authorize?..." }.
GET /api/auth/bankid/callback
BankID OIDC callback — creates/finds user, issues session cookie.
| Property | Value |
|---|---|
| Auth | None (validates state cookie) |
| Rate Limit | 10/min per IP |
Method:Query params: Bearercode, Token (JWT)state
ObtainSuccess: tokens:Redirects to , sets POST/dashboarddrop_token cookie. Creates user with kyc_status=approved on first login. Verifies age >= 18 from BankID pid.
GET /api/auth/loginme
Get current authenticated user with linked bank accounts.
| Property | Value |
|---|---|
| Auth | Required ( |
IncludeResponse in requests:(200):
Authorization:{
Bearer"data": <access_token>{
"id": "usr_a1b2c3",
"email": "[email protected]",
"firstName": "Amir",
"lastName": "Bašić",
"totalBalance": 58030.0,
"bankAccounts": [
{ "id": "ba_1", "bankName": "DNB", "accountNumber": "1234.56.78901",
"balance": 45230.0, "currency": "NOK", "isPrimary": true }
],
"kycStatus": "approved",
"createdAt": "2026-01-01T00:00:00.000Z"
}
}
TokenNote: lifetimes:balance values are AISP-read from user's real bank — NOT Drop-held funds.
AccessPOST
token:/api/auth/logout15Logout
minutes— Refreshrevoketoken:all30sessions.daysProperty Value Auth Required ( rotatecookie)onuse)RefreshResponsetokens:(200):POST /auth/refreshwith{ "refreshToken"message": "Logged out" }
POST /api/auth/refresh
Refresh JWT token.
Property Value Auth Required (cookie) Response (200):
{ "data": { "userId": "usr_...", "email": "...", "role": "user" } }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.Deprecated
CommonEndpointsRequest/Response(410Headers4.1 Request HeadersGone)HeaderEndpointRequiredDescriptionReplacementAuthorizationPOST /api/auth/loginYesBankID(auth routes)OIDCBearer <token>Content-TypePOST /api/auth/register viaYesAuto(POST/PUT/PATCH)BankIDapplication/jsonAcceptPOST /api/auth/verify-otp BankIDNoreplacesapplication/json(default)X-Request-IDNoClient-provided idempotency IDAccept-LanguageNoen,nb, etc. — affects response localeOTP
Transaction Endpoints
4.2GETResponse/api/transactionsHeadersList user transactions with pagination and filtering.
HeaderPropertyDescriptionValueAuth Required Query Parameters:
Param Type Default Notes Content-Typepageint 1 Min 1 application/json;limitcharset=utf-8int 20 Min 1, Max 50 typestring — remittanceorqr_paymentX-Request-IDstatusEcho of client request ID (or server-generated)string— ,X-RateLimit-LimitprocessingTotalcompleted,requestsorallowed in windowX-RateLimit-RemainingfailedRemaining requests in current windowX-RateLimit-ResetUnix timestamp when window resetsRetry-AfterSeconds to wait (set when 429 returned)5. ErrorResponse
Format(200):{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details"data": [ { "field"id": "email"tx_rem_1", "message"type": "Invalid email format"remittance", "code"status": "INVALID_FORMAT"completed", "amount": -2000, "currency": "NOK", "recipientName": "Mama Jasmina", "createdAt": "..." } ], "requestId"pagination":"req_7f3a2b1c",{ "timestamp"page": 1, "2024-01-15T10:30:00Z"limit": 20, "total": 3 } }
GET /api/transactions/[id]
StandardGeterrorsinglecodes:transaction details.HTTP StatusPropertyError CodeMeaningValue400AuthVALIDATION_ERRORRequest body / params failed validation400BAD_REQUESTMalformed request401UNAUTHORIZEDMissing or invalid authentication401TOKEN_EXPIREDJWT has expired — refresh required403FORBIDDENAuthenticated but lacks permission404NOT_FOUNDResource does not exist409CONFLICTResource already exists / version conflict422UNPROCESSABLEValid format but business rule violation429RATE_LIMITEDToo many requests500INTERNAL_ERRORUnexpected server error503SERVICE_UNAVAILABLETemporary downtimeRequired6. Resources6.1 AuthenticationPOST /auth/loginAuthenticate user and receive token pair.Auth required:NoRequest body:{ "email": "[email protected]", "password": "{{password}}" }Response
(200):200 OK{ "accessToken": "eyJhbGciOiJIUzI1NiJ9...", "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g...", "expiresIn": 900, "user"data": { "id": "usr_01HX7.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": "...", "email"completedAt": "[email protected]",..."name": "John Doe", "role": "user"} }
GET /api/transactions/summary
Get all-time + this-month statistics.
Property Value Auth Required ErrorResponseresponses:(200):{ "data": { "allTime": { totalCount, totalSent, totalPaid, remittanceCount, qrPaymentCount }, "thisMonth": { ... } } }
POST /api/transactions/remittance
Create international money transfer.
Property Value Auth Required Rate Limit 10/min per IP KYC kyc_statusmust beapprovedRequest Body:
Field Type Required Validation recipientIdstring Yes Must belong to current user amountnumber Yes 100–50,000 NOK, max 2 decimal places currencystring No Defaults to NOKbankAccountIdstring No Defaults to primary bank account Business logic: Fee = 0.5% of amount. Atomic: debit bank account + create transaction (status:
processing).Response (201): Full transaction object with exchange rate, fee, ETA.
Errors:
Status Code Condition 401400INVALID_CREDENTIALSbad_requestWrongMissing/invalidemail or passwordfields429400RATE_LIMITEDno_bank_account>No5linkedfailedbankattemptsaccountin15min402 insufficient_balanceBalance too low 403 kyc_requiredKYC not approved 404 not_foundRecipient not found 422 validation_errorUnsupported currency corridor
api/transactions/qr-paymentPOST /
auth/refreshRotateCreateaccessQRandpaymentrefreshtotokens.a merchant.Property Value Auth required:Required NoRate Limit 10/min per IP Request
body:{ "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g..." }Response200 OK:Same as login response.POST /auth/logoutRevoke refresh token.Auth required:Yes (Bearer)Request body:{ "refreshToken": "dGhpcyBpcyBhIHJlZnJlc2g..." }Response204 No Content6.2 UsersEndpoints:Body:MethodFieldPathTypeDescriptionRequiredValidation merchantIdstring Yes Must exist amountnumber Yes 1–100,000 NOK, max 2 decimal places Business logic: Fee = 1% of amount. Instant (status:
completed).
POST /api/transactions/disclosure
Get fee + exchange rate disclosure before payment (Finansavtaleloven compliance).
Property Value Auth Required Request Body:
{ type, amount, currency?, recipientId? }Response (200):
{ "amount": 2000, "fee": 10, "feePercentage": 0.5, "exchangeRate": 10.17, "receiveAmount": 20340, "receiveCurrency": "RSD", "estimatedDelivery": "1-2 business days", "totalCost": 2010 }
GET /api/transactions/[id]/receipt
Get transaction receipt.
Property Value Auth Required Returns full receipt with amounts, fees, exchange rate, recipient info. 404 if not owned by user.
Recipient Endpoints
GET /api/recipients
List saved recipients. Bank account numbers masked (
*****5678).Property Value Auth Required Query Params:
page,limit(max 50). Supported countries: RS, BA, PL, PK, TR.
POST /api/recipients
Add a recipient.
Property Value Auth Required Request Body:
{ name, country, currency, bankAccount, bankName? }
DELETE /api/recipients/[id]
Delete a recipient. Response (204). 404 if not found/owned.
Property Value Auth Required
Exchange Rate Endpoints
GET /api/rates
Get all NOK exchange rates (public).
Property Value Auth None Rate Limit 120/min per IP 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 specific currency from NOK. Includes
fee: 0.005(0.5% remittance fee).Property Value Auth None Rate Limit 120/min per IP
Merchant Endpoints
POST /api/merchants/register
Register as merchant (upgrades role to
merchant).Property Value Auth Required Request Body:
Field Type Required Validation businessNamestring Yes validateName()orgNumberstring Yes Exactly 9 digits, unique addressstring No Sanitized to 300 chars bankAccountstring Yes Payout account Returns QR code URI:
drop://pay/{merchantId}
GET /api/merchants/dashboard
Merchant revenue stats.
Property Value Auth Required (merchant role) Query Params:
period=today(default) /week/monthReturns:
{ revenue, transactionCount, fees, netRevenue, nextPayout, payoutTime }
GET /api/merchants/qr
Get merchant QR code data.
Property Value Auth Required (merchant role) Returns:
{ merchantId, businessName, qrValue: "drop://pay/{id}", address }
GET /api/merchants/transactions
List merchant's QR payment transactions. Customer names partially anonymized.
Property Value Auth Required (merchant role)
Notification Endpoints
GET /api/notifications
Property Value Auth Required Feature Flag notifications(default: enabled)
PATCH /api/notifications
Mark notifications as read. Max 100 IDs per request.
Property Value Auth Required Feature Flag notificationsRequest Body:
{ "notificationIds": ["noti_..."] }
Settings Endpoints
GET /api/settings
Get user settings (auto-creates defaults:
currency=NOK, language=nb).Property Value Auth Required
PATCH /api/settings
Update user settings.
Property Value Auth Required Request Body (all optional):
{ currency?, language?, pushEnabled?, emailEnabled? }Currency whitelist: EUR, USD, GBP, BAM, CHF, PLN, NOK, RSD, TRY, PKR Language whitelist: nb, en, bs, sq
GDPR & Compliance Endpoints
GET /api/user/data-export
Export all user data (GDPR Art. 20 — right to portability).
Property Value Auth Required Returns full export: user profile, transactions, recipients, bank accounts, settings, consents.
DELETE /api/user/account
Request account deletion (GDPR Art. 17 — right to erasure). Data retained 5 years per hvitvaskingsloven.
Property Value Auth Required Response (200):
{ "message": "Account scheduled for deletion", "retentionNote": "Data retained for 5 years per AML requirements" }
GET /api/consents
List GDPR consents.
Property Value Auth Required
POST /api/consents
Grant or withdraw consent.
Property Value Auth Required Request Body:
Field Type Validation consentTypestring terms,privacy,marketing,cookies_analytics,cookies_marketinggrantedboolean trueto grant,falseto withdrawRecords IP address with consent action.
GET /api/complaints
List user's complaints.
Property Value Auth Required Query Params:
page,limit(max 100)
POST /api/complaints
Submit a complaint (Finansavtaleloven §3-53 — 15 business day response SLA).
Property Value Auth Required Request Body:
Field Type Validation categorystring transaction,service,fees,privacy,technical,othersubjectstring Max 200 chars descriptionstring Max 2000 chars
Cards Endpoints (FUTURE — Feature-Flagged, All Disabled)
All card endpoints are behind feature flags defaulting to
false. Requires card issuing partner before activation.Endpoint Feature Flag GET /api/cardsvirtualCardsPOST /usersapi/cardsList users (paginated)AdminvirtualCardsGET /api/cards/[id]cardDetailsPATCH /users/:idapi/cards/[id]GetcardFreezeuserbyIDDELETE /api/cards/[id]Self or AdminvirtualCardsPOST /api/cards/[id]/physical/usersCreate userAdminPATCH/users/:idUpdate user fieldsSelf or AdminDELETE/users/:idDelete user (soft)AdminGET/users/meGet current userAuthenticatedPATCH/users/meUpdate current userAuthenticatedGET /usersList users with pagination and filtering.Auth required:AdminQuery parameters:ParameterTypeDefaultDescriptionpageinteger1Page numberpageSizeinteger25Items per page (max: 100)searchstring—Search name or email (min 2 chars)rolestring—Filter by role:admin,user,viewerphysicalCardsstatusPOST /api/cards/[id]/pinstringactiveFilter by status:active,inactive,allcardPinsortGET/PUT /api/cards/[id]/limitsstringcreatedAtSort fielddirstringdescSort direction:asc,descspendingLimitsCard numbers always masked (
---- ---- ---- XXXX). CVV never exposed. PCI-DSS compliant masking.
Health Check
GET /api/health
System health — no auth required.
Response
(200):200 OK{ "data":[{"id": "usr_01HX7...", "email": "[email protected]", "name": "Jane Doe", "role": "user","status": "active"ok", "createdAt"version": "2024-01-15T10:30:00Z"0.1.0", "updatedAt"uptime":"2024-01-15T10:30:00Z" } ],3600, "pagination"checks": { "page"db":1, "pageSize": 25, "total": 142, "totalPages": 6 } }GET /users/:idAuth required:Self or AdminPath parameters:ParameterTypeDescriptionidUUIDUser IDResponse200 OK:{"id": "usr_01HX7...", "email": "[email protected]", "name": "Jane Doe", "role": "user","status": "active"pass", "profile"latencyMs": 2, "driver": "pg" }, "services": { "avatarUrl"mode": "https://cdn.domain.com/avatars/...",production""bio": "Software developer"} }, "createdAt"timestamp": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z" }Error responses:StatusCodeCondition404NOT_FOUNDUser does not exist403FORBIDDENNon-admin accessing another userPOST /usersAuth required:AdminRequest body:{ "email": "[email protected]", "name": "New User", "role": "user", "password": "{{password}}" }Response201 Created:Full user object (same as GET /users/:id)PATCH /users/:idAuth required:Self or AdminRequest body(all fields optional):{ "name": "Updated Name", "profile": { "bio": "Updated bio"2026-02-23T12:00:00.000Z" } }Response
— DB unreachable):200(503OKUpdated user object.DELETE /users/:idSoft-deletes user (setsdeletedAt, anonymizes PII).Auth required:AdminResponse204 No Content6.3 {{RESOURCE_NAME}}Endpoints:MethodPathDescriptionAuthGET/{{resource}}List{{resource}}{{Auth level}}GET/{{resource}}/:idGet by ID{{Auth level}}POST/{{resource}}Create{{Auth level}}PATCH/{{resource}}/:idUpdate{{Auth level}}DELETE/{{resource}}/:idDelete{{Auth level}}TODO:Document all endpoints for this resource following the pattern above.7. Pagination FormatAll list endpoints return the same pagination envelope:{ "data":[...]{ "status": "down", "pagination"checks": { "page"db":1,{ "pageSize"status":25,"total": 142, "totalPages": 6, "hasNextPage": true, "hasPreviousPage": falsefail" } } } }Cursor pagination(high-performance, for infinite scroll):GET /feed?cursor=eyJpZCI6MTIzfQ&pageSize=20Response includesnextCursor— pass ascursorin next request.
8.RelatedFiltering & Sorting ConventionsDocumentsFilterparameters:- Backend
GETArchitecture/orders?status=pending&createdAt[gte]=2024-01-01&total[lte]=1000- Middleware Design
Sort:?sort=createdAt&dir=desc(default:createdAt desc)9. Webhooks DocumentationWebhook 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:EventTriggeruser.createdNew user registereduser.updatedUser profile changeduser.deletedUser account deleted{{resource}}.{{action}}{{Description}}Retry policy:5 retries with exponential backoff. Undeliverable after 24 hours → marked as failed.10. Rate LimitingEndpoint GroupLimitWindowPublic endpoints100 req1 minuteAuthenticated endpoints1000 req1 minuteAdmin endpoints5000 req1 minuteAuth endpoints (login)5 req15 minutesWebhook deliveryN/A—Response when rate limited (429 Too Many Requests):{ "error": { "code": "RATE_LIMITED", "message": "Too many requests. Please retry after 60 seconds.", "retryAfter": 60 } }11. Code ExamplescURL# 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 token) curl -X GET "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 (!response.ok) { const error = await response.json(); throw new Error(error.error.message); } const { data, pagination } = await response.json();Pythonimport 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 AvailabilityLanguagePackageRepositoryStatusTypeScript / JavaScript@{{company}}/api-client{{URL}}{{Available/Planned}}Python{{company}}-python{{URL}}{{Available/Planned}}Go{{company}}-go{{URL}}{{Planned}}- Backend
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Platform Architect (AI) | 2026-02-23 | |
| Alem | |||
| Bašić |