TPP Standards · v2.1 · Consent · API Guide

Consent — API Guide 3 min read

In UAE Open Finance, a Consent is a structured, user-authorized agreement that grants a TPP specific rights to access data or initiate payments on a user's behalf. All API access is consent-bound — you cannot call a resource endpoint without a valid, authorized consent.

Consents are created through the Pushed Authorization Request flow (FAPI 2.0 PAR). Rather than creating a consent resource directly, the TPP embeds the consent definition inside a signed Request JWT and pushes it to the Authorization Server. The user then authenticates at the LFI and explicitly authorizes the consent.

02 End-to-end

API sequence flow

03 Stage the consent

POST /par

POST/par

Push the signed Request JWT to the Authorization Server. The authorization_details inside the JWT carries the full consent definition — account permissions, payment amounts, billing details, and (for payments) encrypted PII.

typescript
// PAR endpoint is read from .well-known/openid-configuration —
// not constructed from the issuer URL (it lives on a different host).
const PAR_ENDPOINT = discoveryDoc.pushed_authorization_request_endpoint

const parResponse = await fetch(PAR_ENDPOINT, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    request:               requestJWT,           // signed Request JWT containing authorization_details
    client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    client_assertion:      await buildClientAssertion(),
  }),
  // agent: new https.Agent({ cert: transportCert, key: transportKey }),
})

const { request_uri } = await parResponse.json()

For the full construction of authorization_details — including field tables, PII encryption, and code examples — see the specific API guides, for example:

See Preparing the Request JWT for how to build and sign the Request JWT, and POST/par for the full API reference.

04 Hand off to the LFI

Redirecting the user

Build the authorization URL using the authorization_endpoint from the LFI's .well-known/openid-configuration and the request_uri returned by /par:

typescript
// authorization_endpoint from .well-known/openid-configuration
// Each LFI sets its own path — there is no fixed structure
// e.g. on the altareq1 sandbox: 'https://auth1.altareq1.sandbox.apihub.openfinance.ae/auth'
const AUTHORIZATION_ENDPOINT = discoveryDoc.authorization_endpoint

const authCodeUrl = `${AUTHORIZATION_ENDPOINT}?client_id=${CLIENT_ID}&response_type=code&request_uri=${encodeURIComponent(request_uri)}`

window.location.href = authCodeUrl
// or server-side: res.redirect(authCodeUrl)

The user will authenticate with their bank and authorize the consent on the LFI's authorization screen.

05 Back to the TPP

Handling the callback

After authorization, the LFI redirects the user back to your redirect_uri:

https://yourapp.com/callback?code=fbe03604-baf2-4220-b7dd-05b14de19e5c&state=d2fe5e2c-77cd-4788-b0ef-7cf0fc8a3e54&iss=https://auth1.altareq1.sandbox.apihub.openfinance.ae

Always validate state and iss before proceeding. See Handling Authorization Callbacks for the full security guide.

06 Exchange the code

POST /token

POST/token

Exchange the authorization code for an access token and refresh token. The code_verifier must match the code_challenge sent in the Request JWT (PKCE).

typescript
// Token endpoint is read from .well-known/openid-configuration —
// not constructed from the issuer URL (it lives on a different host).
const TOKEN_ENDPOINT = discoveryDoc.token_endpoint

const tokenResponse = await fetch(TOKEN_ENDPOINT, {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type:            'authorization_code',
    code,
    redirect_uri:          REDIRECT_URI,
    code_verifier:         codeVerifier,
    client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
    client_assertion:      await buildClientAssertion(),
  }),
  // agent: new https.Agent({ cert: transportCert, key: transportKey }),
})

const { access_token, refresh_token, expires_in } = await tokenResponse.json()

The access token is consent-bound — it carries the scope and ConsentId granted during authorization. See Tokens & Assertions for token lifetimes and the refresh flow.

When obtaining an access token you also receive the current state of the consent (including the status) to confirm it has moved to the Authorized state before making resource API calls.

07 Track changes over time

Maintaining consent state

After a consent is created, your application needs to track its status over time. There are two approaches:

Option 1

Subscribe to webhook events Recommended

When a consent is created with subscription.Webhook.IsActive: true, on every consent status change — for example, when a user revokes, or the consent expires — the API Hub delivers a Consent Status Event to your registered webhook URL. This avoids the need to poll and ensures your application reacts to status changes in real time.

Note: as Events are delivered as JWEs, this approach requires a valid Encryption Certificate on your Application. See the Consent Status Event — API Guide for the full flow.

Option 2

Poll the consent endpoint

If you need to check the current state of a consent on demand, call the consent endpoint directly. Both endpoints require a client credentials access token — not the user's consent-bound access token.

Obtaining a client credentials token

typescript
const params = new URLSearchParams({
  grant_type:            'client_credentials',
  scope:                 'openid accounts',   // or 'openid payments' for service initiation
  client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
  client_assertion:      await buildClientAssertion(),
})

// Reuse the TOKEN_ENDPOINT discovered above (discoveryDoc.token_endpoint).
const tokenResponse = await fetch(TOKEN_ENDPOINT, {
  method:  'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body:    params.toString(),
  // agent: new https.Agent({ cert: transportCert, key: transportKey }),
})

const { access_token } = await tokenResponse.json()

Bank Data Sharing

typescript
const LFI_API_BASE = process.env.LFI_API_BASE_URL!

const consentResponse = await fetch(
  `${LFI_API_BASE}/open-finance/v2.1/account-access-consents/${consentId}`,
  {
    headers: { Authorization: `Bearer ${access_token}` },
    // agent: new https.Agent({ cert: transportCert, key: transportKey }),
  }
)

const { Data: { Status, Permissions, ExpirationDateTime } } =
  await consentResponse.json()

if (Status !== 'Authorized') {
  throw new Error(`Consent not authorized: ${Status}`)
}

See GET/account-access-consents/{ConsentId} for the full response schema.

You can also retrieve all consents created under a long-lived base consent by passing baseConsentId as a query parameter to GET/account-access-consents.

Service Initiation

typescript
const consentResponse = await fetch(
  `${LFI_API_BASE}/open-finance/v2.1/payment-consents/${consentId}`,
  {
    headers: { Authorization: `Bearer ${access_token}` },
    // agent: new https.Agent({ cert: transportCert, key: transportKey }),
  }
)

const { Data: { Status, ControlParameters, ExpirationDateTime } } =
  await consentResponse.json()

if (Status !== 'Authorized') {
  throw new Error(`Consent not authorized: ${Status}`)
}

See GET/payment-consents/{ConsentId} for the full response schema.

You can also retrieve all payment consents under a long-lived base consent by passing baseConsentId as a query parameter to GET/payment-consents.

Insurance Data Sharing

typescript
const consentResponse = await fetch(
  `${LFI_API_BASE}/open-finance/v2.1/insurance-consents/${consentId}`,
  {
    headers: { Authorization: `Bearer ${access_token}` },
    // agent: new https.Agent({ cert: transportCert, key: transportKey }),
  }
)

const { Data: { Status, Permissions, ExpirationDateTime } } =
  await consentResponse.json()

if (Status !== 'Authorized') {
  throw new Error(`Consent not authorized: ${Status}`)
}

See GET/insurance-consents/{ConsentId} for the full response schema.

You can also retrieve all insurance consents under a long-lived base consent by passing baseConsentId as a query parameter to GET/insurance-consents.

Consent States

A consent moves through a defined lifecycle — AwaitingAuthorizationAuthorizedConsumed / Expired / Revoked. See Consent Overview for the full state machine and transition rules.