TPP · Insurance · Data Sharing

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.

01 Prerequisites

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 Premium JWE when the customer has granted ReadInsurancePremium. 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.
02 API Sequence Flow

End-to-end Insurance Data Sharing

Sequence diagramInsurance Data Sharing API FlowClick to expand
03 POST /par · Step 1 — Constructing Authorization Details

Build the consent payload

POST/par

To send a /par request, first we need to generate the request JWT. We do this by first constructing authorization_details of type (urn:openfinanceuae:insurance-consent:v2.1).

authorization_details

FieldTypeDescriptionExample
type*enumMust be urn:openfinanceuae:insurance-consent:v2.1urn:openfinanceuae:insurance-consent:v2.1
consent*objectProperties of the consent agreed by the User with the TPP. Described below.Described below
subscriptionobjectOptional subscription to Event Notifications, to be sent to the TPP Webhook Url. Described below.Described below

consent (Required) | authorization_details.consent

FieldTypeDescriptionExample
ConsentId*string (uuid)Unique ID assigned by the TPP (1–128 chars)123e4567-e89b-12d3-a456-426614174001
BaseConsentIdstring (uuid)Used when renewing or modifying an existing consent123e4567-e89b-12d3-a456-426614174000
Permissions*array<object>One entry per insurance sector being consented — described belowDescribed below
ExpirationDateTime*date-timeExpiry date/time (ISO 8601 with timezone, max 1 year)2025-11-03T15:46:00+00:00
OpenFinanceBilling*objectBilling parameters specified by the TPP. Described below.Described below
OnBehalfOfobjectProvided when TPP is acting for another regulated entity. Described below.Described below

Permissions entry | authorization_details.consent.Permissions[*]

FieldTypeAllowed Values
InsuranceType*enumEmployment, Health, Home, Life, Motor, Renters, Travel
Permissions*array<enum>ReadInsurancePolicies, ReadCustomerBasic, ReadCustomerDetail, ReadCustomerPaymentDetails, ReadInsuranceProduct, ReadCustomerClaims, ReadInsurancePremium

OpenFinanceBilling (Required) | authorization_details.consent.OpenFinanceBilling

FieldTypeAllowed ValuesExample
UserType*enumRetail, SME, CorporateRetail
Purpose*enumAccountAggregation, RiskAssessment, TaxFiling, Onboarding, Verification, QuoteComparison, BudgetingAnalysis, FinancialAdvice, AuditReconciliationQuoteComparison

OnBehalfOf (Optional) | authorization_details.consent.OnBehalfOf

FieldTypeDescriptionExample
TradingNamestringTrading name if acting on behalf of another entityAcme Ltd
LegalNamestringLegal name of represented entityAcme Legal Name
IdentifierTypeenumOnly Other currently supportedOther
IdentifierstringIdentifier value9876543210

subscription (Optional) | authorization_details.subscription

FieldTypeDescriptionExample
Webhook*objectDescribed below.Described below

Webhook (Required) | authorization_details.subscription.Webhook

FieldTypeDescriptionExample
Url*stringHTTPS callback URLhttps://tpp.example.com/webhook
IsActive*booleanWhether webhook is activetrue

Example request

See an example of a valid authorization_details for urn:openfinanceuae:insurance-consent:v2.1:

authorization_detailsjson
"authorization_details": [
  {
    "type": "urn:openfinanceuae:insurance-consent:v2.1",
    "consent": {
      "ConsentId": "{{unique-guid}}", // Unique ID assigned by the TPP (uuid format)
      "ExpirationDateTime": "2026-05-03T15:46:00+00:00", // Max 1 year from today (ISO 8601 format with timezone)

      // Permissions are grouped per insurance sector. Repeat one block per
      // sector the customer is sharing.
      "Permissions": [
        {
          "InsuranceType": "Motor",
          "Permissions": [
            "ReadInsurancePolicies",
            "ReadCustomerBasic",
            "ReadCustomerDetail",
            "ReadCustomerPaymentDetails",
            "ReadInsuranceProduct",
            "ReadCustomerClaims",
            "ReadInsurancePremium"
          ]
        },
        {
          "InsuranceType": "Home",
          "Permissions": [
            "ReadInsurancePolicies",
            "ReadInsurancePremium"
          ]
        }
      ],

      "OpenFinanceBilling": {
        "UserType": "Retail",                 // Options: Retail, SME, Corporate
        "Purpose": "QuoteComparison"           // e.g. AccountAggregation, QuoteComparison, RiskAssessment
      }

      // Optional: to link to other ConsentId e.g. when renewing long-lived consents
      // "BaseConsentId": "existing-consent-id",

      // Optional: for consent on behalf of another legal entity
      // "OnBehalfOf": {
      //   "TradingName": "Ozone",
      //   "LegalName": "Ozone-CBUAE",
      //   "IdentifierType": "Other", // Only 'Other' allowed for now
      //   "Identifier": "1234567890"
      // }
    },

    // Optional: to receive webhook notifications from LFI
    // "subscription": {
    //   "Webhook": {
    //     "Url": "https://tpp.example.com/webhook",
    //     "IsActive": true
    //   }
    // }
  }
]
04 POST /par · Step 2 — Constructing the Request JWT

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.

typescript
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,
})
Store the code_verifier

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.

05 POST /par · Step 3 — Creating a Client Assertion

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:

typescript
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.

06 POST /par · Step 4 — Sending the /par Request

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.

typescript
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()
mTLS transport certificate

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:

FieldDescriptionExample
request_uriA single-use reference to your pushed authorization requesturn:ietf:params:oauth:request-uri:bwc4JDpSd7
expires_inSeconds until the request_uri expires — redirect the user before this window closes90
07 Redirecting the User · Step 5 — Building the Authorization URL

Send the user to the LFI to authenticate

Use the request_uri returned by /par to build the redirect URL. The authorization_endpoint is found in the LFI's .well-known/openid-configuration — not constructed from the issuer URL directly. All authorization parameters are already inside the signed Request JWT, so the only query parameters needed are client_id, response_type, scope, and request_uri.

typescript
// authorization_endpoint is discovered from the LFI's .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 response_type = 'code'

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

// Redirect the user
window.location.href = authCodeUrl
// or server-side:
// res.redirect(authCodeUrl)
User Experience

See User Experience for screen mockups of the Consent and Authorization pages the user sees at their insurer, including the per-sector permission previews.

After redirecting, the user will:

  • Authenticate with their insurer.
  • Review the consent — sectors, policies, permissions, and expiry — on the insurer's authorization screen.
  • Approve or decline.
08 Handling the Callback · Step 6 — Extracting the Authorization Code

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:

callback URL
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:

typescript
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.

09 Exchanging the Code for Tokens · Step 7 — POST /token (Authorization Code)

Swap the auth code for an access and refresh token

POST/token

Exchange the authorization code for an access token and refresh token. Include the code_verifier from Step 2 — the API Hub will verify it against the code_challenge in your Request JWT before issuing tokens.

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,            // from Step 2
    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,   // 600 — access token lasts 10 minutes
  token_type,   // 'Bearer'
} = await tokenResponse.json()

Store both tokens securely. The access token expires in 10 minutes; the refresh token remains valid for the lifetime of the consent.

Token storage

Never store tokens in localStorage. Use httpOnly cookies or a server-side session store. See Tokens & Assertions for the full token lifecycle and expiry guidance.

10 Calling the Policy APIs · Step 8 — GET /{type}-insurance-policies

Retrieve the consented policies for a sector

GET/{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.

typescript
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
No pagination

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.

11 Calling the Policy APIs · Step 9 — GET /{type}-insurance-policies/{InsurancePolicyId}

Fetch detailed information for a specific policy

GET/{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.

typescript
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:

PermissionUnlocks
ReadInsurancePoliciesCore policy attributes — ID, number, status, dates, sums insured, coverage.
ReadCustomerBasicBasic policy-holder identity and contact details.
ReadCustomerDetailFull customer details, including additional verification fields.
ReadCustomerPaymentDetailsCustomer payment methods recorded on the policy.
ReadInsuranceProductUnderwritten product detail — cover type, features, terms, add-ons.
ReadCustomerClaimsClaims raised against the policy — status, dates, amounts.
ReadInsurancePremiumThe Premium field. Returned as a JWE that the customer device decrypts — see Encrypted Premiums.
Premium is encrypted in transit

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.

12 Refresh Token Flow · Step 10 — Refreshing the Access Token

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.

typescript
// 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 }
}
Refresh token rotation

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.