📚 Compliance Implementation Guides
Comprehensive developer guide for implementing KYC, investor accreditation, and token whitelist enforcement.
🚀 Quick Navigation
🎯 Getting Started
New to Compliance? Start here.
✨ Best Practices
Patterns for reliable integrations.
🔧 Advanced Topics
Dive deeper into advanced flows.
🎯 API Overview & Architecture
Business Purpose
- Centralize KYC case creation, retrieval, and review workflows
- Manage investor accreditation under supported regimes (e.g., US, EU, UAE)
- Enforce token whitelists by wallet/account and token class
- Support organizational segregation via
orgIdpath scoping - Provide audit-friendly operations with explicit create/update semantics
- Operate across multi-jurisdiction compliance regimes
Technical Architecture
Core Data Models
All properties shown below are taken directly from the OpenAPI schemas.
KycCase
id(uuid),orgId(uuid),accountId(uuid)-
type(PERSONENTITY) -
status(PENDINGAPPROVEDREJECTED) riskScore(float),evidenceUrls(uri[])createdAt(date-time),updatedAt(date-time)
Accreditation
id(uuid),orgId(uuid),accountId(uuid)-
regime(US_SEC_RULE501EU_PROF_INVESTORUAE_FSRAVARA) -
status(PENDINGAPPROVEDREVOKEDEXPIRED) expiresAt(date-time),docs(uri[])createdAt(date-time)
WhitelistEntry
id(uuid),orgId(uuid)tokenClassId(uuid),walletId(uuid),accountId(uuid)-
status(APPROVEDREVOKED) reasons(string[])createdAt(date-time)
🎯 Quick Start
Prerequisites
- Access to Production
https://api.quub.exchange/v2or Sandboxhttps://api.sandbox.quub.exchange/v2 - An
orgIdyou are authorized to operate within - Authentication per OAuth2 (scoped) and/or API Key, as required by each operation
- (Where referenced) required headers from
./common/components.yamlsuch asorgIdHeaderandidempotencyKey
5-Minute Setup
Node.js (axios)
import axios from "axios";
const baseURL = "https://api.sandbox.quub.exchange/v2"; // or production
const orgId = "11111111-1111-1111-1111-111111111111";
const client = axios.create({
baseURL,
// Attach auth per your oauth2/apiKey setup (see Authentication section)
});
// Example: list KYC cases (no invented params)
const res = await client.get(`/orgs/${orgId}/kyc/cases`, {
params: {
// Optional filters per YAML:
// accountId: "22222222-2222-2222-2222-222222222222",
// status: "PENDING",
// cursor: "...", // from ./common/pagination.yaml
// limit: 20
},
// Include referenced headers as defined in ./common/components.yaml (orgIdHeader, etc.)
});
console.log(res.data);
Python (requests)
import requests
base_url = "https://api.sandbox.quub.exchange/v2" # or production
org_id = "11111111-1111-1111-1111-111111111111"
session = requests.Session()
# Attach auth per your oauth2/apiKey setup (see Authentication section)
r = session.get(f"{base_url}/orgs/{org_id}/kyc/cases", params={
# 'accountId': '22222222-2222-2222-2222-222222222222',
# 'status': 'PENDING',
# 'cursor': '...',
# 'limit': 20,
})
print(r.json())
🏗️ Core API Operations
Important: This section documents only the operations defined in the YAML. Response bodies reference schemas exactly as specified.
KYC
List KYC cases — GET /orgs/{orgId}/kyc/cases
-
Query params (optional): accountId(uuid),status(PENDINGAPPROVEDREJECTED),cursor,limit - Security:
oauth2(read:compliance) orapiKey - Response 200: JSON with pagination (from
PageResponse) anddata: KycCase[]
Node.js
const res = await client.get(`/orgs/${orgId}/kyc/cases`, {
params: { status: "PENDING" },
});
/*
res.data conforms to:
allOf:
- PageResponse (via ./common/pagination.yaml)
- { data: KycCase[] }
*/
Python
resp = session.get(f"{base_url}/orgs/{org_id}/kyc/cases", params={"status": "PENDING"})
print(resp.json())
Create KYC case — POST /orgs/{orgId}/kyc/cases
- Headers:
idempotencyKey(from./common/components.yaml) - Body (required):
accountId(uuid) required-
type(PERSONENTITY) required evidenceUrls(uri[]) optional
- Security:
oauth2(write:compliance) orapiKey - Response 201:
{ data: KycCase }
Node.js
const body = {
accountId: "22222222-2222-2222-2222-222222222222",
type: "PERSON",
evidenceUrls: ["https://example.com/documents/kyc-doc1.pdf"],
};
const res = await client.post(`/orgs/${orgId}/kyc/cases`, body, {
// headers: { 'Idempotency-Key': '...' } // use exact header name from common/components.yaml
});
console.log(res.data);
Retrieve KYC case — GET /orgs/{orgId}/kyc/cases/{caseId}
- Path parameter:
kycId(uuid) (as defined in YAML parameter; see note in Troubleshooting) - Security:
oauth2(read:compliance) orapiKey - Response 200:
{ data: KycCase }
Python
kyc_case_id = "33333333-3333-3333-3333-333333333333"
r = session.get(f"{base_url}/orgs/{org_id}/kyc/cases/{kyc_case_id}")
print(r.json())
Update KYC case — PATCH /orgs/{orgId}/kyc/cases/{caseId}
- Path parameter:
kycId(uuid) (see note in Troubleshooting) - Body (one or more):
-
status(PENDINGAPPROVEDREJECTED) riskScore(float 0–100)reviewer(string)
-
- Security:
oauth2(write:compliance) orapiKey - Response 200:
{ data: KycCase }
Node.js
const kycCaseId = "33333333-3333-3333-3333-333333333333";
const res = await client.patch(`/orgs/${orgId}/kyc/cases/${kycCaseId}`, {
status: "APPROVED",
riskScore: 12.5,
});
console.log(res.data);
Accreditation
List accreditations — GET /orgs/{orgId}/accreditations
- Query params (optional):
accountId(uuid)-
regime(US_SEC_RULE501EU_PROF_INVESTORUAE_FSRAVARA) -
status(PENDINGAPPROVEDREVOKEDEXPIRED) cursor,limit
- Security:
oauth2(read:compliance) orapiKey - Response 200: pagination +
data: Accreditation[]
Python
r = session.get(f"{base_url}/orgs/{org_id}/accreditations", params={
"regime": "US_SEC_RULE501",
"status": "APPROVED"
})
print(r.json())
Create accreditation — POST /orgs/{orgId}/accreditations
- Headers:
idempotencyKey(from./common/components.yaml) - Body (required):
accountId(uuid)-
regime(US_SEC_RULE501EU_PROF_INVESTORUAE_FSRAVARA) expiresAt(date-time, optional)docs(uri[] optional)
- Security:
oauth2(write:compliance) orapiKey - Response 201:
{ data: Accreditation }
Node.js
const res = await client.post(
`/orgs/${orgId}/accreditations`,
{
accountId: "22222222-2222-2222-2222-222222222222",
regime: "US_SEC_RULE501",
expiresAt: "2026-12-31T23:59:59Z",
docs: ["https://example.com/documents/accreditation-letter.pdf"],
}
// ,{ headers: { 'Idempotency-Key': '...' } }
);
console.log(res.data);
Whitelist
List whitelist entries — GET /orgs/{orgId}/whitelist
- Query params (optional):
tokenClassId(uuid),accountId(uuid)-
status(APPROVEDREVOKED) cursor,limit
- Security:
oauth2(read:compliance) orapiKey - Response 200: pagination +
data: WhitelistEntry[]
Node.js
const res = await client.get(`/orgs/${orgId}/whitelist`, {
params: { status: "APPROVED" },
});
console.log(res.data);
Add whitelist entry — POST /orgs/{orgId}/whitelist
- Headers:
idempotencyKey(from./common/components.yaml) - Body (required):
tokenClassId(uuid)walletId(uuid)accountId(uuid, optional)reasons(string[] optional)
- Security:
oauth2(write:compliance) orapiKey - Response 201:
{ data: WhitelistEntry } - Error:
409 Conflict(per YAML)
Python
payload = {
"tokenClassId": "44444444-4444-4444-4444-444444444444",
"walletId": "55555555-5555-5555-5555-555555555555",
"reasons": ["Initial allowlist"]
}
r = session.post(f"{base_url}/orgs/{org_id}/whitelist", json=payload)
print(r.status_code, r.json())
Update whitelist entry — PATCH /orgs/{orgId}/whitelist/{entryId}
- Path parameter:
id(uuid) -
Body: status(APPROVEDREVOKED) - Security:
oauth2(write:compliance) orapiKey - Response 200:
{ data: WhitelistEntry } - Errors:
404 NotFound,422 ValidationError,429 TooManyRequests
Node.js
const entryId = "66666666-6666-6666-6666-666666666666";
const res = await client.patch(`/orgs/${orgId}/whitelist/${entryId}`, {
status: "REVOKED",
});
console.log(res.data);
🔐 Authentication Setup
Use only the security schemes specified in the YAML:
oauth2(scopes used by operations:read:compliance,write:compliance)apiKeybearerAuthis defined incomponents.securitySchemes, but operations in this spec explicitly requireoauth2and/orapiKey.
OAuth2 (example request usage)
// Attach OAuth2 access token obtained per your configured flow.
// Scope must include either 'read:compliance' or 'write:compliance' for the target operation.
client.interceptors.request.use((cfg) => {
cfg.headers = cfg.headers || {};
cfg.headers.Authorization = `Bearer ${process.env.ACCESS_TOKEN}`;
return cfg;
});
API Key (example request usage)
client.interceptors.request.use((cfg) => {
cfg.headers = cfg.headers || {};
// Use the exact header or parameter as defined in ./common/components.yaml for apiKey
// e.g., cfg.headers['X-API-Key'] = process.env.QUUB_API_KEY;
return cfg;
});
Also include referenced headers such as
orgIdHeaderandidempotencyKeyexactly as defined in./common/components.yaml.
✨ Best Practices
- Idempotency on POSTs: Always send the
idempotencyKeyheader (see./common/components.yaml) for create operations. - Filter precisely: Use provided query filters (
status,regime,accountId, etc.)—do not rely on undocumented parameters. - Org scoping: Use the correct
orgIdin the path for every call. If your platform uses an additional org header (orgIdHeader), include it as defined. - Schema-only payloads: Ensure request bodies contain only properties defined in the YAML; avoid extra fields.
🔒 Security Guidelines
- Scopes: Match operation scopes (
read:compliancefor reads,write:compliancefor mutations). - API Key usage: When using
apiKey, pass it exactly per./common/components.yaml(header/query as defined there). - Data minimization: Only submit
evidenceUrls,docs, orreasonsnecessary for the operation. - Access control: Keep
orgIdand account/resource IDs scoped to your tenant processes.
🚀 Performance Optimization
- Pagination: Use
cursorandlimitfrom./common/pagination.yamlon list endpoints to iterate large datasets. - Targeted queries: Apply
status,regime, and ID filters to reduce payload sizes. - Selective updates: Use
PATCHpayloads with only the fields you need to change.
🔧 Advanced Configuration
- Jurisdiction routing: Use the
regimeenum for accreditations to drive jurisdiction-specific workflows downstream. - Whitelist governance: Combine
tokenClassIdandwalletIdwithstatustransitions (APPROVED⇄REVOKED) to align with token lifecycle events. - Risk signaling: When updating KYC, use
riskScore(0–100) to propagate review outcomes through your internal rules.
🔍 Troubleshooting
-
401 Unauthorized / 403 Forbidden
- Verify OAuth2 token scopes (
read:compliance/write:compliance) - Confirm
apiKeypresence and placement per./common/components.yaml - Ensure the
orgIdin the path is valid for the authenticated principal
- Verify OAuth2 token scopes (
-
400 BadRequest / 422 ValidationError
- Check enums:
type,status,regimemust be exact - Validate formats: UUIDs and
date-timestrings - For
riskScore, ensure 0–100 float range
- Check enums:
-
409 Conflict (Whitelist POST)
- Indicates a conflict condition as defined in the service (e.g., existing entry)
-
404 NotFound (Whitelist PATCH)
- The
idpath parameter must reference an existing whitelist entry
- The
-
Parameter naming note (from YAML)
GET/PATCH /orgs/{orgId}/kyc/cases/{caseId}declare a path parameter namedkycIdin the spec. Use the path shape shown here and the parameter name exactly as defined by the YAML when generating clients or server stubs.
📊 Monitoring & Observability
- Request logging: Log method, path, and response codes for all compliance calls.
- Audit alignment: Retain mutation requests/responses for
POST/PATCHto support audits. - KPIs: Track volumes for KYC cases by
status, accreditation byregime/status, and whitelist transitions.
📚 Additional Resources
- API Documentation:
../api-documentation/ - Service Overview:
../overview/ - OpenAPI Specification:
/openapi/compliance.yaml_(This guide is generated strictly from the YAML and references./common/_.yamlfor shared components.)*