Skip to main content

Integration Design

Integration Design Document

Project: {{PROJECT_NAME}}Drop Integration: {{INTEGRATION_NAME}}BankID OIDC + Open Banking (Neonomics AISP/PISP) + Sumsub KYC/AML Version: {{VERSION}}1.0 Date: {{DATE}}2026-02-23 Author: {{AUTHOR}}Petter Graff, Senior Enterprise Architect Status: Draft | In Review | Approved Reviewers: {{REVIEWERS}}Alem Bašić (CEO), John (AI Director)

Document History

Version Date Author Changes
0.1 {{DATE}}2026-02-23 {{AUTHOR}}Petter Graff Initial draft from real integration docs

1. Integration Overview & Context

Integration Name: {{INTEGRATION_NAME}}Drop External Integration Stack (BankID OIDC + Neonomics Open Banking + Sumsub KYC) Type: Synchronous (REST/gRPC)HTTPS) |+ Asynchronous (Events/Queue) | Bidirectional | File-basedWebhooks)

Business Purpose: {{WHY_THIS_INTEGRATION_EXISTS}}

Drop

Criticality:cannot Criticalfunction |without Highthese |three Medium | Lowintegrations:

  • Impact if down:BankID: {{BUSINESS_IMPACT_IF_UNAVAILABLE}}Every user must authenticate with Norwegian Strong Customer Authentication — no other login method exists
  • AcceptableOpen downtime:Banking (Neonomics/ASPSP): {{RTO}}All |balance reads (AISP) and payment initiations (PISP) require live bank API connectivity — Drop never holds funds
  • Sumsub KYC: Norwegian AML law (hvitvaskingsloven) requires identity verification before any financial operation

Criticality:

  • BankID: MaxCritical — all logins blocked if down; RTO 1 hour
  • Open Banking PISP: Critical — all payments blocked if down; RTO 2 hours; RPO 0 (no payment data loss:lost — payments either completed at bank or not initiated)
  • Open Banking AISP: High {{RPO}}— balance display degrades to cached value; acceptable for 24 hours
  • Sumsub: High — new user KYC blocked; existing approved users unaffected; RTO 4 hours

Parties:

Party System Team Contact
Consumer (caller)Drop) {{CONSUMER_SYSTEM}}drop-api (Hono v4) {{TEAM_A}}ALAI Backend {{CONTACT_A}}[email protected]
ProviderProvider: (server)BankID {{PROVIDER_SYSTEM}}auth.bankid.no OIDC {{TEAM_B}}BankID Norge {{CONTACT_B}}developer.bankid.no
Provider: Open BankingNeonomics REST APINeonomicsneonomics.io
Provider: KYCSumsub REST API + WebhooksSumsubsumsub.com

2. Integration Topology Diagram

flowchart LRTB
    subgraph ConsumerSide[UserSide["ConsumerUser Devices"]
        {{CONSUMER_SYSTEM}}Browser["Browser (Next.js)"]
        C_SVC[{{ConsumerService}}]App["Mobile C_CB[Circuit Breaker]
        C_RETRY[Retry Handler]
    end

    subgraph Integration[(Expo)"Integration Layer"]
        GW[API Gateway / Load Balancer]
        Q[Message Queue\n{{QUEUE_NAME}}]
        DLQ[Dead Letter Queue\n{{DLQ_NAME}}]
    end

    subgraph ProviderSide[DropPlatform["ProviderDrop Platform {{PROVIDER_SYSTEM}}(AWS eu-north-1)"]
        P_SVC[{{ProviderService}}subgraph Edge["Cloudflare Edge"]
            P_DB[(ProviderCF["WAF DB)+ CDN + DDoS"]
        P_WORKER[Event Worker]
        end
        C_SVCsubgraph App_["drop-web (Next.js BFF)"]
            WebBFF["Next.js API Routes\n/api/auth/bankid/*"]
        end
        subgraph API["drop-api (Hono v4)"]
            AuthRoute["routes/auth.ts\nBankID OIDC callback"]
            TxRoute["routes/transactions.ts\nPISP orchestration"]
            KYCRoute["routes/user.ts\nKYC initiation + webhooks"]
            WebhookRoute["POST /v1/webhooks/sumsub\nHMAC-verified"]
        end
        subgraph DB["PostgreSQL 16"]
            Users["users + sessions"]
            Txs["transactions"]
            Kyc["screening_results\nkyc_status"]
        end
    end

    subgraph BankIDProvider["BankID Norge (auth.bankid.no)"]
        BIDAuth["OIDC /auth endpoint"]
        BIDToken["OIDC /token endpoint"]
        BIDJWKS["JWKS /certs endpoint"]
    end

    subgraph Neonomics["Neonomics Open Banking"]
        NeoAISP["AISP: GET /v1/accounts/{id}/balances"]
        NeoPISP["PISP: POST /v1/payments/sepa-credit-transfers"]
        NeoCB["Circuit Breaker\n3 failures → 60s cooldown"]
    end

    subgraph SumsubProvider["Sumsub KYC"]
        SumAPI["REST API\n/resources/applicants"]
        SumWebhook["Webhooks (HMAC-signed)\nPOST → Drop /v1/webhooks/sumsub"]
        SumSDK["WebSDK (web) +\nReact Native SDK (mobile)"]
    end

    Browser & App --> C_CBCF
    C_CBCF --> C_RETRYWebBFF C_RETRY& -->|HTTPSAPI
    REST| GW
    GWWebBFF --> P_SVCBIDAuth
    P_SVCBIDToken --> P_DBAuthRoute
    P_WORKERBIDJWKS -->|Publish| QAuthRoute
    QAuthRoute -->|Consume| C_SVCUsers
    QTxRoute -->|Failed| DLQ
    DLQNeoCB -->|Alert| AlertSystem[PagerDuty]NeoPISP & NeoAISP
    KYCRoute --> SumAPI
    SumWebhook --> WebhookRoute
    WebhookRoute --> Kyc
    SumSDK --> Browser & App

3. Service Contracts

3.1 Integration: {{INTEGRATION_NAME_1}}BankID OIDC (Authentication)

Protocol: REST/OpenID Connect 1.0 Authorization Code Flow over HTTPS | gRPC | GraphQL | WebSocket | AMQP Direction: {{CONSUMER}}Drop{{PROVIDER}}BankID Norge Idempotency: YES — usestate and Idempotency-Keynonce headerparameters | NOper-request

Authentication

Bearer{{TOKEN}}
Method Details
Type BearerOAuth2 JWTAuthorization Code with Client Secret
Client ID Header Sent in token exchange POST body: Authorization:client_id=BANKID_CLIENT_ID
Client SecretSent in token exchange POST body: client_secret=BANKID_CLIENT_SECRET
Key rotation EveryBankID {{ROTATION_PERIOD}}JWKS keys rotate periodicallycoordinatedjose vialibrary {{ROTATION_PROCESS}}handles automatic JWKS refresh
Token endpoint {{AUTH_ENDPOINT}}https://auth.bankid.no/auth/realms/prod/protocol/openid-connect/token (if OAuth2)

Request Contract — Step 1: Initiate (Redirect)

Endpoint: {{HTTP_METHOD}}GET {{BASE_URL}}https:/{{PATH}}/auth.bankid.no/auth/realms/prod/protocol/openid-connect/auth

Headers:Query Parameters:

Authorization:client_id=BANKID_CLIENT_ID
Bearerredirect_uri=https://getdrop.no/api/auth/bankid/callback
{{JWT_OR_API_KEY}}response_type=code
Content-Type:scope=openid+profile
application/jsonstate=CRYPTO_RANDOM_UUID
Accept: application/json
X-Request-ID: {{UUID}}
X-Idempotency-Key: {{IDEMPOTENCY_KEY}}nonce=CRYPTO_RANDOM_UUID

Drop stores state in: httpOnly cookie bankid_state={state} (web) or in-memory (mobile)

Request Contract — Step 2: Token Exchange

Endpoint: POST https://auth.bankid.no/auth/realms/prod/protocol/openid-connect/token

Request Body:

{grant_type=authorization_code
"{{field1}}":code=AUTH_CODE
"{{type}}redirect_uri=https://getdrop.no/api/auth/bankid/callback
client_id=BANKID_CLIENT_ID
{{description}}",
  "{{field2}}": "{{type}} — {{description}}",
  "metadata": {
    "sourceSystem": "{{CONSUMER_SYSTEM_ID}}",
    "timestamp": "ISO8601"
  }
}client_secret=BANKID_CLIENT_SECRET

Successful Response 200 / 201:

{
  "{{responseField1}}"id_token": "{{type}}eyJ...",
  "{{responseField2}}"access_token": "{{type}}...",
  "requestId"token_type": "echoBearer",
  of"expires_in": X-Request-ID"300
}

ID Token Claims Used by Drop

ClaimTypeUsage
pidstringNorwegian fødselsnummer (11 digits). Hashed (SHA-256) → users.national_id_hash
namestringFull name. Split into users.first_name + users.last_name
substringFallback subject identifier if pid absent
issstringValidated: https://auth.bankid.no/auth/realms/prod
audstringValidated: matches BANKID_CLIENT_ID
expnumberToken expiry — verified via jose
noncestringAnti-replay — matched against stored nonce

Error Handling

HTTP Status Error Code ConsumerDrop Action
User cancels BankIDN/A (redirect with error=access_denied)Return bankid_cancelled 400
400 VALIDATION_ERRORinvalid_grant LogReturn error,token_exchange_failed do NOT retry — fix request502
401 UNAUTHORIZEDunauthorized_client RefreshAlert token,ops retry onceclient ID invalid
JWKS verification failsN/AReturn jwks_verification_failed 502 — alert ops
403pid age check fails (< 18) FORBIDDENN/A AlertReturn engineering,underage do NOT retry
404NOT_FOUNDLog, do NOT retry403checkNorwegian resourcelegal ID
409CONFLICTLog, skip (idempotent)
422BUSINESS_RULELog error, do NOT retry — escalate
429RATE_LIMITEDBackoff per Retry-After header
500INTERNAL_ERRORRetry with exponential backoff
502/503UNAVAILABLECircuit breaker — fail fastrequirement

Retry Policy

MaxToken retries:exchange: {{MAX_RETRIES}}No retry (state-dependent flow — retry onlymeans onrestarting 500,from 502,login)
503,JWKS 429,fetch: networkjose errors)
Strategy: Exponential backoffhandles with jitter
Delays: [{{DELAY_1}}ms, {{DELAY_2}}ms, {{DELAY_3}}ms]
Timeout per attempt: {{TIMEOUT_MS}}ms

Circuit Breaker Configuration

Failure threshold: {{FAILURE_PERCENT}}% failures built-in {{WINDOW_SECONDS}}scaching window(5-minute OpenTTL)
duration:On {{OPEN_DURATION_SECONDS}}sJWKS Half-open test: 1 requestfailure: Alert on:Sentry, Circuitreturn open502 forto > {{ALERT_THRESHOLD_SECONDS}}suser

Rate Limiting

Limit Value Window Action when exceeded
RequestsBankID per minuteinitiate {{RPM}}10 req 60s slidingper IP HTTP 429,429 Retry-Afterfrom Drop rate limiter
BurstBankID limitcallback {{BURST}}10 req 1s60s per IP HTTP 429 immediatelyfrom Drop rate limiter

3.2 Integration: Open Banking AISP (Balance Reads)

Protocol: Berlin Group NextGenPSD2 v1.3.12+ over HTTPS Direction: Drop → Neonomics → ASPSP (user's bank) Idempotency: YES — X-Request-ID UUID per request

Authentication

MethodDetails
TypeOAuth2 Client Credentials (Neonomics) + eIDAS QWAC certificate (ASPSP direct)
Daily quotaHeader Authorization: Bearer {{DAILY}}neonomics_access_token}
Key rotation 24hOAuth2 token refresh; eIDAS cert rotation annually
Token endpoint HTTP 429, contact supporthttps://api.neonomics.io/auth/token

Request Contract — AISP Balance Read

Endpoint: GET https://api.neonomics.io/v1/accounts/{accountId}/balances

Headers:

Authorization: Bearer {NEONOMICS_ACCESS_TOKEN}
X-Request-ID: {UUID}
X-Consent-ID: {consentId}
Content-Type: application/json

Successful Response 200:

{
  "balances": [
    {
      "balanceType": "expected",
      "balanceAmount": {
        "currency": "NOK",
        "amount": "45230.00"
      },
      "lastChangeDateTime": "2026-02-23T08:00:00.000Z"
    }
  ]
}

PSD2 Constraint: Maximum 4 TPP-initiated balance reads per account per day (RTS Art. 36(6)). User-initiated reads are unlimited.

Drop caches in: bank_accounts.balance (in øre) + bank_accounts.balance_synced_at

Error Handling

HTTP StatusBerlin Group CodeDrop Handling
403CONSENT_EXPIREDDelete consent, prompt user to re-link bank account
429ACCESS_EXCEEDEDBack off, show cached balance with timestamp
500+Server errorCircuit breaker, show cached balance

Circuit Breaker Configuration

Failure threshold: 3 failures in 60s window
Open duration: 60s
Half-open test: 1 request
Alert on: Circuit open for > 5 minutes → Sentry HIGH alert
Fallback: Return cached balance from bank_accounts.balance with staleness warning

3.3 Integration: Open Banking PISP (Payment Initiation)

Protocol: Berlin Group NextGenPSD2 v1.3.12+ over HTTPS Direction: Drop → Neonomics → ASPSP (user's bank) Idempotency: YES — X-Request-ID = idempotency_key from transactions table

Request Contract — PISP Remittance

Endpoint: POST https://api.neonomics.io/v1/payments/sepa-credit-transfers

Headers:

Authorization: Bearer {NEONOMICS_ACCESS_TOKEN}
Content-Type: application/json
X-Request-ID: {idempotency_key}
X-Consent-ID: {pisConsentId}

Request Body:

{
  "debtorAccount": {
    "iban": "NO9386011117947"
  },
  "instructedAmount": {
    "currency": "NOK",
    "amount": "2010.00"
  },
  "creditorName": "Marko Petrovic",
  "creditorAccount": {
    "bban": "265-1234567-89"
  },
  "remittanceInformationUnstructured": "Drop remittance tx_rem_abc123"
}

Successful Response 201:

{
  "paymentId": "pay_xyz123",
  "transactionStatus": "RCVD",
  "_links": {
    "scaRedirect": {
      "href": "https://dnb.no/sca/pay/abc123"
    }
  }
}

SCA Redirect Flow: Drop returns scaRedirect URL to client → client redirects user to bank → user authenticates with BankID at bank → bank redirects back to Drop callback → Drop polls payment status.

Retry Policy

Max retries: 3 (retry only on 500, 502, 503 — NOT on 4xx)
Strategy: Exponential backoff with jitter
Delays: [1000ms, 2000ms, 4000ms]
Timeout per attempt: 30000ms
Idempotency: X-Request-ID = idempotency_key prevents double-payment on retry

Timeout Configuration

Timeout Type Value Notes
Connection timeout {{CONN_TIMEOUT_MS}}ms5000ms TimeFail tofast establishif connectionNeonomics unreachable
Read timeout {{READ_TIMEOUT_MS}}ms30000ms TimeASPSP processing can take up to receive first byte30s
TotalSCA requestcallback timeout {{TOTAL_TIMEOUT_MS}}ms300s (5 min) End-to-endMark budgettransaction failed if no callback received

3.24 Integration: {{INTEGRATION_NAME_2}}Sumsub (if applicable)KYC/AML

Protocol: gRPCREST HTTPS (outbound) + Webhooks HTTPS (inbound) ServiceDirection: definition:Drop → Sumsub (applicant creation), Sumsub → Drop (webhook status updates) Idempotency: YES — webhook idempotency via screening_results table check

Authentication

MethodDetails
Outbound (Drop → Sumsub)Authorization: Bearer {SUMSUB_APP_TOKEN}
Inbound Webhook VerificationHMAC-SHA256 of request body using SUMSUB_SECRET_KEY
Webhook HeaderX-Payload-Digest: HMAC-SHA256(body, SUMSUB_SECRET_KEY)
Key rotationManual rotation in Sumsub dashboard; update SUMSUB_SECRET_KEY in Secrets Manager

Request Contract — KYC Initiation

Endpoint: POST https://api.sumsub.com/resources/applicants

Headers:

serviceAuthorization: Bearer {{ServiceName}}SUMSUB_APP_TOKEN}
Content-Type: application/json
X-Request-ID: {UUID}
rpc
{{MethodName}}

Request ({{RequestMessage}})Body:

returns ({{ResponseMessage}}); rpc {{StreamMethodName}} ({{RequestMessage}}) returns (stream {{ResponseMessage}}); } message {{RequestMessage}}
{
  string"externalUserId": id"usr_abc123def456gh78",
  ="email": 1;"[email protected]",
  string"levelName": tenant_id = 2;
  {{FieldType}} {{field_name}} = 3;
}

message {{ResponseMessage}} {
  string id = 1;
  {{FieldType}} {{field_name}} = 2;
  google.protobuf.Timestamp created_at = 3;"basic-kyc-level"
}

Successful Response 201:

{
  "id": "5f9e1b2c3d4e5f6g7h8i9j0k",
  "externalUserId": "usr_abc123def456gh78",
  "review": {
    "reviewStatus": "init"
  }
}

Webhook Contract (Sumsub → Drop)

Endpoint: POST /v1/webhooks/sumsub Verification: HMAC-SHA256(rawBody, SUMSUB_SECRET_KEY) must match X-Payload-Digest header

Webhook Payload:

{
  "type": "applicantReviewed",
  "applicantId": "5f9e1b2c3d4e5f6g7h8i9j0k",
  "externalUserId": "usr_abc123def456gh78",
  "reviewResult": {
    "reviewAnswer": "GREEN",
    "rejectLabels": []
  },
  "levelName": "basic-kyc-level",
  "createdAt": "2026-02-23T10:00:00.000Z"
}

Drop Action per Event:

Event TypeDrop kyc_status ChangeSide Effects
applicantReviewed + GREENpending → approvednotification: "Du er nå verifisert", audit_log
applicantReviewed + REDpending → rejectednotification: "Verifisering avslått", audit_log, AML check
applicantReviewed + RETRYstays pendingnotification: "Vennligst prøv igjen", audit_log
applicantPendingstays pendingaudit_log only
applicantOnHoldstays pendingaudit_log only

Idempotency Strategy for Webhooks

For each webhook delivery:
1. Check screening_results WHERE user_id = ? AND screening_type = 'kyc' AND result = new_result
2. If found (duplicate): return 200 OK, skip processing
3. If not found: process, INSERT INTO screening_results, UPDATE users.kyc_status
4. Return 200 OK to prevent Sumsub retry

Sumsub Retry Policy (on non-2xx response):

  • 8 retry attempts over 7h 42m (immediate → 30s → 2m → 10m → 30m → 1h → 2h → 4h)

Rate Limiting

LimitValueNotes
Applicant creation100 req/minSumsub API limit
Webhook endpointNo rate limitMust always return 200 quickly

4. Event-Driven Integrations

4.1 EventSumsub SchemasWebhook (CloudEvents 1.0)Events

Event: {{entity}}.{{ACTION}}

Published by: {{PUBLISHER_SYSTEM}}Sumsub Consumed by: {{CONSUMER_SYSTEM_1}},Drop {{CONSUMER_SYSTEM_2}}POST /v1/webhooks/sumsub

Event: applicantReviewed

{
  "specversion": "1.0",
  "type": "{{REVERSE_DNS_EVENT_TYPE}}"applicantReviewed",
  "source"applicantId": "https://{{SYSTEM_DOMAIN}}/{{resource}}"sumsub_internal_id",
  "id"externalUserId": "{{UUID}}"usr_abc123def456gh78",
  "time"inspectionId": "2024-01-01T00:00:00Z"inspection_id",
  "datacontenttype"correlationId": "application/json"correlation_id",
  "subject"levelName": "{{RESOURCE_ID}}"basic-kyc-level",
  "data"sandboxMode": false,
  "reviewStatus": "completed",
  "createdAt": "2026-02-23T10:00:00.000Z",
  "reviewResult": {
    "entityId"reviewAnswer": "UUID of affected resource"GREEN",
    "tenantId"rejectLabels": "UUID of tenant"[],
    "actorId": "UUID of user who triggered event",
    "{{DOMAIN_FIELD_1}}": "domain-specific data",
    "{{DOMAIN_FIELD_2}}": "domain-specific data",
    "previousState"reviewRejectType": null,
    "newState"moderationComment": "{{STATE}}"null
  }
}

4.2 Topics / Queues

Drop

doesnotuseamessagebrokeratcurrentscale.WebhookdeliveryisdirectHTTP.InternalsideeffectsusesynchronousDB
Topic/Queue Partitions Retention Consumers Producer
{{TOPIC_NAME_1}} {{N}} {{RETENTION}} {{CONSUMER_GROUPS}} {{PRODUCER_SERVICE}}
{{TOPIC_NAME_2}} {{N}} {{RETENTION}}{{CONSUMER_GROUPS}}{{PRODUCER_SERVICE}}
writes.

4.3 Ordering Guarantees

cookieIdempotencyPoll
Integration Ordering ScopeNotes
{{INTEGRATION_1}}BankID OIDC callback StrictPer-session order(stateful via state cookie) PerState tenantId Kafkaensures partitioncorrect by tenantIdsession
{{INTEGRATION_2}}Sumsub webhooks Best-effort Global FIFOkey queueprevents duplicate no strict orderingprocessing
{{INTEGRATION_3}}Open Banking payment callbacks NoPer-payment ordering(paymentId) N/A Independentstatus eventsif callback missing

4.4 Idempotency Strategy

ForBankID eachcallback:
  consumedState event:cookie 1.+ nonce = per-session idempotency; JWT issuance is idempotent (same pid = same user)

Open Banking PISP:
  X-Request-ID = transactions.idempotency_key
  Unique DB index ensures duplicate INSERT is rejected → return existing transaction

Sumsub webhooks:
  Check processed_events table: SELECT 1 WHERE event_id = $1 AND consumer_group = $2
2. If found: log "Duplicate event skipped" and ACK (do not reprocess)
3. If not found: process event
4. On success: INSERT INTO processed_events (event_id, consumer_group, processed_at)
5. ACK message

Deduplication window: {{DEDUP_WINDOW}} (keep processed_eventsscreening_results for thisexisting duration)result before processing
  Duplicate: acknowledge (200 OK), skip processing

5. Data Consistency Patterns

5.1 Consistency Model

Model: Strong |(within Drop DB) + Eventual |(between CausalDrop and external systems) Acceptable lag: {{MAX_LAG_SECONDS}}sPISP status lag: 5 minutes max; AISP balance lag: 6 hours (PSD2 constraint)

5.2 PISP Payment Saga Pattern (if used for distributed transactions)

sequenceDiagram
    autonumber
    participant ODrop as OrchestratorDrop API
    participant S1DB as {{SERVICE_1}}PostgreSQL
    participant S2PISP as {{SERVICE_2}}Neonomics PISP
    participant S3ASPSP as {{SERVICE_3}}User's O-Bank
    participant User as User

    Drop->>S1:DB: Execute Step 1
    S1-->>O: Step 1 succeeded {result1}
    O->>S2: Execute Step 2 (with result1)
    S2-->>O: Step 2 succeeded {result2}
    O->>S3: Execute Step 3 (with result2)
    S3-->>O: Step 3 FAILED

    Note over O: CompensatingINSERT transactions (reversestatus='processing', order)idempotency_key)
    O-Drop->>S2:DB: CompensateCOMMIT
    StepDrop->>PISP: 2POST S2-/v1/payments/sepa-credit-transfers (X-Request-ID=idempotency_key)
    PISP-->>O:Drop: Compensated{paymentId, O->>S1:scaRedirect}
    Compensate Step 1
    S1-Drop-->>O:User: Compensated201 O-+ scaRedirect URL
    User->>ASPSP: Complete BankID SCA at bank
    ASPSP-->>Client:User: TransactionRedirect rolledto backDrop callback
    User->>Drop: GET /api/payments/callback?paymentId=xxx
    Drop->>PISP: GET /v1/payments/{paymentId}/status
    PISP-->>Drop: {transactionStatus: "ACCP"}
    Drop->>DB: UPDATE transactions SET status='completed'

Compensation strategies:

Step Compensation Notes
{{STEP_1}}DB INSERT failed {{COMPENSATION_1}}Rollback automatically — no PISP call made {{NOTES}}Clean state
{{STEP_2}}PISP initiation failed {{COMPENSATION_2}}Transaction stays processing; PISP idempotency key prevents double charge on retry {{NOTES}}Alert ops if persistent
SCA timeout (no callback in 5min)UPDATE transactions SET status='failed'; restore cached balanceUser notified to retry

6. Integration Testing Strategy

6.1 Contract Testing (Pact)

  • Consumer-drivenBankID: contracts: Consumer writesIntegration tests definingagainst expectedBankID providertest behaviorenvironment (BANKID_MOCK=false, BankID preprod)
  • ProviderNeonomics: verification:Integration Providertests CIagainst runsNeonomics consumer contracts on every buildsandbox
  • PactSumsub: Broker:Mock SDK ({{PACT_BROKER_URL}}NEXT_PUBLIC_SERVICE_MODE=mock) for unit tests; staging Sumsub account for integration

6.2 Integration Test Environments

Environment Purpose Trigger
Local (BANKID_MOCK=true) Dev testing with mockedBankID providermock Manual
Staging Full integration with BankID test + Neonomics sandbox + Sumsub staging provider Every PR merge
Production Synthetic monitoring via GET /v1/health Every 5 minutes

6.3 Test Scenarios

Happy path:

  • {{SCENARIO_1}}BankID login expected outcomeJWT issued → dashboard loads
  • {{SCENARIO_2}}Sumsub KYC expectedinitiated outcome→ webhook GREEN → kyc_status=approved
  •  AISP balance read → cached in bank_accounts (Phase 2)
  •  PISP remittance → SCA redirect → payment completed (Phase 2)

Error scenarios:

  • ProviderBankID auth cancelled → 400 bankid_cancelled
  •  User under 18 → 403 underage
  •  Sumsub webhook with invalid HMAC → 401 (rejected)
  •  Duplicate Sumsub webhook → 200 (idempotent skip)
  •  Neonomics API returns 500 circuit breaker opens after threshold502 (Phase 2)
  • ProviderPISP timesSCA outtimeout retrytransaction policymarked kicksfailed in
  • (Phase
  •  Auth token expired — token refresh flow works
  •  Rate limit exceeded — 429 handled, backoff applied
  •  Duplicate event consumed — idempotency key prevents double-processing2)

7. Monitoring & Alerting

7.1 Key Metrics

Metric Type Alert Condition Severity
integration_{{name}}_requests_totalbankid_login_errors_total Counter
integration_{{name}}_error_rateGauge> {{THRESHOLD}}% for 5m10/min HIGH
integration_{{name}}_latency_p99_msbankid_underage_rejections_total HistogramCounter > {{THRESHOLD}}ms5/min for(unusual 5mspike) MEDIUM
integration_{{name}}_circuit_openpisp_payment_failures_totalCounter> 5/minCRITICAL
pisp_circuit_open Gauge == 1 CRITICAL
integration_{{name}}_dlq_depthsumsub_webhook_failures_total GaugeCounter > 0 HIGH
integration_{{name}}_consumer_lagaisp_balance_staleness_hours Gauge > {{LAG_THRESHOLD}}12hMEDIUM
kyc_approval_rateGauge< 70% over 1h HIGH

7.2 Distributed Tracing

  • Trace ID propagation: X-Request-IDx-request-id andheader traceparentgenerated headersper forwardedrequest (UUID), propagated to all external calls
  • Sampling rate: {{SAMPLE_RATE}}% in production, 100% in stagingstaging, 10% in production (Sentry performance monitoring)
  • Tracing tool: {{TRACING_TOOL}}Sentry Performancedashboard:transactions {{DASHBOARD_URL}}tracked per endpoint

7.3 Alert Routing

Condition Alert Channel Escalation
CircuitBankID OIDC unreachableSentry HIGH alertOn-call engineer via Sentry
PISP circuit breaker open PagerDutySentry {{TEAM_A}}CRITICAL + Slack #{{CHANNEL}}alert On-call engineer+ Alem notification
DLQSumsub depthwebhook >HMAC 0failure SlackSentry #{{CHANNEL}}HIGH alert InvestigateCheck withinSUMSUB_SECRET_KEY 1hrotation
ErrorAISP ratebalance > {{THRESHOLD}}%12h stale PagerDutySentry MEDIUM alert On-callInvestigate engineerNeonomics API

Approval

Role Name Date Signature
Author Petter Graff 2026-02-23
Consumer Team Lead John (AI Director)
Provider Team Lead External (BankID/Neonomics/Sumsub)
Platform/Infra
Approver (CEO) Alem Bašić