Insurance Data Sharing — API Guide 5 min read
Create an Insurance Data Sharing consent, redirect the user to authenticate at their LFI, exchange the authorization code for tokens, and call the policy APIs — an end-to-end walkthrough of the PSU-present insurance data-sharing flow.
What you need before creating a consent
Before creating an Insurance Data Sharing consent, ensure the following requirements are met:
- Registered Application — the application must be created within the Trust Framework and assigned the ISP role as defined in Roles.
- Valid Transport Certificate — an active transport certificate must be issued and registered in the Trust Framework to establish secure mTLS communication.
- Valid Signing Certificate — an active signing certificate must be issued and registered in the Trust Framework. This certificate is used to sign request objects and client assertions.
- Valid Encryption Certificate — required to decrypt the
PremiumJWE when the customer has grantedReadInsurancePremium. See Encrypted Premiums. - Registration with the relevant API Hub (Authorisation Server) — the application must be registered with the API Hub (Server) of the LFI for which you intend to create an Insurance Data Sharing consent.
- Understanding of the FAPI Security Profile and Tokens & Assertions — you should understand how request object signing, client authentication, and access token validation underpin secure API interactions.
- Understanding of Consents — you should understand how to create, retrieve, and manage consents, including consent states and lifecycle transitions.
End-to-end Insurance Data Sharing
Bind PKCE and authorization details into a signed JWT
With your authorization_details ready, generate a PKCE code pair then use the buildRequestJWT() helper from the FAPI page, passing openid insurance as the scope.
import crypto from 'node:crypto'
import { generateCodeVerifier, deriveCodeChallenge } from './pkce' // from FAPI page
import { buildRequestJWT } from './request-jwt' // from FAPI page
// 1. Generate PKCE pair — store codeVerifier in your session before redirecting
const codeVerifier = generateCodeVerifier()
const codeChallenge = deriveCodeChallenge(codeVerifier)
// 2. Define the authorization_details for this consent
const authorizationDetails = [
{
type: 'urn:openfinanceuae:insurance-consent:v2.1',
consent: {
ConsentId: crypto.randomUUID(),
ExpirationDateTime: new Date(Date.now() + 364 * 24 * 60 * 60 * 1000).toISOString(),
Permissions: [
{
InsuranceType: 'Motor',
Permissions: [
'ReadInsurancePolicies',
'ReadCustomerBasic',
'ReadInsurancePremium',
],
},
],
OpenFinanceBilling: {
UserType: 'Retail',
Purpose: 'QuoteComparison',
},
},
},
]
// 3. Build and sign the Request JWT
const requestJWT = await buildRequestJWT({
scope: 'openid insurance',
codeChallenge,
authorizationDetails,
})
Save codeVerifier in your server-side session or an httpOnly cookie. You will need it in Step 7 to exchange the authorization code for tokens.
See Preparing the Request JWT for the full JWT claim reference and PKCE helpers.
Prove the application's identity to the API Hub
Every call to the API Hub (Authorisation Server) requires a client assertion — a short-lived signed JWT that proves your application's identity in place of a client secret. Use the signJWT() helper from the FAPI Message Signing page:
import crypto from 'node:crypto'
import { signJWT } from './sign-jwt' // from FAPI Message Signing page
const CLIENT_ID = process.env.CLIENT_ID!
const ISSUER = process.env.AUTHORIZATION_SERVER_ISSUER! // from .well-known
async function buildClientAssertion(): Promise<string> {
return signJWT({
iss: CLIENT_ID,
sub: CLIENT_ID,
aud: ISSUER,
jti: crypto.randomUUID(),
})
}
See Tokens & Assertions for the full claims reference and Preparing Your Client Assertion for a step-by-step walkthrough.
Push the request to the API Hub
With your signed Request JWT and client assertion ready, POST both to the API Hub's /par endpoint. The connection must use your mTLS transport certificate.
Include x-fapi-interaction-id — a UUID v4 you generate per request. The API Hub echoes it in the response, enabling end-to-end traceability. See Request Headers for the full header reference.
import crypto from 'node:crypto'
// 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',
'x-fapi-interaction-id': crypto.randomUUID(),
},
body: new URLSearchParams({
request: requestJWT,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: await buildClientAssertion(),
}),
// Node.js: pass an https.Agent configured with your transport cert and key
// agent: new https.Agent({ cert: transportCert, key: transportKey }),
})
const { request_uri, expires_in } = await parResponse.json()
You must present your transport certificate on every connection to the API Hub and resource APIs. In Node.js, configure an https.Agent with your PEM certificate and private key. See Certificates for how to obtain and configure your transport certificate.
The /par response contains:
| Field | Description | Example |
|---|---|---|
request_uri | A single-use reference to your pushed authorization request | urn:ietf:params:oauth:request-uri:bwc4JDpSd7 |
expires_in | Seconds until the request_uri expires — redirect the user before this window closes | 90 |
Validate state and issuer on the redirect
After the user approves, the insurer redirects them back to your redirect_uri. The callback includes an authorization code, the state you sent in your Request JWT, and the iss (issuer) of the API Hub Authorisation Server:
https://yourapp.com/callback?code=fbe03604-baf2-4220-b7dd-05b14de19e5c&state=d2fe5e2c-77cd-4788-b0ef-7cf0fc8a3e54&iss=https://auth1.altareq1.sandbox.apihub.openfinance.ae Extract all three parameters and validate state and iss before proceeding:
const params = new URLSearchParams(window.location.search)
// or server-side: new URLSearchParams(req.url.split('?')[1])
const code = params.get('code')!
const state = params.get('state')!
const iss = params.get('iss')!
if (state !== storedState) {
throw new Error('State mismatch — possible CSRF attack. Abort the flow.')
}
if (iss !== ISSUER) {
throw new Error(`Unexpected issuer: ${iss}`)
}
See Handling Authorization Callbacks for a full guide on security best practices including issuer verification, replay prevention, and keeping callback logic minimal.
Retrieve the consented policies for a sector
/{type}-insurance-policies With a valid access token, retrieve all policies of a given sector the user consented to share. The endpoint is the same shape for every sector — substitute the sector slug (employment, health, home, life, motor, renters, travel) into the path. Include x-fapi-interaction-id on every request, and when the customer is present also send x-fapi-customer-ip-address, x-customer-user-agent, and x-fapi-auth-date. See Request Headers.
import crypto from 'node:crypto'
const LFI_API_BASE = process.env.LFI_API_BASE_URL! // resource server base URL from .well-known
// Substitute the sector the customer consented to — employment, health, home,
// life, motor, renters, or travel.
const policiesResponse = await fetch(
`${LFI_API_BASE}/open-finance/insurance/v2.1/motor-insurance-policies`,
{
headers: {
Authorization: `Bearer ${access_token}`,
'x-fapi-interaction-id': crypto.randomUUID(),
'x-fapi-auth-date': lastCustomerAuthDate, // RFC 7231 — last time user authenticated with TPP
'x-fapi-customer-ip-address': customerIpAddress, // customer's IP address
// 'x-customer-user-agent': req.headers['user-agent'],
},
// agent: new https.Agent({ cert: transportCert, key: transportKey }),
}
)
const { Data: { Policy: policies } } = await policiesResponse.json()
// Store the InsurancePolicyId(s) for sub-resource queries
const insurancePolicyId = policies[0].InsurancePolicyId
Insurance policy endpoints return the full set of consented policies for the named sector in a single response. There is no page query parameter, and Meta does not carry TotalPages or TotalRecords.
Repeat the call once per sector the consent grants — one for each InsuranceType in the Permissions array. See the OpenAPI reference for every sector under Insurance Data Sharing.
Fetch detailed information for a specific policy
/{type}-insurance-policies/{InsurancePolicyId} Use an InsurancePolicyId returned in Step 8 to fetch the detailed policy — cover, riders, claims, premium, and beneficiaries. Each field set requires the matching permission in the consented sector. Apply the same FAPI headers as Step 8.
const policyResponse = await fetch(
`${LFI_API_BASE}/open-finance/insurance/v2.1/motor-insurance-policies/${insurancePolicyId}`,
{
headers: {
Authorization: `Bearer ${access_token}`,
'x-fapi-interaction-id': crypto.randomUUID(),
'x-fapi-auth-date': lastCustomerAuthDate,
'x-fapi-customer-ip-address': customerIpAddress,
// 'x-customer-user-agent': req.headers['user-agent'],
},
// agent: new https.Agent({ cert: transportCert, key: transportKey }),
}
)
const { Data: { Policy: policy } } = await policyResponse.json()
// Premium is anyOf { object | string }. A string is a compact JWE.
// See the Encrypted Premiums page for how to handle it client-side.
const premium = policy.Premium
Permissions and the data fields they unlock:
| Permission | Unlocks |
|---|---|
ReadInsurancePolicies | Core policy attributes — ID, number, status, dates, sums insured, coverage. |
ReadCustomerBasic | Basic policy-holder identity and contact details. |
ReadCustomerDetail | Full customer details, including additional verification fields. |
ReadCustomerPaymentDetails | Customer payment methods recorded on the policy. |
ReadInsuranceProduct | Underwritten product detail — cover type, features, terms, add-ons. |
ReadCustomerClaims | Claims raised against the policy — status, dates, amounts. |
ReadInsurancePremium | The Premium field. Returned as a JWE that the customer device decrypts — see Encrypted Premiums. |
When the consent grants ReadInsurancePremium, the Premium field on the policy is returned as a compact JWE string. The TPP server MUST NOT decrypt it — forward it to the customer’s device, where it is unwrapped. Full walkthrough: Encrypted Premiums.
Keep the session alive without re-authorisation
Access tokens expire after 10 minutes. Track the expires_in value returned by /token and refresh proactively rather than waiting for a 401 Unauthorized. Each refresh requires a fresh client assertion.
// Reuse the TOKEN_ENDPOINT discovered in Step 7 (discoveryDoc.token_endpoint).
async function refreshAccessToken(refreshToken: string) {
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
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: newRefreshToken, expires_in } = await response.json()
// Always replace both tokens — some servers rotate the refresh token on each use
return { access_token, refresh_token: newRefreshToken, expires_in }
}
Always replace both access_token and refresh_token from the response. If the API Hub rotates refresh tokens, continuing to use the old one will return 400 invalid_grant.
The refresh token remains valid until the consent's ExpirationDateTime. Once expired, the user must go through the full authorization flow again — send a new /par request with a new ConsentId.
