Consent Journey — API Guide 6 min read
End-to-end implementation guide for the consent authorization journey: from the TPP's POST/par through your Authorization Endpoint, the Headless Heimdall and Consent Manager API calls, and the final doConfirm/doFail redirect back to the TPP.
What must be in place before you start
Before implementing the consent journey, ensure the following are in place:
- API Hub onboarded — Your API Hub instance is provisioned and your environment-specific configuration is complete
- C3-hh-cm-client application created — Registered in the Trust Framework with mTLS connectivity established in both directions. See Creating the C3-hh-cm-client Application
- Connectivity verified — Bidirectional mTLS connectivity confirmed between your systems and the API Hub. Use GET
/hello-mtlson both the Headless Heimdall and Consent Manager base URLs to verify. See Connectivity & Certificates - Authorization Endpoint registered — Your Authorization Endpoint is configured in the API Hub to receive TPP user redirects
Required API implementations
You MUST implement the following endpoints:
| Endpoint | Direction | Purpose |
|---|---|---|
GET/auth | LFI → API Hub | Initiate the authorization interaction |
GET/consents/{consentId} | LFI → API Hub | Retrieve the full consent details |
PATCH/consents/{consentId} | LFI → API Hub | Update consent status, end user identifiers, and account IDs |
POST/auth/{interactionId}/doConfirm | LFI → API Hub | Complete the authorization interaction and redirect back to TPP successfully |
POST/auth/{interactionId}/doFail | LFI → API Hub | Complete the authorization interaction and redirect back to TPP with a failure |
End-to-end flow diagram
Steps 1–3 — TPP /par, optional validate, optional event
Step 1 — TPP creates the consent via /par
The consent journey begins when a TPP sends a Pushed Authorization Request (POST/par) to the API Hub. The TPP embeds the consent definition inside a signed Request JWT. See the TPP Consent API Guide for the TPP's API Guide and POST/par for the full /par request structure.
At this point your LFI systems are not yet involved — the API Hub receives the request and begins processing the consent.
Step 2 — (Optional) Validate the consent
If your LFI has configured the POST/consent/action/validate endpoint in the API Hub, the API Hub will call your Ozone Connect server with the full consent payload before the consent is created.
This gives your LFI the opportunity to inspect the consent and determine whether it is one you support — for example, validating that a debtor account exists in your systems, or that the requested permissions are supported.
Your LFI MUST return data.status set to one of:
| Status | Effect |
|---|---|
valid | The consent is created and processing continues |
invalid | The consent is rejected and an error is returned to the TPP |
// Example: handling POST /consent/action/validate on your Ozone Connect server
app.post('/consent/action/validate', (req, res) => {
const consent = req.body
// Perform your validation logic
const isSupported = validateConsentIsSupported(consent)
res.status(200).json({
data: {
status: isSupported ? 'valid' : 'invalid',
code: isSupported ? undefined : 'UNSUPPORTED_CONSENT',
description: isSupported ? undefined : 'The requested consent type is not supported',
},
meta: {},
})
})If you have not configured the POST/consent/action/validate endpoint, the API Hub assumes all consents are valid and creates them immediately.
See the Validate Consent API Reference for the full request and response schemas.
Step 3 — (Optional) Receive the consent event
If your LFI has configured the POST/consent/event/{operation} endpoint, the API Hub will call POST/consent/event/post on your Ozone Connect server once the consent has been successfully created.
This is an informational notification — the API Hub does not expect a response body. Return 204 No Content to acknowledge receipt.
// Example: handling POST /consent/event/:operation on your Ozone Connect server
app.post('/consent/event/:operation', (req, res) => {
const { operation } = req.params // 'post' for creation, 'patch' for updates
const consent = req.body
// Store or log the consent event for your records
logConsentEvent(operation, consent)
res.status(204).send()
})Steps 4–6 — receive redirect, GET /auth, GET /consents
Step 4 — End user is redirected to your Authorization Endpoint
After the consent is created via POST/par, the TPP redirects the end user to your Authorization Endpoint with the following query parameters:
https://your-auth-endpoint.example.com/authorize?client_id={clientId}&response_type=code&request_uri={request_uri} Where request_uri is the value returned from the /par response. Your Authorization Endpoint is the URL you registered during environment-specific configuration.
Your Authorization Endpoint MUST NOT reject the redirect based on the query parameters it receives. If a TPP appends additional parameters beyond client_id, response_type, and request_uri, simply ignore them — do not treat the redirect as malformed.
The authoritative authorization request is the signed Request JWT referenced by request_uri; the API Hub validates it when you call GET/auth in Step 5. Forward whatever you receive and let Headless Heimdall be the source of truth.
Step 5 — Call GET/auth
Upon receiving the end user redirect, your authorization server MUST immediately call GET/auth on the Headless Heimdall base URL, passing through all the query parameters received from the redirect.
const HH_BASE = process.env.HEADLESS_HEIMDALL_BASE_URL!
// e.g. 'https://hh.{lfiCode}.preprod.apihub.openfinance.ae'
// Pass through all query parameters received at your authorization endpoint
const queryString = new URLSearchParams({
client_id: req.query.client_id,
response_type: req.query.response_type,
request_uri: req.query.request_uri,
}).toString()
const authResponse = await fetch(`${HH_BASE}/auth?${queryString}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// mTLS with C3 transport certificate
// agent: new https.Agent({ cert: c3TransportCert, key: c3TransportKey }),
})
if (authResponse.status === 303) {
// Redirectable failure — redirect the end user to the URI in the Location header without modification
return res.redirect(authResponse.headers.get('location'))
}
if (authResponse.status === 400) {
// Non-redirectable failure — render an error page to the end user
return res.status(400).render('auth-error')
}
const authData = await authResponse.json()
// Extract the interactionId and consentId — store both for subsequent calls
const interactionId = authData.interaction.interactionId
const consentId = authData.interaction.consentIdsList[0]The 200 response contains:
| Field | Description |
|---|---|
interaction.interactionId | Unique identifier for this authorization interaction — required for doConfirm and doFail |
interaction.consentIdsList | Array of consent IDs associated with this interaction (currently one element) |
tpp | TPP details including clientId, tppName, orgId, and directory record |
GET/auth can return three outcomes:
200— Success. Continue with the authorization journey.303— Redirectable failure. The OIDC client was valid but the authorization request parameters failed validation. You MUST redirect the end user to the URI in theLocationheader without modification.400— Non-redirectable failure. The OIDC client could not be verified. You MUST render an error page and MUST NOT redirect back to the TPP.
See the GET/auth API Reference for the full response schema.
Step 6 — Retrieve the consent details
Using the consentId from the GET/auth response, call GET/consents/{consentId} on the Consent Manager to retrieve the full consent object.
const CM_BASE = process.env.CONSENT_MANAGER_BASE_URL!
// e.g. 'https://cm.{lfiCode}.preprod.apihub.openfinance.ae'
const consentResponse = await fetch(`${CM_BASE}/consents/${consentId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
// mTLS with C3 transport certificate
// agent: new https.Agent({ cert: c3TransportCert, key: c3TransportKey }),
})
const consent = await consentResponse.json()
// The consent object contains the full consent details:
// - consent.data.consentBody.Data.Permissions (for data sharing)
// - consent.data.consentBody.Data.ExpirationDateTime
// - consent.data.consentBody.Data.PersonalIdentifiableInformation (if encrypted PII is present)Some consent types include encrypted personally identifiable information (PII) — for example, debtor or creditor details on payment consents. If the consent contains encrypted PII fields, your LFI MUST decrypt the data and validate that the decrypted values align exactly with the Open Finance API specification. See Personal Identifiable Information for decryption and validation details.
See the GET/consents/{consentId} API Reference for the full response schema.
Step 7 — authenticate the end user
Your LFI MUST authenticate the end user using your standard authentication mechanisms. The authentication MUST satisfy the requirements defined in the Authentication Requirements.
psuIdentifiers and accountIds MUST be opaque
The values the LFI patches onto the consent — psuIdentifiers and accountIds — are stored centrally in the API Hub. They MUST be opaque, non-sensitive, LFI-defined references.
The LFI MUST NOT use Emirates ID, passport number, name, email, phone number, IBAN, account number, card number, CIF, or any other value that on its own identifies a natural person or a real-world account.
See Consent Identifiers for the full rationale.
psuIdentifiers.userId
| Rule | Requirement |
|---|---|
| Type | String (required field on psuIdentifiers) |
| Pattern | LFI-defined opaque string. UUID (v4) recommended |
| Uniqueness | MUST uniquely identify a single end user within the LFI |
| Stability | MUST be the same value for the same end user across every consent they authorise — used by GET/psu/{userId}/consents |
| Sensitive values | MUST NOT be an Emirates ID, email, phone, CIF, or any other PII |
Additional custom fields on psuIdentifiers are permitted but MUST follow the same non-sensitive rule.
accountIds[]
| Rule | Requirement |
|---|---|
| Type | Array of strings, minItems: 1 |
| Item format | String, 1–40 characters. UUID (v4) recommended |
| Value | MUST match the AccountId the LFI returns from its own /accounts APIs — the API Hub uses it to enrich downstream TPP requests |
| Immutability | Once issued, the AccountId for an account MUST NOT change |
| Uniqueness | MUST uniquely identify a single account within the LFI |
| Sensitive values | MUST NOT be an IBAN, account number, card number, or any externally-meaningful account identifier |
| Cardinality | Bank Service Initiation: exactly one (the debtor account). Bank Data Sharing: one or more selected accounts. Insurance Data Sharing: not patched directly — the Consent Manager mirrors insurancePolicyIds into accountIds via trigger |
