Skip to main content

UAE open finance for TPPs: an end-to-end API Hub integration with Zerq

UAE open finance for TPPs, built end to end: Trust Framework certificates, FAPI 2.0 client duties, and Zerq as the governed egress layer to the API Hub.

  • how-to
  • fintech
  • open-banking
  • security
  • compliance
Zerq team

A UAE open finance TPP integration is two projects. One is the regulated client work every third-party provider carries under the CBUAE's Open Finance Regulation (Circular No. 03/2025): authorization, Trust Framework onboarding, and a FAPI 2.0 client that signs its own requests. The other carries your apps' calls to the API Hub with identity, limits and a record attached. Zerq does the second: one governed egress layer, with a collection per LFI, holding your transport certificate inside your own infrastructure.

A TPP never connects to a bank. Every call goes to a per-LFI instance of the API Hub over mutual TLS, with a Trust Framework certificate and access tokens of ten minutes or less bound to it. When several product teams each embed that certificate, renewal becomes a multi-service change, Hub fees have no per-app ceiling, and "which app called which LFI" needs a join across log systems.

Below are the requirements, where Zerq fits, eight build steps and a checklist. The bank side, including how the Hub reaches each LFI, is in the LFI implementation guide; licensing and consent rules are in our UAE open finance regulation guide.

Why the usual TPP integration patterns fall short

A FAPI client library embedded in each product produces the sprawl above: it signs requests but gives no per-app identity, spend control or per-LFI record. Managed gateways come next. Apigee, Azure API Management and Kong Konnect can all present a client certificate upstream, so mutual TLS is not the hard part; where their analytics and control plane keep request data is (why the usual gateway architectures fail the boundary test). And FAPI signing, tested against the OpenID Foundation's relying-party suite, belongs in a dedicated client, not a gateway.

What the UAE framework requires of a TPP

Authorization comes first: an open finance license for data sharing (BDSP), service initiation (BSIP) or both. Banks, finance companies, retail payment service providers, insurers and insurance brokers, and stored-value facilities are deemed licensed but, per law-firm analyses of the original text, still need CBUAE approval or a no-objection first. The regime covers onshore UAE; a DIFC or ADGM firm needs CBUAE authorization to serve onshore customers.

Your FAPI client service runs each consent journey under the UAE FAPI 2.0 profile: discovery on auth1; PAR on as1 with a PS256-signed request object, authorization_details, PKCE and private_key_jwt; redirect to the LFI's authorization URL; callback; token exchange on as1 for a certificate-bound token of ten minutes or less; resource calls to rs1; refresh before expiry. Payments add a signed application/jwt body, an x-idempotency-key and personal data encrypted to the LFI's key.

Consent duties are yours: purpose statements, Emirates ID capture where required, x-fapi-customer-ip-address only when the customer is present, and one-page revoke and pause in a consent dashboard. Say on the cancellation page what happens to data already retrieved, and offer a delete-history option (revocation alone does not oblige deletion). Notify the Hub of revocations made in your app (Consent Setup 5.4) and keep your consent state in step with it (5.3). The Hub rejects requests on any consent not Authorized, but a paused consent stays Authorized at the Hub and the LFI, so pause is yours to enforce. Article 22 caps recurring-transaction consent at 12 months.

Every Hub request and response must be kept for five years, unaltered. Certification runs in the Hub sandbox with your production applications (OIDF relying-party tests, Nebras functional and customer-experience certification); a penetration test and production proving with at least one LFI follow before go-live. Inserting Zerq into a certified path is a material change, so plan to retest. Live Hub calls carry fees published in a 2.5 to 12.5 fils range, LFIs charge under the scheme's commercial model, and nothing is payable before go-live.

Where Zerq sits, and where it does not

Your apps call Zerq, and Zerq calls each LFI's resource server, rs1.<LFI code>.apihub.openfinance.ae, over mutual TLS. Your FAPI client service signs, calls as1 for tokens, stores refresh tokens, and hands apps short-lived access tokens.

ConcernLives inWhy
PAR, request objects, private_key_jwt, PKCEFAPI client serviceZerq has no RSA or EC signing
Token and refresh calls to as1FAPI client serviceZerq's OAuth2 credential is client_secret_basic only
Payment JWS, PII encryption, Hub notificationsFAPI client serviceNeeds your signing and encryption keys
Resource calls to rs1Zerq, one collection per LFIOne certificate holder, per-LFI logs
App identity, methods, limits, quotasZerq clients, profiles, policiesRefusals before any Hub fee

Both need the transport certificate: RFC 8705 binds a token to the certificate's thumbprint, not a connection, so tokens the FAPI client service obtains pass the Hub's check when Zerq presents the same certificate. as1 stays out of Zerq because Zerq logs bodies unredacted, refresh tokens included.

This design is per software statement. Each customer-facing app or brand needs its own FAPI, functional and CX certification; one with its own software statement also needs its own transport certificate (its CN is that statement's ID), mTLS credential and per-LFI collections, since tokens are bound to the certificate.

Building the TPP integration in Zerq

The FAPI client service is yours to build; it needs Step 1's certificates and Step 4's registration call before its first PAR.

Step 1: onboard to the Trust Framework sandbox

Nominate a Primary Business Contact (PBC) and Primary Technical Contact (PTC) to the Trust Framework sandbox; the PBC signs the ecosystem participation document. The PTC creates the Application, your software statement (its Entity Identifier becomes your client_id), with BDSP and/or BSIP roles and redirect URIs, and generates keys and CSRs with the directory's scripts. The Trust Framework issues transport, signing and encryption certificates: RSA-2048, valid 13 months, replaced a month before expiry, with the software statement ID as CN and your organization ID as OU. Of the three keys, only the transport key also goes to Zerq. Production repeats this after CBUAE authorization.

Step 2: deploy Zerq in-country and turn off response caching

Zerq ships as container images with a Docker Compose file, not Kubernetes manifests or a Helm chart. Write your own Deployments for the backend, frontend and portal, with in-country MongoDB and Redis; the residency post shows the Secret and environment variables they need. Run one deployment for the Hub sandbox (*.altareq1.apihub.openfinance.ae) and one for production.

You promote by exporting collections as JSON. Imports carry credential references, not secrets, and credential IDs differ between deployments. Deactivate each collection on import, create the production mTLS credential, re-point Endpoint at the production host, re-attach the credential on the collection and in every workflow node, then activate.

GATEWAY_ENABLE_CACHE: "false"        # every response is customer-specific
GATEWAY_ENABLE_IDEMPOTENCY: "false"  # the Hub owns idempotency
GATEWAY_ENABLE_LIMITER: "true"       # per-app limits (Step 5)
AUTH_METHODS_ENABLED: "token,mtls"
AUDIT_ENABLED: "true"

Set GATEWAY_ENABLE_CACHE=false for open finance traffic: every response is customer-specific and must never be served from cache, and the shipped Compose file turns caching on. Turn GATEWAY_ENABLE_IDEMPOTENCY off too; payment idempotency is handled by the Hub's x-idempotency-key, not the gateway.

Step 3: load the transport certificate as an mTLS credential

  1. Open Credentials, click New Credentials, and choose mTLS (Mutual TLS).
  2. Add the transport certificate, its private key and, as the CA certificate, the Trust Framework chain that issues the Hub's server certificates.

Through the management API, an env-backed version looks like this:

{
  "name": "of-transport-production",
  "type": "mtls",
  "client_certificate_use_env": true,
  "client_certificate_env_var": "OF_TRANSPORT_CERT_PEM",
  "client_private_key_use_env": true,
  "client_private_key_env_var": "OF_TRANSPORT_KEY_PEM",
  "ca_certificate_use_env": true,
  "ca_certificate_env_var": "OF_TRUST_FRAMEWORK_CA_PEM"
}

The CA replaces the system trust store, which suits as1 and rs1 but not auth1, whose certificate is publicly trusted. Keys must be unencrypted PEM. Use env-backed credential fields so key material stays out of the database. Zerq reads them from its process environment, so renewal needs a rolling restart, and does not parse their expiry, so track that outside Zerq.

Step 4: create one collection per LFI and register with it

  1. In Collections, use Import from OpenAPI with the Standards v2.1 account-information spec. Zerq creates an active collection with a default /api/<title> base path, copies the spec's relative server URL into Endpoint, and adds one draft proxy per path.
  2. In Edit Collection, set Base Path to /hub/xxxxyy/ais (xxxxyy is the LFI code), Endpoint to https://rs1.xxxxyy.apihub.openfinance.ae/open-finance/account-information/v2.1 (in the sandbox, the model LFI's host under altareq1.apihub.openfinance.ae) and API Credentials to the mTLS credential.
  3. Publish only the proxies your products and consent service use; unpublished ones return 404.
  4. Repeat for payments under /hub/xxxxyy/pis, with the endpoint ending in /open-finance/payment/v2.1.
  5. For the next LFI, use Duplicate Collection. The copy is active with draft proxies, so nothing routes until you publish. It keeps the source endpoint, credential and workflows, so update the base path, endpoint and the LFI host inside the workflows.
  6. Before the FAPI client service's first PAR to an LFI, call POST /open-finance/onboarding/v1.0/tpp-registration with an empty body from a small workflow (trigger plus an http_request_node with the mTLS credential) on an unpublished proxy, using Execute workflow. A 204 proves the certificate, CA chain and egress route.

Proxies that declare query parameters reject undeclared ones with a 400, so check the import.

Step 5: give every app its own client, profile and policy

A plain proxy forwards every caller header, so Authorization must carry the Hub access token. That fits the mtls auth type: your internal ingress terminates mutual TLS from each workload and sets its X-Client-ID and X-Profile-ID, so the ingress is where each app is authenticated. Strip caller-sent X-Client-ID, X-Profile-ID and X-Proxy-ID there, because only the ingress may set gateway routing headers. Zerq IDs are 24-character ObjectIDs, so map certificate subjects to them (likewise for $zerq_profile_id) rather than forwarding CN or OU:

map $ssl_client_s_dn $zerq_client_id {
  "CN=budgeting-app,OU=apps" "66f1c2a9e4b0d3a7c8e91f02";
  default "";
}
proxy_set_header X-Client-ID  $zerq_client_id;
proxy_set_header X-Profile-ID $zerq_profile_id;
proxy_set_header X-Proxy-ID   "";   # only the ingress sets routing headers
  1. In Clients, click New Client for each app, for example budgeting-app, and assign only the collections it needs.
  2. Click New Profile, set Allowed HTTP Methods to GET for read-only apps (anything else returns 405) and save. Then use Manage Authentication on the profile row and choose mTLS (Mutual TLS).
  3. In Policies, create a policy with a rate limit and a quota (Usage Quota Interval of 1, 7 or 30 days) and select it on the client.

The quota doubles as spend control: a runaway sync job stops at 429 quota_limit_exceeded before generating Hub or LFI charges. Limits are per client, in fixed windows, with no per-LFI caps.

AI agents are apps too. They use the gateway MCP endpoint (/mcp), which sends its JSON-RPC calls as POST, so the agent's profile must allow POST (plus GET for the SSE stream). Keep the agent read-only at the proxy level instead: give its client only the collection for your service that normalizes Hub payloads, whose proxies declare only GET, so any other method returns 405. execute_endpoint calls back through the gateway as the agent's own client, so its policy and per-agent request logs apply (MCP). App teams get its docs and the Testing Console in the developer portal; grant portal access on sandbox clients only. Get a legal read before exposing Hub data to partners: the regulation restricts onward sharing.

Step 6: route consent updates and enforce pause

Revocation is a write: notifying the Hub means PATCH /account-access-consents/{ConsentId} or /payment-consents/{ConsentId} on rs1. Give your consent dashboard's service its own client with a GET and PATCH profile; only the consent proxies declare PATCH, and product apps stay GET-only.

For changes made elsewhere, such as at the LFI, subscribe to Consent State Update and Payment Status Update notifications in the PAR authorization_details, or poll the consent. Notifications arrive as a JWE wrapping a Hub-signed JWS and must be acknowledged with 202 or 204. Host that webhook in the FAPI client service: Zerq routes need a Zerq client identity the Hub does not send, and only that service holds your encryption key.

For pause, have the FAPI client service stop issuing tokens for a paused consent. To refuse live tokens too, add a Zerq workflow that checks a pause flag by consent ID before any Hub call, as in our consent enforcement workflow guide.

Step 7: put a workflow on the payment submission proxy

Payment submission deserves a workflow, so malformed requests never leave your network. On the /payments proxy, click Workflow and chain http_trigger (id trigger), validate_node, code_node and http_request_node. The validate_node checks $json['trigger'].request.headers (canonical names, array values) against a schema requiring Authorization, X-Fapi-Interaction-Id, X-Idempotency-Key and a Content-Type matching ^application/jwt; wire invalid to a response_node returning 400 with validation_errors.

The Hub headers must carry x-fapi-customer-ip-address on customer-present payments (CRG-25.2) and x-consent-id for payments under a Combined Consent Group. An unattended call must not carry x-fapi-customer-ip-address, so the code_node copies only the headers that are present:

{
  "id": "build_headers",
  "type": "code_node",
  "data": {
    "config": {
      "code": "const h = $json['trigger'].request.headers; const out = {}; for (const k of ['Authorization', 'X-Fapi-Interaction-Id', 'X-Idempotency-Key', 'X-Fapi-Auth-Date', 'X-Fapi-Customer-Ip-Address', 'X-Customer-User-Agent', 'X-Consent-Id']) { if (h[k] && h[k][0]) out[k] = h[k][0]; } return out;"
    }
  }
}
{
  "id": "submit_payment",
  "type": "http_request_node",
  "data": {
    "config": { "credentials_id": "<mtls-credential-id>", "timeout_ms": 10000 },
    "inputs": {
      "url": "https://rs1.xxxxyy.apihub.openfinance.ae/open-finance/payment/v2.1/payments",
      "method": "POST",
      "headers": "{{ $json['build_headers'].result }}",
      "body": {
        "type": "raw",
        "content_type": "application/jwt",
        "content": "{{ $json['trigger'].request.body }}"
      }
    }
  }
}

Leave retry_config unset on payment submission; recovery belongs to the app. On a 502 or timeout, the app replays the identical signed request with the same x-idempotency-key; the Hub treats a repeat from the same TPP within 24 hours as the same payment. Final status comes from Payment Status Update notifications, or from GET /payments/{PaymentId} once a 201 has returned the ID.

Route success to a response_node returning the Hub's status, Content-Type and body. error also covers transport failures, so a condition_node on $json['submit_payment'].response.status_code > 0 passes Hub errors through and turns status 0 (timeout, TLS failure) into a 502. Each branch needs its own response_node, since a node with several incoming edges waits for all of them, plus a kafka_node shipping the sent headers, signed body and full Hub response to your archive.

Forward to the Hub with http_request_node, not proxy_node, and map headers and query explicitly, so the workflow definition shows exactly what the Hub receives. Test with Execute workflow, then save, switch on Enable Workflow and publish; until it is enabled, the proxy forwards as a plain proxy (workflow testing guide).

Step 8: prove the path in the sandbox

Use Listen for request, send one sandbox payment request to the test URL it shows (with a sandbox token only, since the captured request is kept in full for replay), then run the payment workflow on the captured request with Execute workflow. Logs should show 401 for unknown identities and 405 for writes from read-only apps. Confirm rate-limit and quota refusals from the 429 response body, which carries client_id, or from your ingress access log. Revoke a test consent and confirm the Hub reports it Revoked. Run Nebras certification through this same path.

What the request log records for Hub calls

Each logged call carries routing, identity and timing fields plus full headers and bodies. A plain-proxy read, trimmed:

{
  "request_id": "6b1d0f3e-8c2a-4e57-9d41-0a7c5e2b9f13",
  "created_at": "2026-10-06T08:41:12Z",
  "method": "GET",
  "path": "/hub/xxxxyy/ais/accounts/acc-2291/balances",
  "target_url": "https://rs1.xxxxyy.apihub.openfinance.ae/open-finance/account-information/v2.1/accounts/acc-2291/balances",
  "status": 200,
  "latency": 212,
  "client_id": "66f1c2a9e4b0d3a7c8e91f02",
  "profile_id": "66f1c2b4e4b0d3a7c8e91f05",
  "collection": "66f1a7d0e4b0d3a7c8e91e77",
  "client_ip": "10.20.4.11"
}

(An illustrative rendering of the documented log fields.) On plain proxies, target_url records the LFI URL Zerq called (empty if the connection failed); workflows calling the Hub through http_request_node leave it empty. Zerq adopts an incoming X-Request-ID as the log's request ID, so have apps send the same UUID as x-fapi-interaction-id and disputes become a filter in Logs, alongside client and collection.

Headers and bodies are stored unredacted, so treat the viewer role as privileged. There is no built-in retention setting, and on workflow proxies the log holds the app-facing exchange, not the Hub call. The gateway request log is your operational record; keep ingress access logs too, and archive both to write-once in-country storage for the five-year requirement, feeding it from GET /api/v1/logs and the payment workflow's kafka_node, alongside your FAPI client's as1 traffic.

The audit log, read through the Auditor role, records changes to clients, profiles, policies, credentials, collections and proxies with the actor's identity. Commit an Export Collection file to version control after each approved change, so workflow definitions have a reviewed history (observability).

Go-live checklist

  • Production Trust Framework access after CBUAE authorization; software statement, certificates and FAPI client service aligned.
  • tpp-registration returned 204 through Zerq for every LFI you will call.
  • Per software statement, one transport certificate from one secret source in the FAPI client service and Zerq, renewal scheduled.
  • Response cache and idempotency middleware off in every deployment.
  • One client per app, scoped collections, GET-only profiles for read-only apps, a quota on every client.
  • X-Client-ID and X-Profile-ID set only by your ingress, X-Proxy-ID stripped there.
  • A dashboard revocation reaches the Hub as a PATCH through Zerq, and the consent reads Revoked afterwards.
  • Status notifications decrypted and acknowledged, or polling in place.
  • A paused consent produces no Hub calls from any app.
  • Payment workflow published: malformed requests get 400 without a Hub call, FAPI risk headers reach the Hub unchanged, Hub errors reach the app unchanged, transport failures return 502.
  • A write-once, in-country, five-year archive receiving Zerq logs, ingress access logs, payment-workflow Hub exchanges and as1 traffic.
  • Viewer, Auditor and admin roles in production held by named people only.

What this looks like in practice

A payments app with checkout and budgeting started the usual way: checkout embedded a FAPI client library and the transport certificate, and budgeting called checkout's wrapper. In a production-proving rehearsal, a budgeting sync job retried on every 401, tripled call volume against one LFI, and nobody could tell which product made which calls.

Now the FAPI client service alone signs and talks to as1, and Zerq alone calls rs1. Budgeting's own client, GET-only profile and 30-day quota turn the same retry storm into 429s at Zerq. Their response bodies name the client ID, and the ingress access log, which records X-Client-ID, shows which client tripped the quota. Malformed checkout payments stop at the edge with a 400, and disputes are traced by one interaction ID. Transport certificate renewal is one secret update and a rolling restart of both components; signing and encryption certificates rotate in the FAPI client service.

A UAE TPP has to be its own FAPI 2.0 client; no gateway removes that. What Zerq takes on for fintech and payments teams is everything around the Hub call: one certificate holder for resource traffic, a collection per LFI, per-app identity and spend limits, and a record tying app, LFI and outcome together, inside your own infrastructure.


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.