{"openapi":"3.1.0","info":{"title":"WealthOS — Open Wealth CSD Aggregator Gateway API","description":"## Overview\nThe WealthOS Platform provides a universal translation and orchestration middleware layer\nfor multi-tenant portfolio aggregation, asset transfer coordination, CSD integration,\nrisk management, compliance, and billing across Emerging Markets\n(Morocco, Egypt, Saudi Arabia, South Africa).\n\n## Authentication\nAll endpoints require an API key passed via the `X-API-Key` header.\nEnterprise tenants may also configure mTLS for hardware-level security.\n\n## Rate Limiting\nRate limits are tenant-specific (default 300 RPM). See the `X-RateLimit-*` response headers.\n\n## Audit Trail\nEvery API call generates an immutable, SHA-256 chained audit log entry.\n\n## Environments\nUse `sandbox` keys (prefix `wos_sand_`) for non-production testing.\nUse `live` keys (prefix `wos_live_`) for production traffic.","version":"1.1.0","contact":{"name":"WealthOS API Support","email":"api@wealthos.io"},"license":{"name":"Proprietary","url":"https://wealthos.io/legal"}},"servers":[{"url":"/api/v1","description":"Production"},{"url":"http://localhost:3000/api/v1","description":"Local Development"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Health","description":"System health and readiness"},{"name":"Tenants","description":"Tenant management and configuration"},{"name":"Portfolios","description":"Portfolio ingestion, aggregation, and retrieval"},{"name":"Normalize","description":"CSD data normalization engine"},{"name":"Transfers","description":"Asset transfer workflow orchestration"},{"name":"Risk","description":"Real-time margin, LTV, and stress testing (RaaS)"},{"name":"Ledger","description":"Identity accounts, pot balances, and virtual ledger"},{"name":"Compliance","description":"KYC/AML identity verification and suitability scoring"},{"name":"Billing","description":"Automated fee computation and invoice management"},{"name":"Webhooks","description":"Webhook endpoint management and test delivery"},{"name":"Logs","description":"API request log retrieval"},{"name":"Audit","description":"Immutable audit log retrieval and chain verification"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Tenant API key. Prefix `wos_sand_` (sandbox) or `wos_live_` (production)."}},"schemas":{"Error":{"type":"object","properties":{"success":{"type":"boolean","example":false},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{"type":"object"}}},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}},"AssetClass":{"type":"string","enum":["EQUITY","MUTUAL_FUND_OPCVM","SOVEREIGN_BOND","CORPORATE_BOND","SUKUK","ETF","CASH","ALTERNATIVE","UNKNOWN"]},"CSDProvider":{"type":"string","enum":["MAROCLEAR","MCDR","EDAA","STRATE","CDCP","CUSTOM"]},"TransferState":{"type":"string","enum":["TRANSFER_INITIATED","PENDING_DELIVERING_BANK_APPROVAL","CSD_SUBMITTED","SETTLED_AT_CSD","RECEIVING_BANK_INGESTED","FAILED_WITH_ROLLBACK"]},"CostBasisLot":{"type":"object","properties":{"lot_id":{"type":"string"},"purchase_date":{"type":"string","format":"date"},"quantity":{"type":"number"},"unit_cost":{"type":"number"},"currency":{"type":"string","example":"MAD"},"fx_rate_at_purchase":{"type":"number"},"base_currency_cost":{"type":"number"}},"required":["lot_id","purchase_date","quantity","unit_cost","currency"]},"NormalizedAsset":{"type":"object","properties":{"asset_id":{"type":"string"},"isin":{"type":"string","example":"MA0000012345"},"local_ticker":{"type":"string","example":"ATW"},"asset_class":{"$ref":"#/components/schemas/AssetClass"},"name":{"type":"string"},"currency":{"type":"string"},"quantity":{"type":"number"},"current_price":{"type":"number","nullable":true},"current_value":{"type":"number","nullable":true},"cost_basis_lots":{"type":"array","items":{"$ref":"#/components/schemas/CostBasisLot"}},"total_cost_basis":{"type":"number"},"unrealized_pnl":{"type":"number","nullable":true},"csd_provider":{"$ref":"#/components/schemas/CSDProvider"},"last_updated":{"type":"string","format":"date-time"}},"required":["asset_id","asset_class","name","currency","quantity","cost_basis_lots","total_cost_basis","csd_provider"]},"RawIngestionPayload":{"type":"object","properties":{"source_system":{"type":"string","example":"LEGACY_CORE_BANKING"},"source_account_id":{"type":"string"},"source_institution":{"type":"string"},"payload_format":{"type":"string","enum":["JSON","CSV","XML","ISO20022","PROPRIETARY"]},"raw_data":{"description":"Raw payload in any format — the normalizer will resolve it"}},"required":["source_system","source_account_id","source_institution","raw_data"]},"TransferParty":{"type":"object","properties":{"institution_name":{"type":"string"},"bic_code":{"type":"string","example":"BMCEMAMC"},"account_number":{"type":"string"},"custody_reference":{"type":"string","nullable":true},"csd_participant_id":{"type":"string","nullable":true}},"required":["institution_name","account_number"]},"Transfer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"state":{"$ref":"#/components/schemas/TransferState"},"direction":{"type":"string","enum":["INWARD","OUTWARD","INTERNAL"]},"delivering_party":{"$ref":"#/components/schemas/TransferParty"},"receiving_party":{"$ref":"#/components/schemas/TransferParty"},"csd_provider":{"$ref":"#/components/schemas/CSDProvider"},"total_value":{"type":"number","nullable":true},"currency":{"type":"string"},"temporal_workflow_id":{"type":"string"},"state_history":{"type":"array"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"AuditLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"actor_id":{"type":"string"},"actor_type":{"type":"string","enum":["USER","API_KEY","SYSTEM","WORKFLOW"]},"action":{"type":"string"},"resource_type":{"type":"string"},"resource_id":{"type":"string","nullable":true},"remote_ip":{"type":"string"},"previous_hash":{"type":"string","description":"SHA-256 of the previous log entry"},"entry_hash":{"type":"string","description":"SHA-256 of this entry"},"created_at":{"type":"string","format":"date-time"}}},"RiskAssetClass":{"type":"string","enum":["EQUITY","FIXED_INCOME","CRYPTO","REAL_ESTATE","PRIVATE_EQUITY","CASH"]},"HealthStatus":{"type":"string","enum":["SAFE","WARNING","MARGIN_CALL","LIQUIDATION"],"description":"SAFE < 50% LTV | WARNING 50–70% | MARGIN_CALL 70–85% | LIQUIDATION ≥ 85%"},"RiskAsset":{"type":"object","required":["id","name","asset_class","quantity","current_price","price_currency"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"isin":{"type":"string"},"asset_class":{"$ref":"#/components/schemas/RiskAssetClass"},"quantity":{"type":"number","minimum":0},"current_price":{"type":"number","minimum":0},"price_currency":{"type":"string","minLength":3,"maxLength":3,"example":"MAD"},"custom_haircut_override":{"type":"number","minimum":0,"maximum":1}}},"StressScenario":{"type":"object","required":["name","shocks"],"properties":{"name":{"type":"string","example":"2008 GFC Replay"},"shocks":{"type":"object","additionalProperties":{"type":"number","minimum":-1,"maximum":1},"description":"Map of asset_class → price shock (e.g. EQUITY: -0.40 for -40%)"},"fx_shock":{"type":"number","minimum":-1,"maximum":1}}},"AssetBreakdown":{"type":"object","properties":{"asset_id":{"type":"string"},"name":{"type":"string"},"asset_class":{"$ref":"#/components/schemas/RiskAssetClass"},"market_value_base":{"type":"number"},"haircut_rate":{"type":"number"},"collateral_value":{"type":"number"},"weight_in_portfolio":{"type":"number"}}},"StressResult":{"type":"object","properties":{"scenario_name":{"type":"string"},"stressed_ncv":{"type":"number"},"stressed_ltv":{"type":"number"},"health_status":{"$ref":"#/components/schemas/HealthStatus"},"breach_probability":{"type":"number","minimum":0,"maximum":1},"breached_thresholds":{"type":"array","items":{"type":"string"}}}},"MarginCalculationResult":{"type":"object","properties":{"portfolio_id":{"type":"string"},"base_currency":{"type":"string"},"gross_asset_value":{"type":"number","description":"Σ market_value_base across all assets"},"net_collateral_value":{"type":"number","description":"Σ collateral_value (after haircuts)"},"outstanding_debt":{"type":"number"},"current_ltv":{"type":"number","description":"outstanding_debt / NCV"},"blended_haircut":{"type":"number","description":"(GAV - NCV) / GAV"},"current_health_status":{"$ref":"#/components/schemas/HealthStatus"},"breached_thresholds":{"type":"array","items":{"type":"string"}},"asset_breakdown":{"type":"array","items":{"$ref":"#/components/schemas/AssetBreakdown"}},"stress_results":{"type":"array","items":{"$ref":"#/components/schemas/StressResult"}},"thresholds_applied":{"type":"object","properties":{"warning_ltv":{"type":"number"},"margin_call_ltv":{"type":"number"},"liquidation_ltv":{"type":"number"}}},"computed_at":{"type":"string","format":"date-time"}}},"EntityType":{"type":"string","enum":["INDIVIDUAL","JOINT","CORPORATE","TRUST"]},"LedgerAccount":{"type":"object","properties":{"id":{"type":"string"},"tenant_id":{"type":"string","format":"uuid"},"operator_account_id":{"type":"string","description":"Your internal account reference"},"entity_type":{"$ref":"#/components/schemas/EntityType"},"display_name":{"type":"string"},"base_currency":{"type":"string","example":"MAD"},"kyc_status":{"type":"string","enum":["PENDING","VERIFIED","FAILED"]},"risk_profile":{"type":"string","nullable":true},"metadata":{"type":"object","nullable":true},"created_at":{"type":"string","format":"date-time"}}},"PotBalanceAsset":{"type":"object","required":["raw_asset_class","quantity","unit_price","currency"],"properties":{"raw_asset_class":{"type":"string","description":"Free-text asset class from source system"},"isin":{"type":"string"},"name":{"type":"string"},"quantity":{"type":"number","minimum":0},"unit_price":{"type":"number","minimum":0},"currency":{"type":"string","minLength":3,"maxLength":3},"tax_wrapper":{"type":"string","enum":["ISA","SIPP","TFSA","JISA","NONE"],"nullable":true}}},"PotBalanceResult":{"type":"object","properties":{"pot_id":{"type":"string"},"account_id":{"type":"string"},"base_currency":{"type":"string"},"total_value_base":{"type":"number"},"asset_count":{"type":"integer"},"tax_wrapper_breach":{"type":"boolean"},"normalized_assets":{"type":"array","items":{"type":"object","properties":{"isin":{"type":"string","nullable":true},"name":{"type":"string"},"asset_class":{"$ref":"#/components/schemas/RiskAssetClass"},"quantity":{"type":"number"},"unit_price_base":{"type":"number"},"total_value_base":{"type":"number"},"weight":{"type":"number","description":"Fraction of total pot value"},"tax_wrapper":{"type":"string","nullable":true}}}},"ingested_at":{"type":"string","format":"date-time"}}},"RiskProfile":{"type":"string","enum":["CONSERVATIVE","MODERATELY_CONSERVATIVE","MODERATE","MODERATELY_AGGRESSIVE","AGGRESSIVE"]},"RiskScoreVector":{"type":"object","properties":{"composite_score":{"type":"integer","minimum":0,"maximum":100},"risk_profile":{"$ref":"#/components/schemas/RiskProfile"},"component_scores":{"type":"object","properties":{"objective_score":{"type":"integer","description":"0–20"},"capacity_score":{"type":"integer","description":"0–25"},"tolerance_score":{"type":"integer","description":"0–30"},"experience_score":{"type":"integer","description":"0–25"}}},"max_equity_allocation_pct":{"type":"number"},"max_alternatives_allocation_pct":{"type":"number"},"max_crypto_allocation_pct":{"type":"number"},"recommended_haircut_ltv":{"type":"number"},"eligible_products":{"type":"array","items":{"type":"string"}},"restricted_products":{"type":"array","items":{"type":"string"}}}},"KYCVerificationResult":{"type":"object","properties":{"verification_id":{"type":"string","format":"uuid"},"account_id":{"type":"string"},"provider":{"type":"string","enum":["SUMSUB","ONFIDO","INTERNAL"]},"status":{"type":"string","enum":["APPROVED","REJECTED","PENDING","MANUAL_REVIEW"]},"risk_level":{"type":"string","enum":["LOW","MEDIUM","HIGH","UNACCEPTABLE"]},"flags":{"type":"array","items":{"type":"string"},"example":["AML_HIT","PEP_MATCH"]},"expires_at":{"type":"string","format":"date-time","nullable":true},"completed_at":{"type":"string","format":"date-time"}}},"FeeType":{"type":"string","enum":["AUM_TIERED","PERFORMANCE_HWM","FLAT_SUBSCRIPTION"]},"FeeLineItem":{"type":"object","properties":{"fee_type":{"$ref":"#/components/schemas/FeeType"},"label":{"type":"string"},"basis":{"type":"string","description":"Human-readable calculation description"},"amount":{"type":"number"},"currency":{"type":"string"}}},"FeeCalculationResult":{"type":"object","properties":{"account_id":{"type":"string"},"pot_id":{"type":"string"},"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"base_currency":{"type":"string"},"average_daily_aum":{"type":"number"},"period_end_aum":{"type":"number"},"fee_line_items":{"type":"array","items":{"$ref":"#/components/schemas/FeeLineItem"}},"total_fees":{"type":"number"},"total_effective_rate_annualized":{"type":"number"},"new_high_water_mark":{"type":"number","nullable":true}}},"WebhookEvent":{"type":"string","enum":["transfer.state_changed","transfer.settled","transfer.failed","risk.health_status.changed","risk.margin_call","risk.liquidation","kyc.status_updated","billing.invoice_ready"]},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"tenant_id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"}},"status":{"type":"string","enum":["active","disabled"]},"description":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}},"paths":{"/health":{"get":{"tags":["Health"],"summary":"System health check","operationId":"healthCheck","security":[],"responses":{"200":{"description":"System is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"version":{"type":"string","example":"1.1.0"},"timestamp":{"type":"string","format":"date-time"}}}}}}}}},"/tenants":{"get":{"tags":["Tenants"],"summary":"List all tenants (admin only)","operationId":"listTenants","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"List of tenants"},"401":{"$ref":"#/components/schemas/Error"}}},"post":{"tags":["Tenants"],"summary":"Create a new tenant","operationId":"createTenant","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","tier"],"properties":{"name":{"type":"string"},"tier":{"type":"string","enum":["enterprise_private_bank","fintech_api","boutique_broker","neobank"]},"branding":{"type":"object"},"webhook_url":{"type":"string","format":"uri","nullable":true}}}}}},"responses":{"201":{"description":"Tenant created — includes raw API key (shown once)"},"400":{"$ref":"#/components/schemas/Error"}}}},"/portfolios":{"get":{"tags":["Portfolios"],"summary":"List tenant portfolios","operationId":"listPortfolios","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":20}}],"responses":{"200":{"description":"Portfolio list"}}}},"/portfolios/{id}":{"get":{"tags":["Portfolios"],"summary":"Get a single portfolio with normalized assets","operationId":"getPortfolio","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Portfolio details"},"404":{"$ref":"#/components/schemas/Error"}}}},"/normalize":{"post":{"tags":["Normalize"],"summary":"Normalize a raw portfolio payload into the canonical WealthOS format","operationId":"normalizePayload","description":"Accepts messy JSON from any Tier-2 bank or fintech system and returns a fully typed, normalized asset list.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RawIngestionPayload"},"example":{"source_system":"LEGACY_CORE_BANKING","source_account_id":"ACC-12345","source_institution":"Banque Centrale du Maroc","payload_format":"JSON","raw_data":{"positions":[{"isin":"MA0000012345","name":"Attijariwafa Bank","qty":500,"asset_class":"equity","currency":"MAD","cost_price":480.5,"purchase_date":"2022-03-15","current_price":520}]}}}}},"responses":{"200":{"description":"Normalization result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"normalized_assets":{"type":"array","items":{"$ref":"#/components/schemas/NormalizedAsset"}},"warnings":{"type":"array","items":{"type":"string"}},"errors":{"type":"array","items":{"type":"string"}},"processing_time_ms":{"type":"number"}}}}}}}}}}},"/transfers":{"get":{"tags":["Transfers"],"summary":"List asset transfers for this tenant","operationId":"listTransfers","parameters":[{"name":"state","in":"query","schema":{"$ref":"#/components/schemas/TransferState"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":20}}],"responses":{"200":{"description":"Transfer list"}}},"post":{"tags":["Transfers"],"summary":"Initiate an asset transfer workflow","operationId":"createTransfer","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["direction","delivering_party","receiving_party","asset_lines","csd_provider","currency"],"properties":{"direction":{"type":"string","enum":["INWARD","OUTWARD","INTERNAL"]},"delivering_party":{"$ref":"#/components/schemas/TransferParty"},"receiving_party":{"$ref":"#/components/schemas/TransferParty"},"asset_lines":{"type":"array"},"csd_provider":{"$ref":"#/components/schemas/CSDProvider"},"currency":{"type":"string"},"expected_settlement_date":{"type":"string","format":"date","nullable":true}}}}}},"responses":{"201":{"description":"Transfer workflow initiated"},"400":{"$ref":"#/components/schemas/Error"}}}},"/transfers/{id}":{"get":{"tags":["Transfers"],"summary":"Get transfer details and state history","operationId":"getTransfer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Transfer details"},"404":{"$ref":"#/components/schemas/Error"}}},"patch":{"tags":["Transfers"],"summary":"Advance transfer to a new state (manual override)","operationId":"advanceTransferState","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["target_state"],"properties":{"target_state":{"$ref":"#/components/schemas/TransferState"},"note":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Transfer state updated"}}}},"/risk/calculate-margin":{"post":{"tags":["Risk"],"summary":"Calculate real-time margin, LTV, and stress scenarios","operationId":"calculateMargin","description":"Stateless, sub-10ms margin computation engine.\n\n**Haircut schedule (default)**\n| Asset Class    | Haircut |\n|----------------|---------|\n| EQUITY         | 20%     |\n| FIXED_INCOME   | 10%     |\n| CRYPTO         | 40%     |\n| REAL_ESTATE    | 30%     |\n| PRIVATE_EQUITY | 50%     |\n| CASH           | 0%      |\n\n**LTV Health Bands**\n- `SAFE` — LTV < 50%\n- `WARNING` — 50% ≤ LTV < 70%\n- `MARGIN_CALL` — 70% ≤ LTV < 85%\n- `LIQUIDATION` — LTV ≥ 85%\n\nA webhook event `risk.health_status.changed` fires automatically when health status is WARNING or worse.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["portfolio_id","base_currency","assets","outstanding_debt"],"properties":{"portfolio_id":{"type":"string","description":"Your portfolio reference ID"},"base_currency":{"type":"string","minLength":3,"maxLength":3,"example":"MAD"},"assets":{"type":"array","items":{"$ref":"#/components/schemas/RiskAsset"},"minItems":1,"maxItems":500},"outstanding_debt":{"type":"number","minimum":0},"debt_currency":{"type":"string","minLength":3,"maxLength":3,"nullable":true},"fx_rates":{"type":"object","additionalProperties":{"type":"number"},"description":"Currency pair → rate, e.g. { USD_MAD: 9.92 }"},"thresholds":{"type":"object","properties":{"warning_ltv":{"type":"number","minimum":0,"maximum":1,"default":0.5},"margin_call_ltv":{"type":"number","minimum":0,"maximum":1,"default":0.7},"liquidation_ltv":{"type":"number","minimum":0,"maximum":1,"default":0.85}}},"stress_scenarios":{"type":"array","items":{"$ref":"#/components/schemas/StressScenario"},"maxItems":50}}},"example":{"portfolio_id":"port-acm-001","base_currency":"MAD","outstanding_debt":5000000,"assets":[{"id":"a1","name":"Attijariwafa Bank","asset_class":"EQUITY","quantity":10000,"current_price":480,"price_currency":"MAD"},{"id":"a2","name":"OCP Group Bond 2027","asset_class":"FIXED_INCOME","quantity":500,"current_price":10000,"price_currency":"MAD"}],"stress_scenarios":[{"name":"Equity Crash -30%","shocks":{"EQUITY":-0.3}},{"name":"2008 GFC Replay","shocks":{"EQUITY":-0.4,"FIXED_INCOME":-0.1}}]}}}},"responses":{"200":{"description":"Margin calculation result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"$ref":"#/components/schemas/MarginCalculationResult"},"meta":{"type":"object","properties":{"request_id":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation error (invalid payload or threshold ordering)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/ledgers/accounts":{"get":{"tags":["Ledger"],"summary":"List identity accounts","operationId":"listLedgerAccounts","parameters":[{"name":"entity_type","in":"query","schema":{"$ref":"#/components/schemas/EntityType"}},{"name":"kyc_status","in":"query","schema":{"type":"string","enum":["PENDING","VERIFIED","FAILED"]}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"List of ledger accounts","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/LedgerAccount"}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"per_page":{"type":"integer"}}}}}}}}}},"post":{"tags":["Ledger"],"summary":"Create an identity account","operationId":"createLedgerAccount","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["operator_account_id","entity_type","display_name","base_currency"],"properties":{"operator_account_id":{"type":"string","description":"Your internal account ID"},"entity_type":{"$ref":"#/components/schemas/EntityType"},"display_name":{"type":"string"},"base_currency":{"type":"string","minLength":3,"maxLength":3,"example":"MAD"},"metadata":{"type":"object","nullable":true}}},"example":{"operator_account_id":"ACC-88291","entity_type":"INDIVIDUAL","display_name":"Mohammed Al-Rashid","base_currency":"MAD"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/LedgerAccount"}}}}}},"409":{"description":"operator_account_id already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/ledgers/pots/balances":{"post":{"tags":["Ledger"],"summary":"Ingest and normalize pot (sub-account) balances","operationId":"ingestPotBalances","description":"Accepts raw asset positions from any source system. The engine:\n1. Infers normalized asset class from free-text (`raw_asset_class`)\n2. Converts all positions to `base_currency` using supplied `fx_rates`\n3. Detects tax-wrapper limit breaches (ISA, SIPP, TFSA)\n4. Returns weights and total pot value\n\nReturns HTTP **207** if a tax-wrapper breach is detected (partial success).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_id","pot_id","base_currency","assets"],"properties":{"account_id":{"type":"string"},"pot_id":{"type":"string"},"base_currency":{"type":"string","minLength":3,"maxLength":3,"example":"MAD"},"assets":{"type":"array","items":{"$ref":"#/components/schemas/PotBalanceAsset"},"minItems":1,"maxItems":1000},"fx_rates":{"type":"object","additionalProperties":{"type":"number"}},"snapshot_date":{"type":"string","format":"date","nullable":true}}}}}},"responses":{"200":{"description":"Balances normalized successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/PotBalanceResult"}}}}}},"207":{"description":"Normalized with tax-wrapper breach warning","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/PotBalanceResult"},"warning":{"type":"string"}}}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/compliance/verify-identity":{"post":{"tags":["Compliance"],"summary":"Initiate or update a KYC/AML identity verification","operationId":"verifyIdentity","description":"Routes the verification to the configured provider (Sumsub, Onfido, or Internal).\nReturns synchronously for `INTERNAL` provider; returns `PENDING` for external providers\nuntil the webhook callback completes the flow.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_id","provider","documents"],"properties":{"account_id":{"type":"string"},"provider":{"type":"string","enum":["SUMSUB","ONFIDO","INTERNAL"]},"documents":{"type":"array","items":{"type":"object"},"description":"Provider-specific document payloads"},"applicant_data":{"type":"object","description":"Name, DOB, nationality etc."},"check_aml":{"type":"boolean","default":true},"check_sanctions":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"Verification completed or initiated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/KYCVerificationResult"}}}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/compliance/suitability":{"post":{"tags":["Compliance"],"summary":"Score investor suitability from a questionnaire response","operationId":"scoreSuitability","description":"Computes a 100-point composite risk score across four dimensions:\n- **Objective** (0–20): Investment goal alignment\n- **Capacity** (0–25): Income, net worth, liquidity reserves\n- **Tolerance** (0–30): Self-assessed risk tolerance and loss acceptance\n- **Experience** (0–25): Years investing and instrument exposure\n\nMaps to one of five MiFID-II aligned risk profiles with allocation guardrails.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_id","questionnaire"],"properties":{"account_id":{"type":"string"},"questionnaire":{"type":"object","required":["primary_objective","time_horizon_years","annual_income_usd","net_worth_usd","investable_assets_usd","self_assessed_risk_tolerance","loss_tolerance_pct","years_investing"],"properties":{"primary_objective":{"type":"string","enum":["CAPITAL_PRESERVATION","INCOME","BALANCED","GROWTH","AGGRESSIVE_GROWTH"]},"time_horizon_years":{"type":"integer","minimum":1,"maximum":50},"annual_income_usd":{"type":"number","minimum":0},"net_worth_usd":{"type":"number","minimum":0},"investable_assets_usd":{"type":"number","minimum":0},"monthly_expenses_coverage_months":{"type":"number","minimum":0},"self_assessed_risk_tolerance":{"type":"integer","minimum":1,"maximum":5},"loss_tolerance_pct":{"type":"number","minimum":0,"maximum":1},"years_investing":{"type":"integer","minimum":0},"has_derivatives_experience":{"type":"boolean"},"has_alternatives_experience":{"type":"boolean"},"has_crypto_experience":{"type":"boolean"}}}}},"example":{"account_id":"la-001","questionnaire":{"primary_objective":"BALANCED","time_horizon_years":10,"annual_income_usd":120000,"net_worth_usd":850000,"investable_assets_usd":400000,"monthly_expenses_coverage_months":6,"self_assessed_risk_tolerance":3,"loss_tolerance_pct":0.2,"years_investing":7,"has_derivatives_experience":false,"has_alternatives_experience":true,"has_crypto_experience":false}}}}},"responses":{"200":{"description":"Suitability score vector","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/RiskScoreVector"}}}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/billing/calculate-fees":{"post":{"tags":["Billing"],"summary":"Compute fees for a valuation period","operationId":"calculateFees","description":"Supports three fee schedule types (mix and match per account):\n\n| Type | Description |\n|------|-------------|\n| `AUM_TIERED` | Annual rate applied to average daily AUM, pro-rated by days in period. Supports breakpoint tiers. |\n| `PERFORMANCE_HWM` | High-water mark performance fee. Accrues only when NAV exceeds max(HWM, HWM × (1 + hurdle)). |\n| `FLAT_SUBSCRIPTION` | Monthly flat fee pro-rated by calendar days. |\n\nThe response includes itemized line items and an annualized effective rate.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["account_id","pot_id","base_currency","valuation_period","fee_schedules"],"properties":{"account_id":{"type":"string"},"pot_id":{"type":"string"},"base_currency":{"type":"string","minLength":3,"maxLength":3},"valuation_period":{"type":"object","required":["start_date","end_date","period_end_aum","base_currency"],"properties":{"start_date":{"type":"string","format":"date"},"end_date":{"type":"string","format":"date"},"period_end_aum":{"type":"number","minimum":0},"base_currency":{"type":"string"},"daily_aum":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"aum":{"type":"number"}}}}}},"fee_schedules":{"type":"array","minItems":1,"items":{"oneOf":[{"type":"object","title":"AUM Tiered","required":["fee_type","aum_tiers"],"properties":{"fee_type":{"type":"string","enum":["AUM_TIERED"]},"label":{"type":"string"},"aum_tiers":{"type":"array","items":{"type":"object","required":["from_aum","to_aum","annual_rate"],"properties":{"from_aum":{"type":"number"},"to_aum":{"type":"number"},"annual_rate":{"type":"number","minimum":0,"maximum":1}}}}}},{"type":"object","title":"Performance HWM","required":["fee_type","performance"],"properties":{"fee_type":{"type":"string","enum":["PERFORMANCE_HWM"]},"label":{"type":"string"},"performance":{"type":"object","required":["rate","high_water_mark"],"properties":{"rate":{"type":"number","minimum":0,"maximum":1},"high_water_mark":{"type":"number","minimum":0},"benchmark_return":{"type":"number","minimum":0}}}}},{"type":"object","title":"Flat Subscription","required":["fee_type","flat"],"properties":{"fee_type":{"type":"string","enum":["FLAT_SUBSCRIPTION"]},"label":{"type":"string"},"flat":{"type":"object","required":["monthly_amount","currency"],"properties":{"monthly_amount":{"type":"number","minimum":0},"currency":{"type":"string"}}}}}]}}}},"example":{"account_id":"la-005","pot_id":"pot-01","base_currency":"MAD","valuation_period":{"start_date":"2026-04-01","end_date":"2026-04-30","period_end_aum":48200000,"base_currency":"MAD"},"fee_schedules":[{"fee_type":"AUM_TIERED","label":"Management Fee","aum_tiers":[{"from_aum":0,"to_aum":10000000,"annual_rate":0.01},{"from_aum":10000000,"to_aum":50000000,"annual_rate":0.0075},{"from_aum":50000000,"to_aum":999999999,"annual_rate":0.005}]},{"fee_type":"PERFORMANCE_HWM","label":"Performance Fee","performance":{"rate":0.2,"high_water_mark":47500000,"benchmark_return":0.05}},{"fee_type":"FLAT_SUBSCRIPTION","label":"Platform Fee","flat":{"monthly_amount":4900,"currency":"MAD"}}]}}}},"responses":{"200":{"description":"Fee calculation result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/FeeCalculationResult"}}}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks":{"get":{"tags":["Webhooks"],"summary":"List configured webhook endpoints","operationId":"listWebhooks","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["active","disabled"]}}],"responses":{"200":{"description":"Webhook endpoint list","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEndpoint"}}}}}}}}},"post":{"tags":["Webhooks"],"summary":"Register a new webhook endpoint","operationId":"createWebhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","description":"HTTPS endpoint to receive webhook payloads"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"},"minItems":1},"description":{"type":"string","nullable":true}}},"example":{"url":"https://your-app.example.com/webhooks/wealthos","events":["risk.health_status.changed","transfer.settled"],"description":"Risk alerts + settlement notifications"}}}},"responses":{"201":{"description":"Webhook endpoint created. `secret_raw` shown once — store it to verify HMAC-SHA256 signatures.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/WebhookEndpoint"},"secret_raw":{"type":"string","description":"Signing secret — shown only on creation. Verify: sha256(secret + raw_body)"}}}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}":{"delete":{"tags":["Webhooks"],"summary":"Delete a webhook endpoint","operationId":"deleteWebhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Webhook deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a test delivery to a webhook endpoint","operationId":"testWebhook","description":"Sends a synthetic event payload to verify your endpoint is reachable and correctly verifying signatures.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"event_type":{"$ref":"#/components/schemas/WebhookEvent","description":"Event to simulate (defaults to transfer.state_changed)"}}}}}},"responses":{"200":{"description":"Test delivery result","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"http_status":{"type":"integer","description":"Status code returned by your endpoint"},"latency_ms":{"type":"integer"},"response_body":{"type":"string","nullable":true},"signature_sent":{"type":"string","description":"X-WealthOS-Signature header value sent"}}}}}},"404":{"description":"Webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/logs":{"get":{"tags":["Logs"],"summary":"Query API request logs for this tenant","operationId":"queryLogs","parameters":[{"name":"status_class","in":"query","schema":{"type":"string","enum":["2xx","4xx","5xx"]},"description":"Filter by HTTP status class"},{"name":"method","in":"query","schema":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"]}},{"name":"path","in":"query","schema":{"type":"string"},"description":"Partial path filter, e.g. /risk"},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Request log entries","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"status":{"type":"integer"},"latency_ms":{"type":"integer"},"actor_id":{"type":"string"},"request_body":{"type":"object","nullable":true},"response_body":{"type":"object","nullable":true},"created_at":{"type":"string","format":"date-time"}}}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"per_page":{"type":"integer"}}}}}}}}}}},"/audit":{"get":{"tags":["Audit"],"summary":"Query immutable audit logs for this tenant","operationId":"queryAuditLogs","parameters":[{"name":"action","in":"query","schema":{"type":"string"}},{"name":"actor_id","in":"query","schema":{"type":"string"}},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Audit log entries"}}}},"/audit/verify":{"post":{"tags":["Audit"],"summary":"Verify the integrity of the audit log hash chain","operationId":"verifyAuditChain","description":"Recomputes entry hashes across the tenant audit log. Returns `valid: true` or the ID of the first broken entry.","responses":{"200":{"description":"Chain verification result","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"broken_at":{"type":"string","format":"uuid","nullable":true},"entries_checked":{"type":"integer"}}}}}}}}}}}