UAE open finance for LFIs: an end-to-end Zerq build, from empty cluster to go-live
UAE open finance for LFIs, built end to end: deploy Zerq in-country, front Ozone Connect for the API Hub, then add limits, records and conformance checks.
- how-to
- banking
- open-banking
- compliance
- workflows
UAE open finance requires every CBUAE-licensed bank and insurer to share customer data and accept initiation requests (payments for banks, quotes for insurers) through one central platform. Under the Open Finance Regulation (Circular No. 03/2025), TPPs never call a licensed financial institution (LFI) directly: they call the API Hub, and the Hub calls an API you host, Ozone Connect, over mutual TLS. Your gateway is the governed, logged, in-country front door for that one caller.
Open banking gateway blueprints from the UK and EU make the bank's gateway the TPP-facing FAPI endpoint. In the UAE the Hub does that work; its Integration Overview says LFIs "are not required to provide an externally facing API, manage TPP identities or permissions, or handle consent parameters for each API request." The hard parts are Ozone Connect payloads and errors over your core systems, a 250 ms budget per Hub call, five years of unaltered records, and a Test Suite that must pass at 100%. Treating the link as plumbing fails too: every response is customer-specific and must never be served from cache, error bodies outside the Ozone Connect shape reach the TPP as generic 5xx errors, and an unsized rate limit returns 429s.
This post builds the LFI side from Nebras onboarding to go-live. For background, start with what the UAE open finance regulation requires; the TPP integration guide covers the other side of the Hub.
What the LFI side actually involves
CBUAE licenses TPPs; the Hub and the Open Finance Trust Framework (OFTF) check each TPP's licence and roles, run the FAPI 2.0 profile and hold the consent record. You own the ingress, the Ozone Connect API over your core systems, decryption of PII encrypted to your Enc1 key, the five-year record, and the customer journey covered at the end. Zerq fronts Ozone Connect and captures the record.
The Hub presents client certificate C4, issued by the OFTF; your Ozone Connect server presents S4. On top of mTLS you choose an application-layer option (Step 3). You host the API sets in scope for your licence (Bank Data Sharing and Bank Service Initiation for banks, Insurance for insurers), the health checks, and the Consent Events and Actions endpoints; the Hub docs label those optional, but banks offering service initiation should treat validate as required under CRG-10.7. The API Hub Sandbox, which you don't host, is a reference implementation to compare against.
Building the LFI side in Zerq
Step 1: onboard with Nebras and create your certificates
Your primary technical contact (PTC) onboards to the OFTF Sandbox, registers on the Nebras Service Desk and raises a pre-production onboarding ticket. Nebras creates four forms: the Prerequisites questionnaire (LFI code, CAAP or not, API sets, optional consent features), the application-layer authentication questionnaire, the environment configuration form and the Postman collection form. Production repeats this on OFTF Production.
For S1, S3 and Sig2, Ozone generates the keys and hands you CSRs to issue on the OFTF. You create C3 (your client certificate for calls to the Hub), S4, Sig4 and Enc1 yourself, with a JWKS URL and KID for each, in the Sandbox directory for pre-production and the production directory later. Certificates last 13 months; replace each at least a month before expiry. Then you and Ozone verify mTLS both ways: you to the Consent Manager and authorization server health endpoints, the Hub to Ozone Connect.
Step 2: deploy Zerq in-country, once per Hub environment
Run one Zerq deployment per Hub environment; configuration moves between them as exported collection JSON. Each deployment is the backend (gateway, management API and MCP on one port), MongoDB, Redis or KeyDB, and your OIDC provider for management sign-in. The UAE data residency post covers Kubernetes secrets, environment and the air-gapped path; you write the manifests yourself. Change these settings:
env:
- name: GATEWAY_ENABLE_CACHE
value: "false" # responses are customer-specific: never cache Hub traffic
- name: GATEWAY_ENABLE_IDEMPOTENCY
value: "false" # payment idempotency is the Hub's x-idempotency-key
- name: GATEWAY_ENABLE_LIMITER
value: "true" # pair with an explicit policy (Step 7)
- name: AUTH_METHODS_ENABLED
value: "token,mtls"
# Client credentials option only (this enables oidc automatically):
# - name: GATEWAY_OIDC_ENABLED
# value: "true"
# - name: GATEWAY_OIDC_ISSUER_URL
# value: "https://auth.internal.example.ae/realms/hub"
# - name: GATEWAY_OIDC_AUDIENCE
# value: "ozone-connect"
- name: AUDIT_ENABLED
value: "true"
The shipped Compose file turns the response cache on. Set GATEWAY_ENABLE_CACHE=false for open finance traffic: every response is customer-specific and must never be served from cache. The gateway, management API and MCP share one port, so have the Hub-facing ingress forward only the Ozone Connect base path; the management API and MCP must not be reachable from it.
Step 3: terminate the Hub's mTLS at your ingress and map it to one client
Zerq listens on plain HTTP and does not validate client certificates, so the ingress does the TLS work:
- Present S4 and require a client certificate chained to the environment's OFTF CA: Sandbox in pre-production, production in production.
- Pin the Hub's C4 subject; a chain check alone accepts any OFTF certificate, TPP ones included.
- Check revocation via the Trust Framework CRL or OCSP; the Certificate Standard caps status caching at 15 minutes.
- Admit only the Hub's egress IP, from the environment configuration form. Zerq sees the ingress address, so enforce it here.
- Strip inbound
X-Client-ID,X-Profile-IDandX-Proxy-ID, because only the ingress may set gateway routing headers, and set the first two yourself.
Then in Zerq:
- In Clients, click New Client, name it
OFP API Hub (pre-production)and leave Developer Portal Access off. You add the collection after Step 4. - In the client's Profiles, click New Profile and allow
GET,POSTandPATCH. - Click Auth on the profile and set Authentication Type for your option (table below).
- Copy the client and profile IDs into the ingress.
| Hub option | Zerq profile | What to know |
|---|---|---|
| API key | API Token | Zerq generates the token you register with the Hub. It expires after TOKEN_EXPIRY_DEFAULT (30 days) unless you set a longer Expiry (hours) in the Auth dialog, then every Hub call gets 401, so schedule rotation with Nebras |
| mTLS only | mTLS | Zerq still requires an Authorization header, so the ingress sets one |
| Client credentials | OpenID Connect | Needs the GATEWAY_OIDC_* settings (Step 2); one issuer and audience, RS256/384/512 only; any client_id claim must equal the Zerq client ID |
| JWT Auth | mTLS | Zerq cannot verify PS256; your Ozone Connect implementation does |
Service Initiation Tokens replace the Authorization value on service-initiation calls, which an API Token profile would reject; if you use them, choose mTLS and validate the token in the payments back end. With the Hub as the only TLS client, there is no per-TPP client or certificate at the LFI; that pattern belongs to bilateral open banking. TPP attribution comes from the logged o3-caller-* headers.
Step 4: import the Ozone Connect specs into one collection
Zerq imports the Ozone Connect OpenAPI 3.0 specs directly:
- In Collections, click New Collection and upload the spec on the Import from OpenAPI tab. Zerq creates an inactive collection with one draft proxy per path.
- In Edit Collection, set Base Path to the path of your registered Ozone Connect base URL, such as
/ozone-connect, and point Endpoint at your internal Ozone Connect service. - Under API Credentials, attach the upstream credential from Credentials, using env-backed credential fields so key material stays out of the database.
- Select the served proxies, choose Bulk Actions and Publish, set the collection Active, and add it to the Hub client.
The Hub knows one base URL per environment and Zerq routes by longest matching base path, then proxy path, so merge your API sets into one spec before import. That makes Bank Data Sharing's GET /accounts and Bank Service Initiation's POST /accounts one proxy with both methods. Parameters and schemas come from one operation per path, so check a merged path's query parameters against both operations; the direct path rejects undeclared ones with 400. /echo-cert needs the ingress to forward the Hub's certificate in a header. Imported schemas are not enforced at runtime; use validate_node where a request must be checked.
Step 5: leave consent to the Hub, and add a check only for defense in depth
The Hub is the consent system of record and rejects any consent not in the Authorized state; the Ozone Connect guide calls Consent.Invalid "unlikely to be implemented by the LFI". Only one of your consent duties passes through Zerq: answering POST /consent/action/validate when the Hub asks you to validate payment consent parameters. The rest run beside the gateway.
If your risk function still wants an independent check, say for drift between your suspension records and the Hub, Steps 3 and 4 of our consent enforcement how-to give the lookup-and-branch pattern. It models a bilateral setup, so adapt it five ways:
- Read the consent ID as
request.headers['O3-Consent-Id'][0](keys canonicalized, values arrays), and skip its Step 2 schema, which requirespath_paramsthe trigger output lacks. - Pass only
Authorized, catchingSuspendedabove all; itsactivecheck fails every call. - Query your own suspension or consent mirror, not the Hub Consent Manager, which needs C3 and possibly JWT Auth.
- Forward with
http_request_node, not itsproxy_node(Step 6), without retries, keeping lookup and forward inside 250 ms. - Nest each node's
configandinputsunderdata, and return every refusal and lookup failure from aresponse_nodewithstatus_code,headersandbodyindata.inputs, in the Ozone Connect error shape, not its custom 400, 403 or 503 bodies.
Step 6: use workflows for payload shape and error mapping
Many proxies need no workflow: the direct path forwards the Hub's headers and query string to the Endpoint. Attach a workflow (proxy row, then Workflow) where the gateway adds something: set_node, code_node or xml_node to map core formats to the Ozone Connect schema, error mapping, or validate_node for parameter checks on workflow proxies. Inside a workflow, forward with http_request_node and map headers and query explicitly. The consented scope (accountIds, insurancePolicyIds) and paging arrive in the query; drop them and the adapter returns everything the customer holds. An insurer's motor policy proxy, where http_trigger_1 is the builder's ID for the trigger node:
{
"id": "policy_adapter",
"type": "http_request_node",
"data": {
"config": {
"timeout_ms": 200,
"credentials_id": "<policy-adapter-mtls-credential-id>"
},
"inputs": {
"url": "https://policy-adapter.internal.example.ae/motor/policies",
"method": "GET",
"headers": {
"o3-provider-id": "{{ $json['http_trigger_1'].request.headers['O3-Provider-Id'][0] }}",
"o3-caller-org-id": "{{ $json['http_trigger_1'].request.headers['O3-Caller-Org-Id'][0] }}",
"o3-ozone-interaction-id": "{{ $json['http_trigger_1'].request.headers['O3-Ozone-Interaction-Id'][0] }}",
"o3-consent-id": "{{ $json['http_trigger_1'].request.headers['O3-Consent-Id'][0] }}",
"o3-psu-identifier": "{{ $json['http_trigger_1'].request.headers['O3-Psu-Identifier'][0] }}"
},
"query_params": {
"insurancePolicyIds": "{{ $json['http_trigger_1'].request.query['insurancePolicyIds'] }}",
"page": "{{ $json['http_trigger_1'].request.query['page'] }}",
"page-size": "{{ $json['http_trigger_1'].request.query['page-size'] }}"
}
}
}
}
Map the operation's other o3-* headers the same way, including o3-is-caap-consent-operation with CAAP. The back end must return only records matching both the consented IDs and o3-psu-identifier, answering Consent.InvalidUserIdentifier on a mismatch. The node's error output fires on transport failures, timeouts and non-2xx answers; wire it to a response_node in the Hub's error shape:
{
"id": "adapter_unavailable",
"type": "response_node",
"data": {
"config": { "continue_after_response": false },
"inputs": {
"status_code": 500,
"headers": { "Content-Type": ["application/json"] },
"body": {
"errorCode": "GenericRecoverableError",
"errorMessage": "Policy data is temporarily unavailable. Please try again shortly."
}
}
}
}
The Hub passes errorMessage to the TPP, which may show it to the customer, so write it for a person. For specific codes such as Consent.AccountTemporarilyBlocked, add a switch_node on response.status_code with a response_node per branch. Set timeout_ms explicitly on each http_request_node, inside the 250 ms budget. Test with Execute workflow, then Validate, Save and Enable Workflow.
Step 7: size a capacity ceiling for Hub traffic
Throttling TPPs is the Hub's job; at the LFI a limit only keeps spikes off your core systems. Zerq applies one policy per client, so the Hub client's policy covers all Hub traffic. With none, the limiter's default of GATEWAY_LIMITER_MAX (100) requests per GATEWAY_LIMITER_EXPIRATION (one minute) applies. In Policies, click New Policy, set Rate Limit Interval and Requests per interval above your stress-tested peak, and select it on the Hub client. The same form sets a Usage Quota defaulting to 10,000 requests a day for the whole client; raise it above your daily peak, or calls fail with 429 quota_limit_exceeded. Windows are fixed, not sliding. The 429 body ({"error":"rate_limit_exceeded","message":"...","client_id":"..."}) is not the Ozone Connect shape, so the Hub relays a generic error; confirm limiter refusals from the 429 body or your ingress access log. And the parity principle (CRG-1.8) bars constraints stricter than your other digital channels, so the ceiling is a breaker, not a throttle.
Step 8: keep the records the standards ask for
The Standards require LFIs to keep all Hub requests and responses for at least five years, in their original, unaltered state. Zerq's request log records proxied calls with routing, status, latency, identity, request headers and bodies, in MongoDB:
{
"request_id": "5f0c2a9e-7d41-4b8a-9e36-1c2d8b7a4f10",
"created_at": "2026-10-06T07:42:19Z",
"method": "GET",
"path": "/ozone-connect/motor-insurance-policies",
"status": 200,
"latency": 143,
"client_id": "66f2b1c4e8a9d30012ab4c71",
"profile_id": "66f2b1d9e8a9d30012ab4c72",
"request_headers": {
"O3-Caller-Org-Id": "3c1d2b9e-…",
"O3-Consent-Id": "aac-7f41c2…",
"O3-Ozone-Interaction-Id": "e81f04a6-…"
}
}
(This is an illustrative rendering of the log fields; target_url is set only for direct-path and proxy_node calls, so it stays empty for this http_request_node workflow.) The gateway request log is your operational record; keep ingress access logs too, capturing raw exchanges if compliance reads "unaltered" strictly, and archive both to write-once in-country storage for the five-year requirement.
Logs payload search skips headers, so tracing an o3-ozone-interaction-id means querying the log store. There is no built-in retention setting: plan five years of storage, ship records to the archive via GET /api/v1/logs or a MongoDB export, and restrict access, because logs hold personal data.
The audit log (see "The audit trail an examiner recognizes" in our regulation overview) records changes to clients, profiles, policies, collections, proxies and credentials. Also commit an Export Collection file to version control after each approved change, so workflow definitions have a reviewed history. The observability dashboard shows request volume, status distribution, failed endpoints and average latency; compute percentiles against the 250 ms budget from the log's latency field.
Step 9: certify in pre-production, then repeat in production
The Testing and Certification Framework asks LFIs for the Ozone Connect Test Suite and Postman collection at 100% in both environments, CX certification by Nebras, a penetration test with no open critical or high findings (Live Proving also asks for medium findings to be fixed), a stress test, live proving with every endpoint exercised by at least two TPPs, and a readiness attestation. FAPI certification sits with the platform. On the gateway side:
- Run the suite with every workflow enabled (see the workflow testing guide); a saved workflow does nothing until enabled.
- Promote with Export Collection, then Import Collection under New Collection in production. Imports keep the exported active, published and workflow-enabled state, and credentials are created per environment, so deactivate the import, create production credentials, re-attach them on the collection and in every workflow node's
credentials_id, then activate. - Create the production Hub client and profile, add the collection, load the IDs into the production ingress, and rerun the suite.
What runs beside the gateway
Three LFI duties never pass through Zerq:
- The authorization journey. The TPP sends the customer to your registered Authorisation URL; your app calls
GET /authon the Hub's headless authorization server (hh.<LFI code>.apihub.openfinance.ae, over C3), runs SCA, and answersPOST /auth/{interactionId}/doConfirmordoFail. CBUAE directive 3057.2025 bars SMS OTP, email OTP and static passcodes as stand-alone factors and requires step-up for payment consents. CAAP, via the AlTareq app, replaces these screens if you choose it in the Prerequisites questionnaire. - The consent management interface, where customers view and revoke consents.
- Outbound Consent Manager calls: suspensions, revocations through your channels and, for banks, a payment-status
PATCHwithin 250 ms of the rail's response, sent from your back ends over C3, with Sig4 JWT Auth if chosen. Zerq does not carry this leg.
Go-live checklist
- OFTF certificates issued per environment, with expiry monitoring (13-month validity, replaced a month early).
- The ingress chains to the right OFTF CA, pins the Hub's C4 subject, checks revocation, admits only the Hub's egress IP and strips
X-Client-ID,X-Profile-IDandX-Proxy-ID. - One Hub client per environment, with the matching authentication type and API Token rotation scheduled if used.
GATEWAY_ENABLE_CACHE=falseeverywhere, and a Hub client policy with rate limit and quota sized from the stress test.- Served proxies published, the collection active, workflows enabled, production credentials attached on the collection and in workflow nodes.
- Workflows forward
o3-*headers, consented IDs and paging parameters, and error paths return prescribederrorCodevalues, tested with the core unreachable. - Request logs and ingress access logs kept unaltered for five years.
- A collection export committed after the last workflow change, and read-only Auditor accounts for compliance.
- The management API and MCP unreachable from the Hub-facing ingress.
- For banks, payment-status
PATCHinside 250 ms of the rail response. - The Test Suite at 100% in pre-production and production.
What this looks like in practice
A UAE motor and health insurer began by drafting a TPP-facing edge with per-TPP certificates, a consent table and partner throttling; the Hub documentation removed that scope. What remained was a SOAP policy system and a vendor adapter returning HTML error pages, and early Test Suite runs failed on error cases. The team put Zerq in front of the adapter: one Insurance collection with only the motor and health proxies published, xml_node unwrapping SOAP, error branches returning prescribed codes, the cache off, and a capacity policy from the stress test. The request log showed which proxies exceeded the 250 ms budget, and a failed call from a Nebras ticket was traced to its log entry by interaction ID.
That is the LFI's share of UAE open finance: one mTLS caller, one Ozone Connect surface, prescribed errors, a tight latency budget and five years of unaltered records. Zerq carries the gateway part in-country; certificates, the authorization journey, PII decryption and archiving stay with the systems built for them.
Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.