Skip to main content

External Services Integration

External Services Integration

Project: Drop{{PROJECT_NAME}} Version: 0.1.0{{VERSION}} Date: 2026-02-23{{DATE}} Author: Platform Architect (AI){{AUTHOR}} Status: Draft | In Review | Approved Reviewers: Alem Bašić (CEO){{REVIEWERS}}

Document History

Version Date Author Changes
0.1 2026-02-23{{DATE}} Platform Architect (AI){{AUTHOR}} Initial draft from source code and services analysis

1. OverviewIntegration Inventory

Drop integrates with the following external services. Service mode is controlled by NEXT_PUBLIC_SERVICE_MODE env var (mock | production).

Service CategoryCriticalitySLAOwner TeamStatus
{{Stripe}}PaymentsCritical99.99%{{Backend}}{{Active}}
{{SendGrid}}Email deliveryHigh99.95%{{Backend}}{{Active}}
{{Twilio}}SMSMedium99.95%{{Backend}}{{Active}}
{{AWS S3}}File storageHigh99.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}}

Criticality definitions:

  • Critical: Service outage causes complete feature failure for end users
  • High: Service outage degrades core functionality
  • Medium: Service outage affects non-critical features
  • Low: Monitoring/internal tools — no user impact

2. Per-Service Integration


2.1 Stripe (Payments)

PropertyValue
PurposePayment processing, subscription billing
API docshttps://stripe.com/docs/api
Auth methodSecret key (Bearer token)
CredentialsVault: stripe/secret-key-{{env}}
Webhook secretVault: stripe/webhook-secret-{{env}}
SDKstripe npm package v14.x
API version2023-10-16 (pinned)

Key endpoints used:

OperationStripe APINotes
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:

EventHandlerAction
payment_intent.succeededPaymentSucceededHandlerMark order paid
payment_intent.payment_failedPaymentFailedHandlerNotify user, release hold
customer.subscription.deletedSubscriptionCancelledHandlerDowngrade user

Rate limits: 100 read requests/s, 100 write requests/s per secret key. Cost: Per transaction (see Finance: Stripe billing dashboard).


2.2 SendGrid (Email)

PropertyValue
PurposeTransactional email delivery
API docshttps://docs.sendgrid.com/api-reference
Auth methodAPI key (Authorization: Bearer)
CredentialsVault: sendgrid/api-key-{{env}}
SDK@sendgrid/mail npm package v8.x
From email{{[email protected]}} (verified sender)

Key operations:

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

Request example:

await sgMail.send({
  to: user.email,
  from: { email: '[email protected]', name: '{{APP_NAME}}' },
  templateId: 'd-XXXXXXXXXXXXXXXXXXXXXX',
  dynamicTemplateData: {
    firstName: user.name.split(' ')[0],
    orderNumber: order.number,
    orderTotal: formatCurrency(order.total),
  },
});

Error handling:

try {
  await sgMail.send(message);
} catch (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: 3 retries via BullMQ queue with 60s, 300s, 900s backoff. Fallback: {{Postmark as backup SMTP | Log and alert team — no fallback}} Rate limits: 100 emails/s on Pro plan.


2.3 AWS 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

Bucket configuration:

usersWebSDK Marketing
BucketAccessLifecycle Purpose
BankID OIDC{{company}}-uploads-{{env}} PRODUCTIONPrivate Norwegian90 eIDday authentication — mandatoryexpiry for alltmp User uploads
Sumsub{{company}}-exports-{{env}} PRODUCTIONPrivate KYC/identity7 verificationday expiry Generated + webhookexports/reports
Open Banking (AISP/PISP){{company}}-public-{{env}} TBDPublic — provider selection pending(CDN) Read bank balance, initiate payments
SlackNone PRODUCTION Operationalassets, alertingpublic via incoming webhook
BetterStackPRODUCTIONExternal uptime monitoring
Swan Open BankingDEPRECATEDWas planned, no longer selected
Stripe IssuingMOCK (future)Card issuance — no SDK, no API keysimages

KeyPre-signed rule: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: SumsubAWS isSDK the ONLY connected external serviceretries with realexponential APIbackoff calls.by Alldefault other(max services3 exceptretries). BankIDCircuit andbreaker: SlackBreaker aretrips currentlyafter mocked.10 consecutive failures. Fallback: {{Cloudflare R2 as fallback storage | Abort upload with user error}}


2. BankID OIDC — Norwegian eID

2.14 Purpose{{SERVICE_NAME}}

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

ParameterProperty Value
ProtocolPurpose OIDC (OpenID Connect){{PURPOSE}}
FlowAPI docs Authorization Code (PKCE){{URL}}
IdentityAuth claimmethod pid{{API Key / OAuth2 / Basic Auth}} — Norwegian national ID (11 digits)
Age verificationCredentials DOBVault: encoded in pid — must be >= 18{{vault/path}}
User storageSDK national_id_hash{{package@version or "Direct HTTP"}} = 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

Key Environmentendpoints Variables

used:

VariableOperation RequiredEndpoint DescriptionNotes
BANKID_CLIENT_ID{{Operation}} Yes{{Method}} (prod)BankID OIDC client ID
BANKID_CLIENT_SECRET{{/path}} Yes (prod)BankID OIDC client secret — stored in Secrets Manager
BANKID_CALLBACK_URL{{Notes}}Yes (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

Request Mockexample:

Mode
// (BANKID_MOCK=trueTODO: Add representative request example
)

InError developmenthandling:

// 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 staging,dashboard BANKID_MOCK=truelink}} enables a simulated BankID flow:


  • Skips

    3. theSDK fullvs OIDCDirect redirect

  • API
  • Auto-generatesCall aDecisions mock pid and authenticates a test user
  • No network calls to BankID

2.5 Error Handling

rejectreject— user sees error message
ScenarioService HandlingApproachRationale
State mismatch (CSRF)Stripe 400SDK SDK rejecthandles callbackretry logic, type safety, webhook verification
Code exchange failureSendGrid 500SDK SDK logsimplifies error,template redirectrendering, toattachment error pagehandling
InvalidAWS ID tokenS3 401SDK v3 Modular SDK reduces bundle size; handles signing
User age < 18{{Service}} 403Direct HTTP No withofficial ageSDK, errorlightweight wrapper sufficient
BankID provider down{{Service}} 503SDK {{Reason}}

Wrapper

3. SumsubpatternKYCall integrations are wrapped in a service class:

// services/stripe.service.ts — abstraction over Stripe SDK
@Injectable()
export class StripeService {
  // IdentityExposes Verification

only

3.1 Purpose

Sumsub provides KYC (Know Your Customer) identity verification. This isoperations the onlyapp production-readyactually externalneeds service// withHides realStripe-specific APIimplementation calls.

details

Verification// checks:

Makes
    testing
  • Document authenticityeasier (passport,injectable, IDmockable) card,async driver'screatePaymentIntent(amount: license,number, residencecurrency: permit)
  • string):
  • 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
  │                               returnsPromise<PaymentIntent> { 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

4. Functions

Mock / Stub Strategy for Development & Testing
FunctionEnvironment DescriptionStrategy
createApplicant({Unit externalUserId, email?, phone? })tests CreateJest KYCmanual applicantmocks in Sumsub(__mocks__/stripe.ts)
getAccessToken(applicantId)Integration tests GetNock WebSDKHTTP accessinterceptors tokenOR (30mintest-mode TTL)credentials
getApplicantStatus(applicantId)Local development CheckTest currentAPI verificationkeys status(Stripe test mode, SendGrid sandbox)
getVerificationResult(applicantId)E2E / staging GetLive per-checktest-mode breakdowncredentials — real API calls to sandbox
onWebhook(callback)Production RegisterLive webhookproduction listener for status updatescredentials

3.4

Mock Applicantsetup Status Flow

example:

init// __mocks__/@sendgrid/mail.ts
pendingconst sendMock queued= jest.fn().mockResolvedValue([{ completedstatusCode: 202 (reviewAnswer:}]);
GREEN=approvedexport default { send: sendMock, setApiKey: jest.fn() };

/ RED=rejected / RETRY)In tests
import sgMail from '@sendgrid/mail';
expect(sgMail.send).toHaveBeenCalledWith(expect.objectContaining({
  templateId: 'd-XXXXX',
}));

Test mode credentials location: .env.test (gitignored) — see onboarding guide.


5. Vendor Lock-In Assessment

Review AnswerService DropLock-in kyc_statusLevel MeaningSwitching CostMigration Complexity
GREENStripeMediumHigh (webhook events, customer IDs) approved{{2-4 weeks}}User verified, can transact
REDSendGridLowLow (standard SMTP + template export) rejected{{1-2 days}}User rejected, cannot transact
RETRYAWS S3MediumMedium (URL changes, S3-compatible APIs) pending{{1 week}}Document unreadable, user must resubmit

3.5

Mitigation Environmentstrategy: Variables

All integrations
VariableRequiredDescription
SUMSUB_API_URLNoDefault: https://api.sumsub.com
SUMSUB_APP_TOKENYes (prod)Sumsub API token — storedwrapped in Secretsservice 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

definedinterfaces.
ScenarioHandling
Sumsub API downLog error, user sees "verification temporarily unavailable"
Webhook signature invalidReject webhookclasses 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

rewriteservice
ComponentStatus
AISP balance readMocked (Swan deprecated)
PISP payment initiationMocked
Real providerTBD —Swapping provider selection= pending
Swan mockDeprecated — will be removed

4.3 Planned Integration Architecture

User initiates payment
  │
  ├── 1. AISP: Refresh balance from bank → update bank_accounts.balance
  ├── 2. PISP: Initiate payment at provider → returns payment_id
  ├── 3. User bank sends consent redirect if needed
  ├── 4. Provider webhook: payment settled → update transaction.status
  └── 5. Notify user

4.4 Required Environment Variables (Future)

VariableDescription
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

ParameterValue
MethodHTTP POST to Slack Incoming Webhook URL
AuthWebhook URL contains auth token
Cooldown10 minutes per alert title (in-memory)
FallbackWhen SLACK_WEBHOOK_URLclass, not setapplication → console.log only

5.4 Environment Variables

VariableRequiredDescription
SLACK_WEBHOOK_URLYes (prod)Slack incoming webhook URL — stored in Secrets Manager

5.5 Alert Format

🚨 Drop Production Alert — Critical

Title: Error spike detected
Message: 8 errors in the last 60 seconds

Time: 2026-02-23 14:30 UTC
logic.


6. BetterStackMigration Plan External(Switching Uptime MonitoringProviders)

6.1Stripe Purpose→ Alternative Payment Provider

BetterStackTrigger providesconditions: externalPricing uptimeincrease monitoring> independent30%, ofreliability Drop's< infrastructure99.9%, compliance detects total infrastructure failures that internal health checks miss.issues.

6.2

Migration Monitorssteps:

Configured

  1. Select
  2. alternative(Adyen,Braintree,etc.)andobtaintestcredentials
  3. Implement
  4. adapterbehindflag
  5. Test
  6. paymentflow
  7. Migrate
  8. tonew
    Monitor URL Check
    Health Endpointhttps://drop.alai.no/api/healthHTTP 200 +new "status":"ok"PaymentService
    Landingfeature Page https://drop.alai.noHTTPin 200staging +with Sendfull penger
    USnew Eastcustomers Health https://drop.alai.no/api/health HTTPprovider 200
  9. Migrate fromexisting USsubscription East
  10. 6.3 Alert Escalation

    Minute 0:   DOWN → Slack #drop-ops
    Minute 5:   Still down → Email [email protected]
    Minute 15:  Still down → SMS +47 40 47 42 51customers (requires paidcustomer plan)consent 
    in

    6.4some Statusjurisdictions) Page

  11. Deprecate

    PublicStripe status page: https://drop-status.betteruptime.com

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


    7. Deprecated Services

    Swan Open Bankingintegration (Deprecated)

    keep webhooks active until all subscriptions migrated)

Status: DEPRECATED — no longer the planned Open Banking provider. Mock code exists at services/mock-swan.ts but will be removed.

Why deprecated: Commercial and technical reasons — 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 defaultData to false. Requires card issuing partner before activation.

Not to be confused with:migrate: StripeCustomer Payments — Drop does NOT use Stripe for payments.


8. Service Initialization

Source: src/lib/services/index.ts

// Called once on app startup
await initializeServices();

// In tests — reset all mock state
resetMockServices();

Mock state uses in-memory storageIDs (server-side)map old resets onnew), processsubscription restart.IDs.



Approval

Bašić
Role Name Date Signature
Author Platform Architect (AI) 2026-02-23
ReviewerBackend Lead
ApproverFinance / Legal (payment integrations) Alem
Security Reviewer