External Services Integration
External Services Integration
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 | Initial draft from source code and services analysis |
1. Integration InventoryOverview
Drop integrates with the following external services. Service mode is controlled by NEXT_PUBLIC_SERVICE_MODE env var (mock | production).
| Service | ||
|---|---|---|
| BankID OIDC | PRODUCTION | Norwegian eID authentication — mandatory for all users |
| Sumsub | PRODUCTION | KYC/identity verification — WebSDK + webhook |
| Open Banking (AISP/PISP) | TBD — provider selection pending | Read bank balance, initiate payments |
| Slack | PRODUCTION | Operational alerting via incoming webhook |
| BetterStack | PRODUCTION | External uptime monitoring |
| Swan Open Banking | DEPRECATED | Was planned, no longer selected |
| Stripe Issuing | MOCK (future) | Card issuance — no SDK, no API keys |
Key rule: Sumsub is the ONLY connected external service with real API calls. All other services except BankID and Slack are currently mocked.
2. BankID OIDC — Norwegian eID
2.1 Purpose
BankID is Drop's sole authentication mechanism. All users must authenticate with Norwegian BankID (eID) before accessing any features. This satisfies PSD2 Strong Customer Authentication (SCA) requirements.
2.2 Integration Details
| Parameter | |
|---|---|
| Protocol | OIDC (OpenID Connect) |
| Flow | Authorization Code (PKCE) |
| Identity claim | pid — Norwegian national ID (11 digits) |
| Age verification | DOB encoded in pid — must be >= 18 |
| User storage | national_id_hash = SHA-256(pid) — never store raw pid |
| Web callback | BANKID_CALLBACK_URL (e.g., https://getdrop.no/api/auth/bankid/callback) |
| Mobile callback | BANKID_CALLBACK_URL_MOBILE = drop://auth/callback (deep link) |
| JWKS verification | Verified against BankID JWKS endpoint |
2.3 Environment Variables
| Variable | Description | |
|---|---|---|
BANKID_CLIENT_ID |
Yes (prod) | BankID OIDC client ID |
BANKID_CLIENT_SECRET |
Yes (prod) | BankID OIDC client secret — stored in Secrets Manager |
BANKID_CALLBACK_URL |
Yes (prod) | Web redirect URI after auth |
BANKID_CALLBACK_URL_MOBILE |
Yes (mobile) | Mobile deep link redirect |
BANKID_AUTHORIZE_URL |
No | Default: BankID production authorize endpoint |
BANKID_TOKEN_URL |
No | Default: BankID production token endpoint |
BANKID_JWKS_URL |
No | Default: BankID production JWKS |
BANKID_ISSUER |
No | Default: BankID production issuer |
BANKID_MOCK |
No | true skips real OIDC (dev/staging only) |
2.4 Mock Mode (BANKID_MOCK=true)
In development and staging, BANKID_MOCK=true enables a simulated BankID flow:
- Skips the full OIDC redirect
- Auto-generates a mock pid and authenticates a test user
- No network calls to BankID
2.5 Error Handling
| Scenario | Handling |
|---|---|
| State mismatch (CSRF) | 400 — reject callback |
| Code exchange failure | 500 — log error, redirect to error page |
| Invalid ID token | 401 — reject |
| User age < 18 | 403 — reject with age error |
| BankID provider down | 503 — user sees error message |
3. Sumsub — KYC / Identity Verification
3.1 Purpose
Sumsub provides KYC (Know Your Customer) identity verification. This is the only production-ready external service with real API calls.
Verification checks:
- Document authenticity (passport, ID card, driver's license, residence permit)
- Liveness check (selfie is a real person)
- Face match (selfie matches document photo)
- Sanctions check (not on international sanctions lists)
- PEP check (not a politically exposed person)
3.2 Integration Architecture
User app (web/mobile)
│
├── 1. GET /api/kyc/initiate → server calls Sumsub createApplicant + getAccessToken
│ returns { token } for WebSDK
│
├── 2. Frontend loads Sumsub WebSDK with token
│ User submits documents + selfie via Sumsub UI
│
└── 3. Sumsub webhook → POST /api/kyc/webhook
Server updates user.kyc_status = 'approved' | 'rejected'
3.3 Key Functions
| Function | Description |
|---|---|
createApplicant({ externalUserId, email?, phone? }) |
Create KYC applicant in Sumsub |
getAccessToken(applicantId) |
Get WebSDK access token (30min TTL) |
getApplicantStatus(applicantId) |
Check current verification status |
getVerificationResult(applicantId) |
Get per-check breakdown |
onWebhook(callback) |
Register webhook listener for status updates |
3.4 Applicant Status Flow
init → pending → queued → completed → (reviewAnswer: GREEN=approved / RED=rejected / RETRY)
| Review Answer | Drop kyc_status |
Meaning |
|---|---|---|
GREEN |
approved |
User verified, can transact |
RED |
rejected |
User rejected, cannot transact |
RETRY |
pending |
Document unreadable, user must resubmit |
3.5 Environment Variables
| Variable | Required | Description |
|---|---|---|
SUMSUB_API_URL |
No | Default: https://api.sumsub.com |
SUMSUB_APP_TOKEN |
Yes (prod) | Sumsub API token — stored in Secrets Manager |
SUMSUB_SECRET_KEY |
Yes (prod) | Webhook signature verification key |
3.6 Mock Mode
When NEXT_PUBLIC_SERVICE_MODE=mock:
- 90% approval rate, 10% rejection
- 3-second simulated delay
- Risk score: 15 (approved) or 85 (rejected)
- Rejected label:
DOCUMENT_UNREADABLE, type:RETRY
3.7 Error Handling
| Scenario | Handling |
|---|---|
| Sumsub API down | Log error, user sees "verification temporarily unavailable" |
| Webhook signature invalid | Reject webhook with 401 |
| Applicant already exists | Reuse existing applicant |
| Document rejected | Set kyc_status=rejected, user can retry with different document |
4. Open Banking — AISP + PISP (Provider TBD)
4.1 Purpose
- AISP (Account Information): Read user's bank balance from their Norwegian bank account
- PISP (Payment Initiation): Initiate transfers directly from user's bank account
This is Drop's core PSD2 architecture — users' funds never move into Drop's control.
4.2 Current State
| Component | Status | ||||
|---|---|---|---|---|---|
AISP balance read |
| deprecated) |
|||
PISP payment initiation |
| Mocked |
|||
Real provider |
| selection pending |
|||
mock |
| | |||
| | | |||
| | | |||
| | | | | removed |
Criticality4.3
definitions:
- Integration
Critical:ArchitectureServiceUseroutageinitiatescausespaymentcomplete│feature├──failure1.forAISP:endRefreshusersbalance High:fromServicebankoutage→degradesupdatecorebank_accounts.balancefunctionality├── Medium:2.ServicePISP:outageInitiateaffectspaymentnon-criticalatfeaturesprovider Low:→Monitoring/internalreturnstoolspayment_id—├──no3. User bank sends consent redirect if needed ├── 4. Provider webhook: payment settled → update transaction.status └── 5. Notify userimpact
2. Per-Service Integration
2.14.4 StripeRequired Environment Variables (Payments)Future)
| Description | |
|---|---|
OPENBANKING_CLIENT_ID |
Provider OAuth client ID |
OPENBANKING_CLIENT_SECRET |
Provider OAuth client secret |
OPENBANKING_CALLBACK_URL |
OAuth redirect URI |
OPENBANKING_WEBHOOK_SECRET |
Webhook signature key |
4.5 Supported Banks (Target)
Norwegian banks supporting PSD2 (via Open Banking provider):
- DNB
- Nordea
- Handelsbanken
- SpareBank 1
- Sbanken / DNB (merged)
5. Slack — Operational Alerting
5.1 Purpose
Slack receives operational alerts from Drop's internal alerting system (src/lib/alerts.ts).
Channel: #drop-ops on alai-talk.slack.com
5.2 Alert Types
| Alert | Severity | Trigger |
|---|---|---|
| App startup | Info (ℹ️) | Application boots successfully |
| App shutdown | Info (ℹ️) | SIGTERM/SIGINT received |
| Error spike | Critical (🚨) | > 5 errors in 60 seconds |
| Unhandled exception | Critical (🚨) | process.on('uncaughtException') |
| Custom alert | Variable | sendAlert() called manually |
5.3 Integration Details
| Parameter | Value |
|---|---|
Webhook URL contains auth token |
|
| |
| |
| → |
|
Key5.4
endpointsEnvironment used:
| ||
| ||
| ||
| ||
|
Request example:
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000, // in cents
currency: 'nok',
customer: customer.stripeId,
metadata: { orderId, userId },
automatic_payment_methods: { enabled: true },
});
Error handling:
try {
await stripe.paymentIntents.create(params);
} catch (err) {
if (err instanceof Stripe.errors.StripeCardError) {
throw new PaymentDeclinedException(err.message);
}
if (err instanceof Stripe.errors.StripeRateLimitError) {
throw new ServiceTemporarilyUnavailableException('Payment service rate limited');
}
// Log unexpected errors to Sentry
this.sentry.captureException(err);
throw new PaymentServiceException('Unexpected payment error');
}
Retry policy: Stripe SDK handles retries on network errors automatically. Business-level failures (card declined) are NOT retried.
Circuit breaker: {{Yes — breaker trips after 5 consecutive failures, opens for 30s}}
Fallback: No fallback for payments — fail clearly with user-facing error message.
Webhooks consumed:
|
Yes (prod) |
|
| stored | in |
| |
Rate5.5
Alert limits:100Format
read
🚨 requests/s,Drop 100Production writeAlert requests/s— perCritical
secretTitle: key.Error Cost:spike Perdetected
transactionMessage: (see8 Finance:errors Stripein billingthe dashboard).last 60 seconds
Time: 2026-02-23 14:30 UTC
6. BetterStack — External Uptime Monitoring
2.6.1 Purpose
BetterStack provides external uptime monitoring independent of Drop's infrastructure — detects total infrastructure failures that internal health checks miss.
6.2 SendGridMonitors (Email)Configured
| Check | ||
|---|---|---|
https:// |
HTTP 200 + "status":"ok" |
|
https://drop.alai.no |
|
|
| ||
HTTP |
Key6.3
operations:
| ||
| ||
| ||
|
Request example:
awaitMinute sgMail.send({0: to:DOWN user.email,→ from:Slack {#drop-ops
email:Minute '[email protected]',5: name:Still '{{APP_NAME}}'down },→ templateId:Email 'd-XXXXXXXXXXXXXXXXXXXXXX',[email protected]
dynamicTemplateData:Minute {15: firstName:Still user.name.split('down ')[0],→ orderNumber:SMS order.number,+47 orderTotal:40 formatCurrency(order.total),47 },42 });51 (requires paid plan)
6.4 Status Page
ErrorPublic handling:status page: https://drop-status.betteruptime.com
Deprecated)Setup guide:
trydocs/infrastructure/BETTERSTACK-SETUP.md{
await7.
sgMail.send(message);Deprecated}ServicescatchSwan Open Banking (
err) { if (err.code === 429) { // Queue for retry await this.emailQueue.add('retry_email', message, { delay: 60000 }); } else { this.logger.error('SendGrid error', { code: err.code, message: err.message }); // Don't throw — email failure is non-critical for most flows } }
Retry policy:Status: 3 retries via BullMQ queue with 60s, 300s, 900s backoff.
Fallback: {{Postmark as backup SMTP | Log and alert teamDEPRECATED — no fallback}}longer the planned Open Banking provider. Mock code exists at services/mock-swan.ts but will be removed.
RateWhy limits:deprecated: 100Commercial emails/sand ontechnical Proreasons plan.— specific provider TBD via procurement process.
Stripe Issuing (Mock/Dev — Future)
Status: MOCK ONLY — no Stripe SDK installed. File: services/mock-stripe.ts.
Purpose: Future card issuance feature (virtual + physical cards). All card feature flags default to false. Requires card issuing partner before activation.
Not to be confused with: Stripe Payments — Drop does NOT use Stripe for payments.
2.38. AWSService S3 (File Storage)
| |
| |
Bucket configuration:
| |||
| |||
|
Pre-signed URL pattern:
const command = new PutObjectCommand({
Bucket: process.env.S3_UPLOADS_BUCKET,
Key: `${userId}/${ulid()}.${extension}`,
ContentType: mimeType,
ContentLength: fileSize,
});
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 900 });
Retry policy: AWS SDK retries with exponential backoff by default (max 3 retries).
Circuit breaker: Breaker trips after 10 consecutive failures.
Fallback:Source: {{Cloudflare R2 as fallback storage | Abort upload with user error}}src/lib/services/index.ts
2.4 {{SERVICE_NAME}}
| |
| |
| |
| |
|
Key endpoints used:
| | |
Request example:
// TODO:Called Addonce representative request example
Error handling:
// TODO: Define error handling strategy
Retry policy: {{Exponential backoff: 1s, 2s, 4s, max 3 retries}}
Circuit breaker: {{Yes/No — threshold: X failures in Y seconds}}
Fallback / degradation: {{Define fallback behavior}}
Rate limits: {{X requests per Y}}
Cost: {{Pricing model reference}}
Monitoring: {{Alert name and dashboard link}}
3. SDK vs Direct API Call Decisions
| ||
| |
Wrapper pattern — all integrations are wrapped in a service class:
// services/stripe.service.ts — abstraction over Stripe SDK
@Injectable()
export class StripeService {
// Exposes only operations theon app actuallystartup
needsawait // Hides Stripe-specific implementation details
// Makes testing easier (injectable, mockable)
async createPaymentIntent(amount: number, currency: string): Promise<PaymentIntent> { ... }
}
4. Mock / Stub Strategy for Development & Testing
initializeServices() | |
Mock setup example:
// __mocks__/@sendgrid/mail.ts
const sendMock = jest.fn().mockResolvedValue([{ statusCode: 202 }]);
export default { send: sendMock, setApiKey: jest.fn() };
// In tests import— sgMailreset fromall '@sendgrid/mail';mock expect(sgMail.send).toHaveBeenCalledWith(expect.objectContaining({state
templateId: 'd-XXXXX',
})resetMockServices();
TestMock modestate credentialsuses location:in-memory storage (.env.testgitignored)server-side) — seeresets onboardingon guide.process restart.
5.Related Vendor Lock-In AssessmentDocuments
| |||
| |||
|
Mitigation strategy: All integrations wrapped in service classes with defined interfaces. Swapping provider = rewrite service class, not application logic.
6. Migration Plan (Switching Providers)
Stripe → Alternative Payment Provider
Trigger conditions: Pricing increase > 30%, reliability < 99.9%, compliance issues.
Migration steps:
SelectBackendalternative (Adyen, Braintree, etc.) and obtain test credentialsArchitectureImplementAuthenticationnewPaymentServiceadapter behind feature flagSourceTestServicesin staging with full payment flowSourceMigrateBetterStacknew customers to new providerSetupMigrate existing subscription customers (requires customer consent in some jurisdictions)Deprecate Stripe integration (keep webhooks active until all subscriptions migrated)
Data to migrate: Customer IDs (map old → new), subscription IDs.
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | Platform Architect (AI) | 2026-02-23 | |
| Alem | |||
| Bašić |