Service Initiation · PII

Personal Identifiable Information (PII) 3 min read

Every payment instruction carries sensitive data about who is paying and who is receiving the funds. This data — the creditor account details, optional debtor account, and risk indicators — is collectively referred to as Personal Identifiable Information (PII).

PII is encrypted and embedded at two points in the payment lifecycle:

StageEndpointPII form
Consent stagingPOST/parEmbedded in consent.PersonalIdentifiableInformation
Payment creationPOST/paymentsEmbedded in payment.PersonalIdentifiableInformation

The Risk structure is the same at both stages. DebtorAccount is only present at POST/par — by the time POST/payments is called, the debtor account has already been fixed through the consent authorisation flow. The creditor data also differs between stages — both in structure and cardinality. See Creditor for the full breakdown.

01 Why PII is encrypted

End-to-end encryption between TPP and LFI

Payment consents are stored centrally at Nebras, the UAE Open Finance Hub. Because Nebras acts as an intermediary between TPPs and LFIs, PII is encrypted end-to-end before it leaves the TPP — ensuring that Nebras, and any other party in transit, cannot read the sensitive payment details.

The encryption uses the destination LFI's public key (see Message Encryption for full cryptographic details). Only the LFI can decrypt the payload. Nebras passes the opaque JWE through without inspection — all PII validation is performed by the LFI after the consent is authorised.

02 Steps to encrypt PII

Sign, then encrypt — Nested JWT (JWS inside JWE)

The PersonalIdentifiableInformation field MUST be sent as a compact JWE — a signed-then-encrypted token (Nested JWT). The process is:

  • Build the PII JSON — construct the PII object for the stage you are at (POST/par or POST/payments). See The PII payload structure below.
  • Sign — sign the PII payload as a JWS using your TPP signing key. The JWS MUST include standard claims (iat, exp, jti, iss, sub, aud).
  • Fetch the LFI's encryption key — retrieve the LFI's JWKS and select the key where "use": "enc".
  • Encrypt — encrypt the signed JWS into a compact JWE using RSA-OAEP-256 / A256GCM.
  • Embed — place the resulting JWE string in the PersonalIdentifiableInformation field of your request.

Example

typescript
import { SignJWT, importJWK, CompactEncrypt } from 'jose'
import { v4 as uuidv4 } from 'uuid'

async function encryptPII(
  piiPayload: Record<string, unknown>,
  signingKey: KeyLike,
  signingKeyId: string,
  signingAlg: string,
  clientId: string,
  audience: string,
  jwksUri: string
): Promise<string> {
  const now = Math.floor(Date.now() / 1000)

  // 1. Sign the PII payload
  const jws = await new SignJWT(piiPayload)
    .setProtectedHeader({ alg: signingAlg, kid: signingKeyId })
    .setIssuedAt(now)
    .setExpirationTime(now + 300)
    .setJti(uuidv4())
    .setIssuer(clientId)
    .setSubject(clientId)
    .setAudience(audience)
    .sign(signingKey)

  // 2. Fetch the LFI's JWKS and find the encryption key
  const response = await fetch(jwksUri)
  const { keys } = await response.json()
  const encKeyJwk = keys.find((k: any) => k.use === 'enc')
  if (!encKeyJwk) throw new Error('No encryption key (use: enc) found in JWKS')

  // 3. Encrypt the signed JWS into a JWE
  const publicKey = await importJWK(encKeyJwk, 'RSA-OAEP-256')
  const jwe = await new CompactEncrypt(new TextEncoder().encode(jws))
    .setProtectedHeader({
      alg: 'RSA-OAEP-256',
      enc: 'A256GCM',
      kid: encKeyJwk.kid,
    })
    .encrypt(publicKey)

  return jwe // → place this string in PersonalIdentifiableInformation
}

For the full breakdown of JWKS discovery, key selection, and JWE structure, see Message Encryption.

Testing on the sandbox

The sandbox provides an O3 Utility endpoint that accepts your private key and JWKS URL and returns a ready-made encrypted PII token — useful for validating your payload structure before writing your own encryption code. See O3 Sandbox Utilities.

03 Structure of the PII object

oneOf — three variants, one form sent in requests

The PersonalIdentifiableInformation field is defined as a oneOf:

VariantFormPurpose
Domestic Payment PII Schema ObjectobjectUnencrypted reference form for domestic payments
International Payment PII Schema ObjectobjectUnencrypted reference form for international payments
Encrypted PII Object (AEJWEPaymentPII)string (compact JWE)The form that MUST be sent at both POST/par and POST/payments

The two object variants document the structure implementers MUST follow when constructing the PII payload before encryption. The encrypted form — AEJWEPaymentPII — is a compact JWE string wrapping a signed JWS containing the serialised PII JSON.

04 The PII payload structure

Different shape at consent staging vs. payment creation

The structure of the unencrypted PII differs between the two stages.

At POST/par (consent staging)

PII payload — POST /parjson
{
  "Initiation": {
    "DebtorAccount": { ... },       // optional — see Debtor Account
    "Creditor": [                   // array of creditor entries — see Creditor
      {
        "CreditorAgent": { ... },
        "Creditor": { "Name": "..." },
        "CreditorAccount": { ... },
        "ConfirmationOfPayeeResponse": "..."
      }
      // ... up to 10 entries; omit array entirely for open beneficiary
    ]
  },
  "Risk": { ... }                   // required — see Risk
}

At POST/payments (payment creation)

PII payload — POST /paymentsjson
{
  "Initiation": {
    "CreditorAgent": { ... },               // flat on Initiation — not inside an array
    "Creditor": { "Name": "..." },
    "CreditorAccount": { ... },
    "ConfirmationOfPayeeResponse": "..."
  },
  "Risk": { ... }
}

The key difference: at POST/par the creditor data is inside an Initiation.Creditor[] array (allowing 1–10 entries, or omitted for open beneficiary). At POST/payments the same fields sit directly on Initiation as a single creditor.

PropertyPOST /parPOST /payments
Initiation.DebtorAccountOptional objectNot present — debtor account is fixed by consent
Initiation.CreditorArray of creditor entry objects (0–10)Object — the party name/address ({ Name, PostalAddress })
Initiation.CreditorAccountNested inside each Creditor[] entryDirect field on Initiation
Initiation.CreditorAgentNested inside each Creditor[] entryDirect field on Initiation
Initiation.ConfirmationOfPayeeResponseNested inside each Creditor[] entryDirect field on Initiation
RiskRequired objectRequired object

See the sub-pages for full schema and rules:

  • Debtor Account — optional at POST/par only; not part of the POST/payments PII
  • Creditor — consent-time models (single/multiple/open), payment-time structure, and match requirements
  • Risk — debtor and creditor risk indicators
05 Decentralised validation

Each LFI validates PII independently after decryption

Because PII is encrypted using the LFI's public key, Nebras cannot decrypt or validate it. The LFI is solely responsible for decrypting and validating the PII — at consent time and at payment time.

Validation is therefore performed independently by each LFI rather than centrally. The standards place explicit validation requirements on every LFI — each LFI must validate the decrypted PII against the schema before accepting a consent or processing a payment.

TPPs must understand LFI validation

A consent that is accepted by one LFI may be rejected by another if the PII does not meet the required format. TPPs should ensure that the PII they construct is strictly valid according to the schema for the payment type being instructed.

See Creditor for the specific validation rules that LFIs apply to the Creditor array for domestic payments.