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 | Initial draft |
1. OverviewIntegration Inventory
Drop integrates with the following external services. Service mode is controlled by NEXT_PUBLIC_SERVICE_MODE env var (mock | production).
| Service | Category | Criticality | SLA | Owner Team | Status |
|---|---|---|---|---|---|
{{Stripe}} |
Payments | Critical | 99.99% | {{Backend}} |
{{Active}} |
{{SendGrid}} |
Email delivery | High | 99.95% | {{Backend}} |
{{Active}} |
{{Twilio}} |
SMS | Medium | 99.95% | {{Backend}} |
{{Active}} |
{{AWS S3}} |
File storage | High | 99.99% | {{Infrastructure}} |
{{Active}} |
{{Sentry}} |
Error tracking | Low | — | {{DevOps}} |
{{Active}} |
{{Google Maps API}} |
Geocoding | Medium | 99.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)
| Property | Value |
|---|---|
| Purpose | Payment processing, subscription billing |
| API docs | https://stripe.com/docs/api |
| Auth method | Secret key (Bearer token) |
| Credentials | Vault: stripe/secret-key-{{env}} |
| Webhook secret | Vault: stripe/webhook-secret-{{env}} |
| SDK | stripe npm package v14.x |
| API version | 2023-10-16 (pinned) |
Key endpoints used:
| Operation | Stripe API | Notes |
|---|---|---|
| Create customer | POST /v1/customers |
On user registration |
| Create payment intent | POST /v1/payment_intents |
Checkout flow |
| Confirm payment | POST /v1/payment_intents/:id/confirm |
After client confirms |
| Create subscription | POST /v1/subscriptions |
Subscription plans |
| Cancel subscription | DELETE /v1/subscriptions/:id |
User-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:
| Event | Handler | Action |
|---|---|---|
payment_intent.succeeded |
PaymentSucceededHandler |
Mark order paid |
payment_intent.payment_failed |
PaymentFailedHandler |
Notify user, release hold |
customer.subscription.deleted |
SubscriptionCancelledHandler |
Downgrade 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)
| Property | Value |
|---|---|
| Purpose | Transactional email delivery |
| API docs | https://docs.sendgrid.com/api-reference |
| Auth method | API key (Authorization: Bearer) |
| Credentials | Vault: sendgrid/api-key-{{env}} |
| SDK | @sendgrid/mail npm package v8.x |
| From email | {{[email protected]}} (verified sender) |
Key operations:
| Operation | Template | Trigger |
|---|---|---|
| Welcome email | d-XXXX |
User registration |
| Password reset | d-XXXX |
Forgot password flow |
| Order confirmation | d-XXXX |
Order placed |
| Invoice | d-XXXX |
Invoice 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)
| Property | Value |
|---|---|
| Purpose | User file uploads, generated reports, media |
| Auth method | IAM Role (EC2/ECS) or AWS Access Key |
| Credentials | IAM role (preferred) / Vault: aws/s3-access-key-{{env}} |
| SDK | @aws-sdk/client-s3 v3.x |
| Buckets | See table below |
Bucket configuration:
| Bucket | Access | Lifecycle | Purpose |
|---|---|---|---|
{{company}}-uploads-{{env}} |
User uploads | ||
{{company}}-exports-{{env}} |
Generated |
||
{{company}}-public-{{env}} |
|||
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
| Value | |
|---|---|
{{PURPOSE}} |
|
{{URL}} |
|
|
|
{{vault/path}} |
|
| |
| |
| |
2.3
Key Environmentendpoints Variablesused:
|
{{Method}} | |
|
||
| ||
| ||
| ||
| ||
| ||
| ||
| |
2.4
Request Mockexample:
// (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:
Skips3.
theSDKfullvsOIDCDirectredirectAPI Auto-generatesCallaDecisionsmock pid and authenticates a test userNo network calls to BankID
2.5 Error Handling
| Rationale | ||
|---|---|---|
| SDK |
||
| SDK |
||
| Modular SDK reduces bundle size; handles signing | ||
{{Service}} |
No |
|
{{Service}} |
{{Reason}} |
Wrapper
— 3. SumsubpatternKYCall integrations are wrapped in a service class:
// services/stripe.service.ts — abstraction over Stripe SDK
@Injectable()
export class StripeService {
// IdentityExposes Verificationonly 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
Key4. 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 Flowexample:
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 Cost
Migration Complexity
GREENStripe
Medium
High (webhook events, customer IDs)
approved{{2-4 weeks}}
User verified, can transact
REDSendGrid
Low
Low (standard SMTP + template export)
rejected{{1-2 days}}
User rejected, cannot transact
RETRYAWS S3
Medium
Medium (URL changes, S3-compatible APIs)
pending{{1 week}}
Document unreadable, user must resubmit
3.5
Mitigation Environmentstrategy: VariablesAll
integrations
Variable
Required
Description
SUMSUB_API_URL
No
Default: https://api.sumsub.com
SUMSUB_APP_TOKEN
Yes (prod)
Sumsub API token — storedwrapped in Secretsservice 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 webhookclasses with 401 defined interfaces.
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
Mocked (Swan deprecated)
PISP payment initiation
Mocked
Real provider
TBD —Swapping provider selection= pending rewrite service
Swan mock
Deprecated — 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)
Variable
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
Method
HTTP POST to Slack Incoming Webhook URL
Auth
Webhook URL contains auth token
Cooldown
10 minutes per alert title (in-memory)
Fallback
When SLACK_WEBHOOK_URLclass, not setapplication → console.log only
5.4 Environment Variables
Variable
Required
Description
SLACK_WEBHOOK_URL
Yes (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
- Select alternative
(Adyen, Monitor Braintree, URL etc.) Check and obtain test credentials
- Implement
Health Endpoint
https://drop.alai.no/api/health
HTTP 200 +new "status":"ok"PaymentService adapter behind Landingfeature Page flag
https://drop.alai.no- Test
HTTPin 200staging +with Sendfull penger payment flow
- Migrate
USnew Eastcustomers Health to https://drop.alai.no/api/health new HTTPprovider
200- Migrate
fromexisting USsubscription East
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
- 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.
Related Documents
Approval
Role
Name
Date
Signature
Author
Platform Architect (AI)
2026-02-23
ReviewerBackend Lead
ApproverFinance / Legal (payment integrations)
Alem
Bašić
Security Reviewer