Skip to main content

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 | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}Platform Architect (AI) 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 CategoryStatus CriticalityPurpose
BankID OIDCPRODUCTIONNorwegian eID authentication — mandatory for all users
SumsubPRODUCTIONKYC/identity verification — WebSDK + webhook
Open Banking (AISP/PISP)TBD — provider selection pendingRead bank balance, initiate payments
SlackPRODUCTIONOperational alerting via incoming webhook
BetterStackPRODUCTIONExternal uptime monitoring
Swan Open BankingDEPRECATEDWas planned, no longer selected
Stripe IssuingMOCK (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 SLAValue
ProtocolOIDC (OpenID Connect)
FlowAuthorization Code (PKCE)
Identity claimpid — Norwegian national ID (11 digits)
Age verificationDOB encoded in pid — must be >= 18
User storagenational_id_hash = SHA-256(pid) — never store raw pid
Web callbackBANKID_CALLBACK_URL (e.g., https://getdrop.no/api/auth/bankid/callback)
Mobile callbackBANKID_CALLBACK_URL_MOBILE = drop://auth/callback (deep link)
JWKS verificationVerified against BankID JWKS endpoint

2.3 Environment Variables

Team
Variable OwnerRequired Description
BANKID_CLIENT_IDYes (prod)BankID OIDC client ID
BANKID_CLIENT_SECRETYes (prod)BankID OIDC client secret — stored in Secrets Manager
BANKID_CALLBACK_URLYes (prod)Web redirect URI after auth
BANKID_CALLBACK_URL_MOBILEYes (mobile)Mobile deep link redirect
BANKID_AUTHORIZE_URLNoDefault: BankID production authorize endpoint
BANKID_TOKEN_URLNoDefault: BankID production token endpoint
BANKID_JWKS_URLNoDefault: BankID production JWKS
BANKID_ISSUERNoDefault: BankID production issuer
BANKID_MOCKNotrue 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

ScenarioHandling
State mismatch (CSRF)400 — reject callback
Code exchange failure500 — log error, redirect to error page
Invalid ID token401 — reject
User age < 18403 — reject with age error
BankID provider down503 — 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

FunctionDescription
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 AnswerDrop kyc_statusMeaning
GREENapprovedUser verified, can transact
REDrejectedUser rejected, cannot transact
RETRYpendingDocument unreadable, user must resubmit

3.5 Environment Variables

VariableRequiredDescription
SUMSUB_API_URLNoDefault: https://api.sumsub.com
SUMSUB_APP_TOKENYes (prod)Sumsub API token — stored in Secrets Manager
SUMSUB_SECRET_KEYYes (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

ScenarioHandling
Sumsub API downLog error, user sees "verification temporarily unavailable"
Webhook signature invalidReject webhook with 401
Applicant already existsReuse existing applicant
Document rejectedSet 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

Mocked(Swan TBDproviderselectionwillbe
Component Status
{{Stripe}}AISP balance read Payments Critical 99.99%{{Backend}}{{Active}}deprecated)
{{SendGrid}}PISP payment initiation Email deliveryHigh99.95%{{Backend}}{{Active}}Mocked
{{Twilio}}Real provider SMS Medium 99.95% {{Backend}} {{Active}}pending
{{AWSSwan S3}}mock FileDeprecated storage High 99.99% {{Infrastructure}}{{Active}}
{{Sentry}}Error trackingLow{{DevOps}}{{Active}}
{{Google Maps API}}GeocodingMedium99.9%{{Backend}}{{Active}}
{{NAME}}{{Category}}{{Critical/High/Medium/Low}}{{X.XX%}}{{Team}}{{Status}}removed

Criticality

4.3 definitions:

Planned
    Integration
  • Critical:Architecture

Service
User outageinitiates causespayment
  complete
  feature├── failure1. forAISP: endRefresh usersbalance 
  • High:from Servicebank outage degradesupdate corebank_accounts.balance functionality
  • ├──
  • Medium:2. ServicePISP: outageInitiate affectspayment non-criticalat features
  • provider
  • Low: Monitoring/internalreturns toolspayment_id ├── no3. User bank sends consent redirect if needed ├── 4. Provider webhook: payment settled → update transaction.status └── 5. Notify user impact

  • 2. Per-Service Integration


    2.14.4 StripeRequired Environment Variables (Payments)Future)

    PropertyVariableDescription
    OPENBANKING_CLIENT_IDProvider OAuth client ID
    OPENBANKING_CLIENT_SECRETProvider OAuth client secret
    OPENBANKING_CALLBACK_URLOAuth redirect URI
    OPENBANKING_WEBHOOK_SECRETWebhook 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

    AlertSeverityTrigger
    App startupInfo (ℹ️)Application boots successfully
    App shutdownInfo (ℹ️)SIGTERM/SIGINT received
    Error spikeCritical (🚨)> 5 errors in 60 seconds
    Unhandled exceptionCritical (🚨)process.on('uncaughtException')
    Custom alertVariablesendAlert() called manually

    5.3 Integration Details

    console.log
    Parameter Value
    PurposeMethod PaymentHTTP processing,POST subscriptionto billingSlack Incoming Webhook URL
    API docsAuth https://stripe.com/docs/apiWebhook URL contains auth token
    Auth methodCooldown Secret10 keyminutes per alert title (Bearer token)in-memory)
    CredentialsFallback Vault:When stripe/secret-key-{{env}}
    Webhook secretVault: stripe/webhook-secret-{{env}}
    SDKstripeSLACK_WEBHOOK_URL npmnot packageset v14.x
    API version2023-10-16 (pinned)only

    Key

    5.4 endpointsEnvironment used:

    Variables

    OperationVariable Stripe APIRequired Notes
    Create customerPOST /v1/customersOn user registration
    Create payment intentPOST /v1/payment_intentsCheckout flow
    Confirm paymentPOST /v1/payment_intents/:id/confirmAfter client confirms
    Create subscriptionPOST /v1/subscriptionsSubscription plans
    Cancel subscriptionDELETE /v1/subscriptions/:idUser-initiated cancel

    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:

    webhookURLstoredin
    EventHandlerActionDescription
    payment_intent.succeededSLACK_WEBHOOK_URL PaymentSucceededHandlerYes (prod) MarkSlack orderincoming paid
    payment_intent.payment_failed PaymentFailedHandler NotifySecrets user, release hold
    customer.subscription.deletedSubscriptionCancelledHandlerDowngrade userManager

    Rate

    5.5 limits:Alert 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

    Health
    PropertyMonitor ValueURLCheck
    Purpose Transactional email delivery
    API docsEndpoint https://docs.sendgrid.com/api-referencedrop.alai.no/api/healthHTTP 200 + "status":"ok"
    AuthLanding methodPage API key (Authorization: Bearer)
    Credentialshttps://drop.alai.no Vault:HTTP 200 + sendgrid/api-key-{{env}}Send penger
    SDKUS East Health @sendgrid/mailhttps://drop.alai.no/api/health npm package v8.x
    From email {{[email protected]}}HTTP (verified200 sender)from US East

    Key

    6.3 operations:

    Alert
    OperationTemplateTrigger
    Welcome emaild-XXXXUser registration
    Password resetd-XXXXForgot password flow
    Order confirmationd-XXXXOrder placed
    Invoiced-XXXXInvoice generated

    Request example:

    Escalation

    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

    Setup guide: trydocs/infrastructure/BETTERSTACK-SETUP.md

    {
    await

    7. sgMail.send(message);Deprecated }Services

    catch

    Swan 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 } }

    Deprecated)

    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.3

    8. AWSService S3 (File Storage)

    PropertyValue
    PurposeUser file uploads, generated reports, media
    Auth methodIAM Role (EC2/ECS) or AWS Access Key
    CredentialsIAM role (preferred) / Vault: aws/s3-access-key-{{env}}
    SDK@aws-sdk/client-s3 v3.x
    BucketsSee table below
    Initialization

    Bucket configuration:

    BucketAccessLifecyclePurpose
    {{company}}-uploads-{{env}}Private90 day expiry for tmpUser uploads
    {{company}}-exports-{{env}}Private7 day expiryGenerated exports/reports
    {{company}}-public-{{env}}Public (CDN)NoneMarketing assets, public images

    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}}

    PropertyValue
    Purpose{{PURPOSE}}
    API docs{{URL}}
    Auth method{{API Key / OAuth2 / Basic Auth}}
    CredentialsVault: {{vault/path}}
    SDK{{package@version or "Direct HTTP"}}

    Key endpoints used:

    OperationEndpointNotes
    {{Operation}}{{Method}} {{/path}}{{Notes}}

    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

    ServiceApproachRationale
    StripeSDKSDK handles retry logic, type safety, webhook verification
    SendGridSDKSDK simplifies template rendering, attachment handling
    AWS S3SDK v3Modular SDK reduces bundle size; handles signing
    {{Service}}Direct HTTPNo official SDK, lightweight wrapper sufficient
    {{Service}}SDK{{Reason}}

    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

    EnvironmentStrategy
    Unit testsJest manual mocks (__mocks__/stripe.tsinitializeServices()
    Integration testsNock HTTP interceptors OR test-mode credentials
    Local developmentTest API keys (Stripe test mode, SendGrid sandbox)
    E2E / stagingLive test-mode credentials — real API calls to sandbox
    ProductionLive production credentials

    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 .env.teststorage (gitignored)server-side)seeresets onboardingon guide.process restart.


    ServiceLock-in LevelSwitching CostMigration Complexity
    StripeMediumHigh (webhook events, customer IDs){{2-4 weeks}}
    SendGridLowLow (standard SMTP + template export){{1-2 days}}
    AWS S3MediumMedium (URL changes, S3-compatible APIs){{1 week}}

    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:

    Data to migrate: Customer IDs (map old → new), subscription IDs.


    Approval

    Alem
    Role Name Date Signature
    Author Platform Architect (AI) 2026-02-23
    Backend LeadReviewer
    Finance / Legal (payment integrations)Approver
    Security ReviewerBašić