openapi: 3.0.3 info: title: OpenWave — Open Banking API description: | # OpenWave Open Banking API v1.0 This file covers the **Open Banking** module of the OpenWave standard. It enables regulated Third-Party Providers (TPPs) to access customer account data and initiate payments on behalf of customers — with explicit, revocable, scope-limited customer consent. Inspired by PSD2 (EU), UK Open Banking, and SAMA Open Banking (Saudi Arabia). Designed for practical deployment in emerging markets without the full complexity of Berlin Group / FAPI 2.0, while maintaining strong security guarantees. --- ## Roles | Role | Abbreviation | Description | |---|---|---| | Account Information Service Provider | AISP | Reads account data (balances, transactions) | | Payment Initiation Service Provider | PISP | Initiates payments from customer accounts | | Bank (Account Servicing PSP) | ASPSP | Holds customer accounts; implements `BankCoreClient` | | Gateway | OpenWave | OAuth authority; routes between TPPs and ASPSPs | A TPP may be both AISP and PISP simultaneously. --- ## Consent Lifecycle ``` 1. TPP → POST /ob/consents Create consent, get consent_url 2. TPP → Redirect customer to consent_url 3. Customer authenticates at ASPSP consent screen 4. ASPSP → GET {redirect_uri}?code={auth_code}&state={state}&consent_id={id} 5. TPP → POST /ob/token Exchange auth_code + PKCE for tokens 6. TPP → API calls with access_token 7. TPP/Customer → DELETE /ob/consents/{id} Revoke at any time ``` ### Consent States ``` AWAITING_AUTHORISATION → (customer approves) → AUTHORISED AWAITING_AUTHORISATION → (customer rejects) → REJECTED AUTHORISED → (TPP/customer revoke) → REVOKED AUTHORISED → (expiry date passed) → EXPIRED ``` --- ## Authentication ### TPP Registration TPPs register with the gateway (admin-reviewed). They receive a `client_id` (public) and `client_secret` (shown once, store securely). ### OAuth 2.0 Authorization Code + PKCE ``` Step 1: TPP generates code_verifier (random 43-128 chars) Step 2: code_challenge = BASE64URL(SHA256(code_verifier)) Step 3: POST /ob/consents { code_challenge, code_challenge_method: "S256", ... } Step 4: Customer redirected to consent_url on the gateway-hosted authorisation surface Step 5: Gateway issues a short-lived hosted authorisation session for customer SCA Step 6: Customer approves with bank OTP/PUSH through the hosted surface or official SDK/webview Step 7: Gateway redirects back with auth_code (10 min TTL, single-use) Step 8: POST /ob/token { grant_type: "authorization_code", auth_code, code_verifier, ... } Step 9: Gateway verifies: SHA256(code_verifier) == stored code_challenge → issues tokens ``` ### Hosted Authorisation Session Open Banking consent approval is a customer-facing security ceremony. A TPP can create a consent request and redirect or embed the returned `consent_url`, but it **must not** collect bank OTPs, invoke SCA directly, or approve consent from its own backend. The gateway-hosted authorisation surface issues a short-lived opaque `authorisation_session` and stores only its hash. Subsequent SCA calls must include `X-OpenWave-Auth-Session`. This binds SCA to the customer surface and prevents merchant or TPP server-to-server code from completing authorisation without entering the gateway-controlled consent flow. ``` GET /ob/auth?consent_id=... → { authorisation_session, scopes, bank_handle } POST /ob/auth/sca Header: X-OpenWave-Auth-Session POST /ob/auth/confirm Header: X-OpenWave-Auth-Session GET /ob/auth/push-status Header: X-OpenWave-Auth-Session ``` ### Token Properties - **access_token**: Opaque UUID, 15 min TTL. Stored as SHA-256 hash server-side. - **refresh_token**: Opaque UUID, 90 day TTL. Stored as SHA-256 hash. Rotated on use. - Tokens are bound to a specific consent. Revoking consent invalidates all tokens instantly. - No JWT — opaque tokens are simpler to revoke and do not leak consent data. ### API Authentication All TPP API calls: `Authorization: Bearer {access_token}` Plus: `X-Consent-Id: {consent_id}` header for explicit consent binding and auditability. --- ## Scopes | Scope | Description | |---|---| | `accounts:read` | List accounts and basic details | | `balances:read` | Read account balances | | `transactions:read` | Read transaction history | | `payments:write` | Initiate single payment orders | | `mandates:write` | Create standing payment mandates (future) | Scopes are requested by the TPP and granted at consent time. A token can only access scopes included in its consent. The hosted authorisation surface must show the customer each requested scope in plain language before SCA, similar to OAuth consent screens used by major identity providers. Raw scope strings alone are not enough for customer approval. --- ## Bank Capabilities Before creating a consent, TPPs should check which OB scopes a bank supports: `GET /banks/{handle}/capabilities` This allows graceful degradation when a bank only supports AISP but not PISP. --- ## SCA (Strong Customer Authentication) For PISP payment orders, the bank may require additional SCA after the initial consent. When required, the `POST /ob/payment-orders` response includes a `sca_url`. The TPP must redirect the customer there. After approval, the order status moves to ACCEPTED. SCA exemption is bank-defined via `sca_exemption_limit` in capabilities. --- ## Amounts All monetary amounts are **minor unit integers** (e.g. 50000 = 50.000 LYD — LYD has 3 decimal places). Always include `currency` (ISO 4217) alongside any `amount` field. --- ## Idempotency All write endpoints accept `Idempotency-Key` header (max 64 chars). Same key within 24 h returns the original response without re-processing. --- ## Error Codes | Code | HTTP | Description | |---|---|---| | `CONSENT_NOT_FOUND` | 404 | Consent ID does not exist | | `CONSENT_EXPIRED` | 403 | Consent has passed its expiry date | | `CONSENT_REVOKED` | 403 | Consent was revoked by customer or TPP | | `CONSENT_AWAITING_AUTH` | 403 | Consent not yet authorised by customer | | `CONSENT_REJECTED` | 403 | Customer rejected the consent | | `SCOPE_INSUFFICIENT` | 403 | Token scope does not cover this operation | | `ACCOUNT_NOT_COVERED` | 403 | Account not included in consent | | `SCA_REQUIRED` | 202 | Bank requires SCA before order executes | | `INVALID_TOKEN` | 401 | Missing, expired, or revoked access token | | `INVALID_AUTH_CODE` | 400 | auth_code is invalid, expired, or already used | | `PKCE_VERIFICATION_FAILED` | 400 | code_verifier does not match code_challenge | | `TPP_NOT_FOUND` | 404 | client_id not registered | | `TPP_INACTIVE` | 403 | TPP registration has been suspended | | `INVALID_REDIRECT_URI` | 400 | redirect_uri does not match registered URIs | | `PAYMENT_ORDER_NOT_FOUND` | 404 | Payment order ID does not exist | | `BANK_NOT_OB_ENABLED` | 503 | Bank does not support Open Banking | | `BANK_CORE_ERROR` | 502 | Bank core backend returned an error | version: "1.0.0" contact: name: OpenWave Standard url: https://github.com/Tellesy/openwave-spec license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0 externalDocs: description: OpenWave Standard Repository url: https://github.com/Tellesy/openwave-spec servers: - url: https://{gateway_host}/api/v1 description: Your OpenWave-compliant gateway variables: gateway_host: default: gateway.example.com description: Your OpenWave gateway host tags: - name: TPP Registration description: Register and manage TPP applications (admin-reviewed) - name: Consent description: Consent lifecycle — create, authorise, token exchange, revoke - name: Token description: OAuth token operations (exchange, refresh, revoke) - name: Accounts description: AISP — account list and details (requires `accounts:read`) - name: Balances description: AISP — account balances (requires `balances:read`) - name: Transactions description: AISP — transaction history (requires `transactions:read`) - name: Payment Orders description: PISP — initiate and track payment orders (requires `payments:write`) - name: Capabilities description: Query which OB features a bank supports # ============================================================================= # PATHS # ============================================================================= paths: # --------------------------------------------------------------------------- # BANK CAPABILITIES — Public # --------------------------------------------------------------------------- /banks/{bank_handle}/capabilities: get: tags: [Capabilities] summary: Get bank Open Banking capabilities security: [] description: | Returns which payment auth modes and Open Banking scopes a bank supports. TPPs should call this before creating a consent to know which scopes are available. This endpoint is public (no authentication required). operationId: getBankCapabilities parameters: - name: bank_handle in: path required: true schema: type: string example: andalus responses: '200': description: Bank capabilities content: application/json: schema: $ref: '#/components/schemas/BankCapabilities' example: bank_handle: andalus bank_name: Andalus Bank payment_auth_modes: [OTP, PUSH] ob_enabled: true ob_scopes_supported: - accounts:read - balances:read - transactions:read - payments:write sca_exemption_limit: 5000 max_consent_expiry_days: 365 '404': $ref: '#/components/responses/NotFound' # --------------------------------------------------------------------------- # TPP REGISTRATION — Admin API # --------------------------------------------------------------------------- /ob/tpp/register: post: tags: [TPP Registration] summary: Register a Third-Party Provider application description: | Registers a new TPP client with the OpenWave gateway. Returns `client_id` (public) and `client_secret` (shown once — store securely). In production this requires manual admin review. The `X-OpenWave-Admin-Key` header must be provided (gateway operator key, not a TPP key). operationId: registerTpp security: - AdminKey: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterTppRequest' responses: '201': description: TPP registered successfully content: application/json: schema: $ref: '#/components/schemas/TppRegistrationResponse' '400': $ref: '#/components/responses/BadRequest' '409': $ref: '#/components/responses/Conflict' /ob/tpp/{client_id}: get: tags: [TPP Registration] summary: Get TPP registration details operationId: getTpp security: - AdminKey: [] parameters: - $ref: '#/components/parameters/ClientId' responses: '200': description: TPP details content: application/json: schema: $ref: '#/components/schemas/TppRegistrationResponse' '404': $ref: '#/components/responses/NotFound' patch: tags: [TPP Registration] summary: Update TPP registration (redirect URIs, name, active status) operationId: updateTpp security: - AdminKey: [] parameters: - $ref: '#/components/parameters/ClientId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateTppRequest' responses: '200': description: TPP updated content: application/json: schema: $ref: '#/components/schemas/TppRegistrationResponse' # --------------------------------------------------------------------------- # CONSENT LIFECYCLE # --------------------------------------------------------------------------- /ob/consents: post: tags: [Consent] summary: Create a consent request description: | TPP creates a consent request specifying the scopes needed, the bank, PKCE parameters, and redirect URI. Returns a `consent_url` to redirect the customer to for authorisation. The `auth_code` returned after customer authorisation is single-use and expires in **10 minutes**. operationId: createConsent security: - TppClientCredentials: [] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateConsentRequest' example: client_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6" scopes: ["accounts:read", "balances:read", "transactions:read"] bank_handle: andalus redirect_uri: https://myapp.example.com/ob/callback state: csrf-protection-random-value code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM code_challenge_method: S256 expiry_days: 90 responses: '201': description: Consent request created — redirect customer to `consent_url` content: application/json: schema: $ref: '#/components/schemas/ConsentResponse' '400': $ref: '#/components/responses/BadRequest' '403': description: TPP not authorised for requested scopes content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ob/consents/{consent_id}: get: tags: [Consent] summary: Get consent status and details operationId: getConsent security: - TppClientCredentials: [] parameters: - $ref: '#/components/parameters/ConsentId' responses: '200': description: Consent details content: application/json: schema: $ref: '#/components/schemas/ConsentResponse' '404': $ref: '#/components/responses/NotFound' delete: tags: [Consent] summary: Revoke consent description: | Revokes an authorised consent. Can be called by the TPP or forwarded from a customer-initiated revocation in their banking app. All access tokens and refresh tokens for this consent are immediately invalidated. Subsequent API calls with tokens from this consent will receive `401 INVALID_TOKEN`. operationId: revokeConsent security: - TppOAuth: [] parameters: - $ref: '#/components/parameters/ConsentId' responses: '200': description: Consent revoked content: application/json: schema: $ref: '#/components/schemas/ConsentResponse' '404': $ref: '#/components/responses/NotFound' '409': description: Consent already revoked or expired content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ob/auth: get: tags: [Consent] summary: Start hosted customer consent authorisation security: [] description: | Customer-facing endpoint rendered by the gateway-hosted authorisation page or official SDK/webview. It returns the requested scopes, display metadata for each scope, bank handler, TPP display information, and a short-lived `authorisation_session` token. TPPs and merchants must redirect/embed this surface. They must not collect OTP/PUSH approval or call SCA approval endpoints directly. operationId: getHostedConsentAuthorisation parameters: - name: consent_id in: query required: true schema: type: string format: uuid - name: state in: query required: false schema: type: string responses: '200': description: Hosted authorisation context content: application/json: schema: $ref: '#/components/schemas/ConsentAuthorisationView' '404': $ref: '#/components/responses/NotFound' /ob/auth/sca: post: tags: [Consent] summary: Start customer SCA for Open Banking consent description: | Starts bank OTP or push approval for a pending consent. Requires the hosted authorisation session issued by `GET /ob/auth`. operationId: startConsentSca security: - HostedAuthorisationSession: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/StartConsentScaRequest' responses: '200': description: SCA challenge started content: application/json: schema: $ref: '#/components/schemas/ConsentScaChallenge' '403': $ref: '#/components/responses/Forbidden' /ob/auth/confirm: post: tags: [Consent] summary: Confirm OTP and issue authorisation code description: | Confirms the customer bank OTP and issues the single-use authorisation code. Requires `X-OpenWave-Auth-Session`. operationId: confirmConsentOtp security: - HostedAuthorisationSession: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ConfirmConsentOtpRequest' responses: '200': description: Consent approved; TPP may exchange auth code using PKCE content: application/json: schema: $ref: '#/components/schemas/AuthoriseConsentResponse' '403': $ref: '#/components/responses/Forbidden' /ob/auth/push-status: get: tags: [Consent] summary: Poll push approval status operationId: getConsentPushStatus security: - HostedAuthorisationSession: [] parameters: - name: consentId in: query required: true schema: type: string format: uuid - name: expiryDays in: query required: false schema: type: integer default: 90 responses: '200': description: Push status content: application/json: schema: $ref: '#/components/schemas/ConsentPushStatus' '403': $ref: '#/components/responses/Forbidden' /ob/auth/reject: post: tags: [Consent] summary: Reject a pending consent from hosted authorisation operationId: rejectConsentFromHostedAuthorisation security: - HostedAuthorisationSession: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RejectConsentRequest' responses: '204': description: Consent rejected '403': $ref: '#/components/responses/Forbidden' # --------------------------------------------------------------------------- # OAUTH TOKEN OPERATIONS # --------------------------------------------------------------------------- /ob/token: post: tags: [Token] summary: Exchange authorization code for access + refresh tokens security: [] description: | Standard OAuth 2.0 token endpoint supporting two grant types: **`authorization_code`** — Exchange the `auth_code` from the consent redirect for an `access_token` and `refresh_token`. Requires PKCE `code_verifier`. **`refresh_token`** — Exchange an existing `refresh_token` for a new `access_token` and rotated `refresh_token`. Previous refresh token is immediately invalidated. `client_id` and `client_secret` must be provided in the request body (or via HTTP Basic Auth). operationId: exchangeToken requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TokenRequest' examples: authorization_code: summary: Exchange auth_code for tokens value: grant_type: authorization_code client_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6" client_secret: "your-client-secret" auth_code: "one-time-auth-code-from-redirect" code_verifier: "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" redirect_uri: "https://myapp.example.com/ob/callback" consent_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6" refresh_token: summary: Refresh access token value: grant_type: refresh_token client_id: "3fa85f64-5717-4562-b3fc-2c963f66afa6" client_secret: "your-client-secret" refresh_token: "existing-refresh-token" responses: '200': description: Tokens issued content: application/json: schema: $ref: '#/components/schemas/TokenResponse' '400': description: Invalid grant (bad code, PKCE mismatch, expired code, wrong redirect) content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Invalid client credentials content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ob/token/revoke: post: tags: [Token] summary: Revoke a specific access or refresh token security: [] description: | Revokes a single token without revoking the entire consent. If a `refresh_token` is revoked, the corresponding `access_token` is also invalidated. If an `access_token` is revoked, the refresh token remains valid (customer can still get a new access token). operationId: revokeToken requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TokenRevokeRequest' responses: '200': description: Token revoked (always 200, even if token was not found — RFC 7009) content: application/json: schema: type: object properties: revoked: type: boolean example: true # --------------------------------------------------------------------------- # ACCOUNTS — AISP # --------------------------------------------------------------------------- /ob/accounts: get: tags: [Accounts] summary: List accounts covered by this consent description: | Returns all accounts the customer authorised access to under the current consent. Requires `accounts:read` scope. If specific accounts were pre-selected at consent creation (`account_ibans`), only those are returned. Otherwise, all accounts the customer approved. operationId: listAccounts security: - TppOAuth: ["accounts:read"] parameters: - $ref: '#/components/parameters/ConsentHeader' responses: '200': description: Account list content: application/json: schema: $ref: '#/components/schemas/AccountListResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /ob/accounts/{account_id}: get: tags: [Accounts] summary: Get single account details operationId: getAccount security: - TppOAuth: ["accounts:read"] parameters: - $ref: '#/components/parameters/AccountId' - $ref: '#/components/parameters/ConsentHeader' responses: '200': description: Account details content: application/json: schema: $ref: '#/components/schemas/Account' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # --------------------------------------------------------------------------- # BALANCES — AISP # --------------------------------------------------------------------------- /ob/accounts/{account_id}/balances: get: tags: [Balances] summary: Get account balances description: | Returns `CURRENT`, `AVAILABLE`, and `PENDING` balances for the account. Requires `balances:read` scope. The account must be covered by the active consent. operationId: getBalances security: - TppOAuth: ["balances:read"] parameters: - $ref: '#/components/parameters/AccountId' - $ref: '#/components/parameters/ConsentHeader' responses: '200': description: Account balances content: application/json: schema: $ref: '#/components/schemas/BalancesResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # --------------------------------------------------------------------------- # TRANSACTIONS — AISP # --------------------------------------------------------------------------- /ob/accounts/{account_id}/transactions: get: tags: [Transactions] summary: Get transaction history description: | Returns paginated transaction history for an account. Requires `transactions:read` scope. **Date filtering**: Use `from_booking_date` and `to_booking_date` (ISO 8601 date). Date range is limited to the consent's authorised period. **Pending transactions**: Include unposted transactions with `include_pending=true`. Pending transactions have `status: PENDING` and no `booking_date`. Results are ordered by `booking_date` descending (most recent first). operationId: getTransactions security: - TppOAuth: ["transactions:read"] parameters: - $ref: '#/components/parameters/AccountId' - $ref: '#/components/parameters/ConsentHeader' - name: from_booking_date in: query schema: type: string format: date example: "2026-01-01" description: Start booking date (inclusive). Defaults to 90 days ago. - name: to_booking_date in: query schema: type: string format: date example: "2026-04-23" description: End booking date (inclusive). Defaults to today. - name: include_pending in: query schema: type: boolean default: false description: Include pending (unposted) transactions - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Transaction list content: application/json: schema: $ref: '#/components/schemas/TransactionListResponse' '400': description: Invalid date range or parameters content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # --------------------------------------------------------------------------- # PAYMENT ORDERS — PISP # --------------------------------------------------------------------------- /ob/payment-orders: post: tags: [Payment Orders] summary: Initiate a payment order description: | Creates a payment order from the customer's account to a beneficiary. Requires `payments:write` scope. The `debtor_iban` must be covered by the active consent. **SCA behaviour:** - If the bank requires Strong Customer Authentication for this payment (amount above `sca_exemption_limit`, first use of PISP, etc.), the response status will be `PENDING_SCA` and a `sca_url` is returned. - The TPP must redirect the customer to `sca_url`. After approval, the order transitions to `ACCEPTED` → `COMPLETED`. - If SCA is not required, order transitions directly to `ACCEPTED`. **Scheduled payments:** - Include `scheduled_date` (future date) to schedule a payment. - Scheduled payments are not executed until that date. Webhook `payment_order.completed` or `payment_order.failed` is sent on completion. operationId: createPaymentOrder security: - TppOAuth: ["payments:write"] parameters: - $ref: '#/components/parameters/ConsentHeader' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePaymentOrderRequest' examples: immediate: summary: Immediate payment value: debtor_iban: LY83002700100020200590 creditor_iban: LY83002700100099900001 creditor_name: Landlord Properties LLC amount: 150000 currency: LYD description: Rent April 2026 merchant_reference: RENT-2026-04 scheduled: summary: Scheduled future payment value: debtor_iban: LY83002700100020200590 creditor_iban: LY83002700100099900001 creditor_name: Internet Provider Co amount: 25000 currency: LYD description: Monthly internet subscription scheduled_date: "2026-05-01" responses: '201': description: Payment order created content: application/json: schema: $ref: '#/components/schemas/PaymentOrderResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '422': description: Insufficient funds or invalid IBAN content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' /ob/payment-orders/{order_id}: get: tags: [Payment Orders] summary: Get payment order status operationId: getPaymentOrder security: - TppOAuth: ["payments:write"] parameters: - name: order_id in: path required: true schema: type: string format: uuid description: Payment order UUID - $ref: '#/components/parameters/ConsentHeader' responses: '200': description: Payment order details content: application/json: schema: $ref: '#/components/schemas/PaymentOrderResponse' '404': $ref: '#/components/responses/NotFound' # ============================================================================= # COMPONENTS # ============================================================================= components: # --------------------------------------------------------------------------- # SECURITY SCHEMES # --------------------------------------------------------------------------- securitySchemes: TppOAuth: type: http scheme: bearer bearerFormat: Opaque access token description: | Opaque access token issued after OAuth 2.0 consent flow. Short-lived (15 min). Use `POST /ob/token` with `refresh_token` grant to renew. TppClientCredentials: type: http scheme: bearer bearerFormat: client_id:client_secret (Base64) description: | HTTP Basic auth using `client_id` as username and `client_secret` as password. Used for consent management operations (create, get) that do not require a user-scoped token. HostedAuthorisationSession: type: apiKey in: header name: X-OpenWave-Auth-Session description: | Short-lived opaque token issued only by the gateway-hosted Open Banking authorisation surface. Required for customer SCA endpoints. Gateways store only a hash and SHOULD expire it within 15 minutes. AdminKey: type: apiKey in: header name: X-OpenWave-Admin-Key description: Gateway operator admin key — for TPP registration management only # --------------------------------------------------------------------------- # PARAMETERS # --------------------------------------------------------------------------- parameters: ConsentId: name: consent_id in: path required: true schema: type: string format: uuid description: Consent UUID AccountId: name: account_id in: path required: true schema: type: string format: uuid description: Account UUID (from `/ob/accounts` list response) ClientId: name: client_id in: path required: true schema: type: string format: uuid description: TPP client UUID ConsentHeader: name: X-Consent-Id in: header required: true schema: type: string format: uuid description: | Active consent UUID that authorises this request. Must match the consent bound to the access token. Provides an explicit audit trail per consent. IdempotencyKey: name: Idempotency-Key in: header required: false schema: type: string maxLength: 64 description: Unique key for idempotent requests (24 h window) PageParam: name: page in: query schema: type: integer default: 1 minimum: 1 LimitParam: name: limit in: query schema: type: integer default: 20 minimum: 1 maximum: 100 # --------------------------------------------------------------------------- # SCHEMAS # --------------------------------------------------------------------------- schemas: # --- Enums --- ConsentStatus: type: string enum: - AWAITING_AUTHORISATION - AUTHORISED - REJECTED - REVOKED - EXPIRED description: | - AWAITING_AUTHORISATION: Created, customer not yet redirected or not yet approved - AUTHORISED: Customer approved, tokens can be issued - REJECTED: Customer declined at bank consent screen - REVOKED: Explicitly revoked by TPP or customer - EXPIRED: Passed expiry_date without being revoked PaymentOrderStatus: type: string enum: - PENDING - PENDING_SCA - ACCEPTED - COMPLETED - REJECTED - FAILED - SCHEDULED description: | - PENDING: Created, awaiting bank processing - PENDING_SCA: Bank requires Strong Customer Authentication (sca_url provided) - ACCEPTED: Bank has accepted; will execute - COMPLETED: Funds transferred successfully - REJECTED: Bank rejected (compliance, limits, etc.) - FAILED: Processing error - SCHEDULED: Waiting for scheduled_date TransactionStatus: type: string enum: - BOOKED - PENDING description: | - BOOKED: Posted to account; has booking_date and value_date - PENDING: Not yet posted; no booking_date TransactionType: type: string enum: - DEBIT - CREDIT BalanceType: type: string enum: - CURRENT - AVAILABLE - PENDING description: | - CURRENT: Ledger balance (posted transactions only) - AVAILABLE: Spendable balance (current minus holds/pending debits + overdraft if any) - PENDING: Total of unposted pending debits AccountType: type: string enum: - CURRENT - SAVINGS - LOAN - INVESTMENT - BUSINESS AccountStatus: type: string enum: - ACTIVE - DORMANT - FROZEN - CLOSED GrantType: type: string enum: - authorization_code - refresh_token TokenType: type: string enum: - access_token - refresh_token # --- Hosted Consent Authorisation --- ConsentAuthorisationView: type: object required: - consentId - bankHandle - tpp - scopes - scopeDetails - status - authorisationSession - authorisationSessionExpiresInSeconds properties: consentId: type: string format: uuid bankHandle: type: string example: andalus tpp: $ref: '#/components/schemas/TppDisplay' scopes: type: array items: type: string scopeDetails: type: array description: Display-safe scope metadata that the hosted consent page must show before OTP/PUSH approval. items: $ref: '#/components/schemas/ScopeDescriptor' state: type: string nullable: true status: $ref: '#/components/schemas/ConsentStatus' authorisationSession: type: string description: Short-lived opaque token for hosted SCA calls. Store only in browser/webview memory. authorisationSessionExpiresInSeconds: type: integer example: 900 TppDisplay: type: object required: [clientId, name] properties: clientId: type: string format: uuid name: type: string description: TPP/application name shown to the customer. description: type: string nullable: true website: type: string format: uri nullable: true logoUrl: type: string format: uri nullable: true ScopeDescriptor: type: object required: [scope, title, summary, details, category, sensitivity] properties: scope: type: string enum: - accounts:read - balances:read - transactions:read - payments:write - mandates:write title: type: string example: See your balances summary: type: string example: Available and booked balances for approved accounts. details: type: string example: The app can refresh balances for accounts covered by this consent. It cannot see accounts you did not approve. category: type: string example: Account information sensitivity: type: string enum: [LOW, MEDIUM, HIGH] StartConsentScaRequest: type: object required: [consentId, customerAlias, authMode] properties: consentId: type: string format: uuid customerAlias: type: string example: mtellesy@andalus authMode: type: string enum: [OTP, PUSH] ConsentScaChallenge: type: object required: [consentId, authMode] properties: consentId: type: string format: uuid authMode: type: string enum: [OTP, PUSH] phoneMasked: type: string nullable: true example: "+218 *** **1234" deviceMasked: type: string nullable: true example: "iPhone 15 Pro" expiresInSeconds: type: integer nullable: true example: 300 ConfirmConsentOtpRequest: type: object required: [consentId, otpCode] properties: consentId: type: string format: uuid otpCode: type: string minLength: 4 maxLength: 12 expiryDays: type: integer default: 90 AuthoriseConsentResponse: type: object required: [authCode, redirectUrl, consentId] properties: authCode: type: string description: Single-use OAuth authorisation code. redirectUrl: type: string format: uri consentId: type: string format: uuid ConsentPushStatus: type: object required: [consentId, pushStatus, status] properties: consentId: type: string format: uuid pushStatus: type: string enum: [PENDING, APPROVED, REJECTED] status: $ref: '#/components/schemas/ConsentStatus' authCode: type: string nullable: true redirectUrl: type: string format: uri nullable: true RejectConsentRequest: type: object required: [consentId] properties: consentId: type: string format: uuid # --- Bank Capabilities --- BankCapabilities: type: object properties: bank_handle: type: string example: andalus bank_name: type: string example: Andalus Bank payment_auth_modes: type: array items: type: string enum: [OTP, PUSH] example: [OTP, PUSH] ob_enabled: type: boolean description: Whether this bank supports Open Banking at all example: true ob_scopes_supported: type: array items: type: string description: Which OB scopes this bank's adapter implements example: ["accounts:read", "balances:read", "transactions:read", "payments:write"] sca_exemption_limit: type: integer format: int64 description: | Minor unit threshold below which SCA may be exempted. 0 means SCA always required for PISP. Null means bank decides case-by-case. example: 5000 nullable: true max_consent_expiry_days: type: integer description: Maximum consent validity period this bank allows example: 365 # --- TPP Registration --- RegisterTppRequest: type: object required: [name, redirect_uris, contact_email, scopes_requested] properties: name: type: string description: Application display name shown on consent screen example: MyFinApp description: type: string example: Personal finance aggregator and payment initiator redirect_uris: type: array minItems: 1 maxItems: 10 items: type: string format: uri description: Allowed redirect URIs — exact match required during consent flow example: - https://myfinapp.example.com/ob/callback - https://myfinapp.example.com/ob/callback-mobile contact_email: type: string format: email example: dev@myfinapp.example.com website: type: string format: uri example: https://myfinapp.example.com scopes_requested: type: array minItems: 1 items: type: string description: Scopes this TPP is permitted to request in consents example: ["accounts:read", "balances:read", "transactions:read", "payments:write"] logo_url: type: string format: uri description: Logo shown on bank consent screen nullable: true UpdateTppRequest: type: object properties: name: type: string redirect_uris: type: array items: type: string format: uri contact_email: type: string format: email logo_url: type: string format: uri nullable: true is_active: type: boolean description: Set to false to suspend this TPP TppRegistrationResponse: type: object properties: client_id: type: string format: uuid description: Public client identifier — include in all consent requests client_secret: type: string description: Secret for token endpoint. Shown ONCE at registration — store securely. nullable: true name: type: string redirect_uris: type: array items: type: string scopes_allowed: type: array items: type: string is_active: type: boolean registered_at: type: string format: date-time # --- Consent --- CreateConsentRequest: type: object required: [client_id, scopes, bank_handle, redirect_uri, code_challenge, code_challenge_method] properties: client_id: type: string format: uuid description: TPP client ID scopes: type: array minItems: 1 items: type: string enum: - accounts:read - balances:read - transactions:read - payments:write - mandates:write example: ["accounts:read", "balances:read", "transactions:read"] bank_handle: type: string description: Target bank (ASPSP) handle example: andalus redirect_uri: type: string format: uri description: Must exactly match one of the TPP's registered redirect URIs example: https://myapp.example.com/ob/callback state: type: string description: CSRF protection value. Returned unchanged in redirect callback. example: random-csrf-state-value code_challenge: type: string description: PKCE challenge — BASE64URL(SHA256(code_verifier)) example: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM code_challenge_method: type: string enum: [S256] default: S256 description: Only S256 is supported (plain is not accepted) expiry_days: type: integer default: 90 minimum: 1 maximum: 365 description: Consent validity period in days from authorisation. Bank may cap this. account_ibans: type: array items: type: string description: | Optional — pre-select specific accounts for the customer to approve. If omitted, customer selects accounts at the bank consent screen. example: - LY83002700100020200590 nullable: true ConsentResponse: type: object properties: consent_id: type: string format: uuid status: $ref: '#/components/schemas/ConsentStatus' client_id: type: string format: uuid bank_handle: type: string scopes: type: array items: type: string consent_url: type: string format: uri description: Redirect customer to this URL for bank authorisation. Present when AWAITING_AUTHORISATION. nullable: true expiry_date: type: string format: date nullable: true description: Date after which the consent expires (set after authorisation) created_at: type: string format: date-time authorised_at: type: string format: date-time nullable: true revoked_at: type: string format: date-time nullable: true # --- Token --- TokenRequest: type: object required: [grant_type, client_id, client_secret] properties: grant_type: $ref: '#/components/schemas/GrantType' client_id: type: string format: uuid client_secret: type: string auth_code: type: string description: Required for `authorization_code` grant code_verifier: type: string description: PKCE verifier. Required for `authorization_code` grant. minLength: 43 maxLength: 128 redirect_uri: type: string format: uri description: Must match the URI used in `POST /ob/consents`. Required for `authorization_code` grant. consent_id: type: string format: uuid description: Required for `authorization_code` grant refresh_token: type: string description: Required for `refresh_token` grant TokenResponse: type: object properties: access_token: type: string description: Opaque access token. TTL 15 minutes. token_type: type: string example: Bearer expires_in: type: integer description: Access token TTL in seconds example: 900 refresh_token: type: string description: Opaque refresh token. TTL 90 days. Rotated on each use. refresh_token_expires_in: type: integer description: Refresh token TTL in seconds example: 7776000 scope: type: string description: Space-separated list of granted scopes example: "accounts:read balances:read transactions:read" consent_id: type: string format: uuid TokenRevokeRequest: type: object required: [client_id, client_secret, token, token_type_hint] properties: client_id: type: string format: uuid client_secret: type: string token: type: string description: The token to revoke token_type_hint: $ref: '#/components/schemas/TokenType' description: Hint to speed up lookup. Use `access_token` or `refresh_token`. # --- Accounts --- Account: type: object properties: account_id: type: string format: uuid description: Stable account identifier within this gateway iban: type: string example: LY83002700100020200590 account_name: type: string example: Mohamed Tellesy — Current Account currency: type: string example: LYD account_type: $ref: '#/components/schemas/AccountType' status: $ref: '#/components/schemas/AccountStatus' bank_handle: type: string example: andalus bank_name: type: string example: Andalus Bank is_default: type: boolean description: Customer's primary account for this bank AccountListResponse: type: object properties: accounts: type: array items: $ref: '#/components/schemas/Account' consent_id: type: string format: uuid total: type: integer # --- Balances --- Balance: type: object properties: balance_type: $ref: '#/components/schemas/BalanceType' amount: type: integer format: int64 description: Balance in minor units. Always ≥ 0 (overdraft shown as 0 available). example: 2500000 currency: type: string example: LYD as_of: type: string format: date-time description: Timestamp of when this balance was computed by the bank BalancesResponse: type: object properties: account_id: type: string format: uuid iban: type: string balances: type: array items: $ref: '#/components/schemas/Balance' description: Contains CURRENT and AVAILABLE at minimum; PENDING if bank supports it # --- Transactions --- Transaction: type: object properties: transaction_id: type: string description: Bank's internal transaction reference status: $ref: '#/components/schemas/TransactionStatus' type: $ref: '#/components/schemas/TransactionType' amount: type: integer format: int64 description: Transaction amount in minor units (always positive) example: 50000 currency: type: string example: LYD description: type: string example: Payment for Order #1042 booking_date: type: string format: date nullable: true description: Date transaction was posted to account. Null for PENDING. value_date: type: string format: date nullable: true description: Date funds became available. Null for PENDING. reference: type: string description: Bank transaction reference number nullable: true counterparty_name: type: string nullable: true counterparty_iban: type: string nullable: true balance_after: type: integer format: int64 nullable: true description: Account balance after this transaction in minor units (BOOKED only) TransactionListResponse: type: object properties: account_id: type: string format: uuid data: type: array items: $ref: '#/components/schemas/Transaction' total: type: integer description: Total matching transactions (for pagination) page: type: integer limit: type: integer from_booking_date: type: string format: date to_booking_date: type: string format: date includes_pending: type: boolean # --- Payment Orders --- CreatePaymentOrderRequest: type: object required: - debtor_iban - creditor_iban - creditor_name - amount - currency - description properties: debtor_iban: type: string description: Customer's account to debit. Must be covered by active consent. example: LY83002700100020200590 creditor_iban: type: string description: Beneficiary account IBAN example: LY83002700100099900001 creditor_name: type: string description: Beneficiary name maxLength: 255 example: Landlord Properties LLC amount: type: integer format: int64 description: Amount in minor units minimum: 1 example: 150000 currency: type: string minLength: 3 maxLength: 3 example: LYD description: type: string maxLength: 255 description: Payment description / purpose example: Rent April 2026 merchant_reference: type: string maxLength: 128 description: TPP's internal reference for this payment example: RENT-2026-04 nullable: true scheduled_date: type: string format: date nullable: true description: Future date to execute payment. If omitted, execute immediately. example: "2026-05-01" metadata: type: object additionalProperties: true nullable: true description: Optional key-value metadata; returned in webhooks PaymentOrderResponse: type: object properties: order_id: type: string format: uuid status: $ref: '#/components/schemas/PaymentOrderStatus' debtor_iban_masked: type: string example: LY83****0590 creditor_iban: type: string creditor_name: type: string amount: type: integer format: int64 currency: type: string description: type: string sca_url: type: string format: uri nullable: true description: Redirect customer here for SCA if status is PENDING_SCA transfer_reference: type: string nullable: true description: Bank transfer reference — available when status is COMPLETED merchant_reference: type: string nullable: true scheduled_date: type: string format: date nullable: true consent_id: type: string format: uuid created_at: type: string format: date-time completed_at: type: string format: date-time nullable: true # --- Error --- ErrorResponse: type: object required: [code, message] properties: code: type: string description: Machine-readable error code (see error codes table in description) example: CONSENT_EXPIRED message: type: string description: Human-readable error description example: The consent has expired. Request a new consent from the customer. details: type: object additionalProperties: true nullable: true description: Additional context for debugging (field errors, etc.) # --------------------------------------------------------------------------- # COMMON RESPONSES # --------------------------------------------------------------------------- responses: BadRequest: description: Invalid request payload or parameters content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Missing, expired, or revoked access token content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: INVALID_TOKEN message: Access token is missing or has expired Forbidden: description: Valid token but insufficient scope or consent does not cover this account content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: SCOPE_INSUFFICIENT message: Token scope does not include balances:read NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Conflict: description: Request conflicts with current resource state content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' # ============================================================================= # WEBHOOK EVENTS — Open Banking # ============================================================================= # All OB webhooks use the same OpenWave envelope as Payments webhooks: # { # "event": "consent.granted", # "gateway": "openwave-gateway", # "api_version": "1.0.0", # "timestamp": "2026-04-23T22:00:00Z", # "data": { ... } # } # # Signature: X-OpenWave-Signature: sha256={HMAC-SHA256(raw_body, tpp_webhook_secret)} # # CONSENT EVENTS: # consent.granted data: { consent_id, tpp_client_id, bank_handle, scopes } # consent.revoked data: { consent_id, revoked_by: "tpp"|"customer"|"bank", reason? } # consent.expired data: { consent_id, expired_at } # # PAYMENT ORDER EVENTS: # payment_order.completed data: { order_id, consent_id, amount, currency, transfer_reference } # payment_order.failed data: { order_id, consent_id, reason } # payment_order.pending_sca data: { order_id, consent_id, sca_url } # payment_order.rejected data: { order_id, consent_id, reason } # =============================================================================