Fixed On Demand — API Guide 15 min read
Fixed On Demand lets a TPP initiate multiple domestic payments at a fixed amount from a customer's account at your LFI via the API Hub. The customer authorises the consent once — approving a specific per-payment amount and periodic limits — and the TPP can then submit individual payments on-demand without re-authorisation. Payments run on AANI as the primary rail with UAEFTS as the fallback. This guide covers the Ozone Connect endpoints your LFI MUST implement so the Hub can serve every payment under the consent from creation through to execution and status retrieval.
The behavioural rules for each endpoint — validation conditions, error mappings, post-execution lifecycle — are in the Fixed On Demand Requirements. This guide covers the request and response shape of each endpoint, with code walkthroughs for the parts that need them: decrypting the PII, validating the creditor, matching the payment-time PII against the consent, and applying the duplicate-in-flight check that is specific to on-demand consent types.
What you need before implementing Fixed On Demand
Before implementing Fixed On Demand, ensure the following are in place:
- API Hub onboarded — Your API Hub instance is provisioned and your environment-specific configuration is complete.
- Enc1 key pair generated and registered — The TPP encrypts PII to your LFI's Enc1 public key. Your LFI MUST hold the corresponding private key and be able to look it up by
kid. - Consent Journey implemented — The Consent Journey API Guide MUST be implemented first. A payment cannot be initiated without an authorized consent.
- Ozone Connect connectivity verified — Bidirectional mTLS connectivity is confirmed between the API Hub and your Ozone Connect base URL. See Connectivity & Certificates.
- Fixed On Demand advertised —
ApiMetadata.FixedOnDemand.Supportedis set totrueon your authorisation server entry in the Trust Framework.
End-to-end Fixed On Demand
Validate the consent before it is created
When a TPP creates a payment consent, the API Hub calls your POST/consent/action/validate endpoint before the consent is created. Your LFI MUST validate the consent and respond with data.status: "valid" or data.status: "invalid". An invalid response prevents the consent being created and the TPP receives an error.
The full set of validation rules is enumerated in Fixed On Demand Requirements — Consent Validation. The two parts that need a code walkthrough are decrypting the PII and validating the creditor; both are covered below.
Decrypting and validating the PII
The consent.PersonalIdentifiableInformation field arrives as a JWE compact string encrypted by the TPP to your LFI's Enc1 public key. Decryption, schema validation, and field-level checks are entirely the LFI's responsibility.
- Read the
kidfrom the JWE protected header and look up the matching Enc1 private key. - Decrypt the JWE → recover the inner JWS.
- Decode the JWS payload (signature verification is optional).
- Validate the decoded payload against the consent-time PII schema —
AEBankServiceInitiationRichAuthorizationRequests.AEDomesticPaymentPII.
import { compactDecrypt, importPKCS8, decodeJwt } from 'jose'
async function decryptAndValidateConsentPII(
piiJwe: string
): Promise<Record<string, unknown>> {
// 1. kid → private key
const [headerB64] = piiJwe.split('.')
const { kid } = JSON.parse(Buffer.from(headerB64, 'base64url').toString())
const privateKey = await importPKCS8(
enc1KeyStore.getPrivateKeyPem(kid),
'RSA-OAEP-256'
)
// 2. JWE → JWS
const { plaintext } = await compactDecrypt(piiJwe, privateKey)
// 3. JWS → payload (signature verification is optional)
const pii = decodeJwt(new TextDecoder().decode(plaintext))
// 4. Schema validation against AEDomesticPaymentPII
validateConsentPiiSchema(pii)
return pii
}
For the per-step deep dive see How to Decrypt PII.
The decrypted consent-time PII for a Fixed On Demand consent looks like:
{
"Initiation": {
"Creditor": [
{
"CreditorAccount": {
"SchemeName": "IBAN",
"Identification": "AE220331234567890876543",
"Name": { "en": "Fatima Al Zaabi" }
},
"CreditorAgent": {
"SchemeName": "BICFI",
"Identification": "BARBAEAAXXX"
}
}
],
"DebtorAccount": {
"SchemeName": "IBAN",
"Identification": "AE070331234567890123456"
}
},
"Risk": {
"PaymentContextCode": "EcommerceGoods",
"MerchantCategoryCode": "4900"
},
"iat": 1745020800,
"exp": 1745021100,
"iss": "tpp-client-id"
}
If decryption fails, schema validation fails, or any required field is missing, respond with invalid per Rejecting an invalid consent.
Validating the Creditor
For Fixed On Demand, Initiation.Creditor MUST be an array of exactly one entry — the single creditor that every payment under this consent will pay. The full Creditor rules are in Creditor.
- Cardinality — exactly one entry.
- Mandatory fields —
CreditorAccount.SchemeName == "IBAN", valid UAE IBAN, at least one ofName.enorName.ar. - BIC consistency — derive the BIC from the IBAN; if
CreditorAgent.Identificationwas supplied it MUST match. - Domestic rail reachability — the receiving bank is reachable on AANI or UAEFTS.
function isValidUaeIban(iban: string): boolean {
if (!/^AE\d{21}$/.test(iban)) return false
const rearranged = iban.slice(4) + iban.slice(0, 4)
const numeric = rearranged
.split('')
.map(c => (/[A-Z]/.test(c) ? (c.charCodeAt(0) - 55).toString() : c))
.join('')
let remainder = 0
for (const digit of numeric) {
remainder = (remainder * 10 + Number(digit)) % 97
}
return remainder === 1
}
function deriveBicFromIban(iban: string): string {
// UAE IBAN positions 5-7 (0-indexed 4-6) carry the bank code.
return bicDirectory.lookupByUaeBankCode(iban.slice(4, 7))
}
interface InvalidResponse { status: 'invalid'; code: string; description: string }
async function validateCreditor(
creditor: Array<{
CreditorAccount: {
SchemeName: string
Identification: string
Name?: { en?: string; ar?: string }
}
CreditorAgent?: { SchemeName: string; Identification: string }
}>
): Promise<InvalidResponse | null> {
// 1. Cardinality
if (!Array.isArray(creditor) || creditor.length !== 1) {
return invalid('InvalidCreditor',
'Fixed On Demand requires exactly one creditor entry.')
}
const c = creditor[0]
// 2. Mandatory fields
if (c.CreditorAccount.SchemeName !== 'IBAN') {
return invalid('InvalidCreditor',
'CreditorAccount.SchemeName MUST be "IBAN" for domestic payments.')
}
if (!isValidUaeIban(c.CreditorAccount.Identification)) {
return invalid('InvalidCreditor',
'CreditorAccount.Identification is not a valid UAE IBAN.')
}
if (!c.CreditorAccount.Name?.en && !c.CreditorAccount.Name?.ar) {
return invalid('InvalidCreditor',
'CreditorAccount.Name MUST include at least one of `en` or `ar`.')
}
// 3. BIC consistency
const derivedBic = deriveBicFromIban(c.CreditorAccount.Identification)
if (c.CreditorAgent?.Identification && c.CreditorAgent.Identification !== derivedBic) {
return invalid('InvalidCreditor',
'CreditorAgent.Identification does not match the BIC derived from the IBAN.')
}
// 4. Domestic rail reachability + receiving account state
const r = await lookupRailReachability(derivedBic)
if (!r.reachableOnAani && !r.reachableOnUaefts) {
return invalid('UnreachableCreditorAccount',
'Creditor bank is not reachable on AANI or UAEFTS.')
}
if (r.canDetermineAccountState && !r.accountCanReceive) {
return invalid('UnreachableCreditorAccount',
'Creditor account cannot currently receive payments.')
}
return null
}
const invalid = (code: string, description: string): InvalidResponse =>
({ status: 'invalid', code, description })
Validating the DebtorAccount
If the TPP supplied Initiation.DebtorAccount in the consent PII, your LFI MUST also validate it before approving the consent: SchemeName is IBAN, the IBAN corresponds to an account held at this LFI, and the account is in a state that permits payment initiation. Customer ownership is checked later during the authorisation journey.
The full check list and the invalid response shape (with code: InvalidDebtorAccount) are in Debtor Account.
Returning the validate response
{
"data": {
"status": "invalid",
"code": "InvalidCreditor",
"description": "CreditorAccount.Identification is not a valid UAE IBAN."
},
"meta": {}
}
See Consent Events & Actions — API Guide for the full validate request and response schema.
Authenticate the customer and pin the debtor account
After consent creation passes validation, the TPP redirects the customer to your LFI's authorization endpoint and your LFI runs the standard consent journey. Full details are in the Consent Journey API Guide.
| 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, customer identifiers, and the selected debtor account |
POST/auth/{interactionId}/doConfirm | LFI → API Hub | Complete the interaction successfully |
POST/auth/{interactionId}/doFail | LFI → API Hub | Complete the interaction with a failure |
After the consent is authorized
When the TPP submits a payment instruction to the API Hub's resource server, the API Hub validates the access token, checks the consent is Authorised, checks the amount/currency match the consent control parameters (for Fixed On Demand this means the instructed amount equals PeriodicSchedule.Amount and the per-period count has not been exceeded), and validates the request against the OpenAPI schema — all before forwarding to your Ozone Connect POST/payments endpoint covered in the next section.
The Hub does not decrypt or inspect the PII. Re-validating the PII and matching it against the consent at payment time is the LFI's responsibility, covered below.
Decrypt, validate, persist, return 201
POST/payments is the central endpoint your LFI implements for payment execution. The API Hub calls it each time the TPP submits a payment under an authorized Fixed On Demand consent. Your LFI MUST decrypt and validate the PII, match it against the consent, run the synchronous validations listed in POST /payments Requirements — including the duplicate-in-flight check that is specific to on-demand consent types — create the payment record, and return 201 with the assigned PaymentId.
Screening, rail submission, and status propagation happen after the 201 response — see After returning 201.
Common request headers
| Header | Required | Description |
|---|---|---|
o3-provider-id | Yes | Identifier for your LFI registered in the Hub |
o3-aspsp-id | Yes (deprecated) | Deprecated alias for o3-provider-id |
o3-caller-org-id | Yes | Organisation ID of the TPP making the underlying request |
o3-caller-client-id | Yes | OIDC client ID of the TPP application |
o3-caller-software-statement-id | Yes | Software statement ID of the TPP application |
o3-api-uri | Yes | The parameterised URL of the API being called by the TPP |
o3-api-operation | Yes | The HTTP method (POST) |
o3-ozone-interaction-id | Yes | Hub-generated interaction ID |
o3-consent-id | Yes | The consentId for which this call is being made |
o3-psu-identifier | Yes | Base64-encoded customer identifier JSON object |
o3-caller-interaction-id | No | Interaction ID passed in by the TPP, if present |
The headers the TPP set on its original call to the API Hub — including x-fapi-interaction-id, x-fapi-auth-date, x-customer-user-agent, and x-idempotency-key — are forwarded to your LFI inside the request body as requestHeaders, not on the HTTP headers of the API Hub → LFI call. Unlike Single Instant Payment, Fixed On Demand does not require x-fapi-customer-ip-address — the customer may not be present at the time the TPP submits an on-demand payment.
Request body
Content-Type: application/json
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
requestUrl | string | No | The TPP-facing resource URL the TPP called |
paymentType | string | Yes | MUST be cbuae-payment for domestic Fixed On Demand |
request.Data | object | Yes | The payment payload |
requestHeaders | object | Yes | The complete set of HTTP headers the TPP sent to the API Hub |
tpp | object | Yes | The TPP's directory record. See The tpp and decodedSsa Context Blocks |
supplementaryInformation | object | No | Free-form pass-through context |
request.Data
| Field | Type | Required | Description |
|---|---|---|---|
ConsentId | string | Yes | MUST equal the o3-consent-id header |
Instruction.Amount.Amount | string | Yes | For Fixed On Demand this MUST equal the consent's PeriodicSchedule.Amount — the Hub enforces this before forwarding |
Instruction.Amount.Currency | string | Yes | MUST be AED for domestic payments |
PaymentPurposeCode | string | Yes | 3-letter ISO 20022 purpose code, e.g. SUBS |
PersonalIdentifiableInformation | string | No | Encrypted PII payload as a JWE compact string |
DebtorReference | string | No | Reference shown on the debtor's statement |
CreditorReference | string | No | Reference shown on the creditor's statement |
OpenFinanceBilling.Type | string | Yes | Billing type |
OpenFinanceBilling.MerchantId | string | No | Optional merchant identifier |
Request example
{
"requestUrl": "https://rs1.[LFICode].apihub.openfinance.ae/open-finance/payment/v2.1/payments",
"paymentType": "cbuae-payment",
"request": {
"Data": {
"ConsentId": "cac2381a-7111-4c5f-bc2f-4319a93da7c5",
"Instruction": {
"Amount": { "Amount": "49.00", "Currency": "AED" }
},
"PaymentPurposeCode": "SUBS",
"PersonalIdentifiableInformation": "eyJhbGciOiJSU0EtT0FFUC0yNTYi...",
"OpenFinanceBilling": { "Type": "Collection" }
}
},
"requestHeaders": {
"o3-provider-id": "lfi-123",
"o3-caller-org-id": "tpp-456",
"o3-caller-client-id": "client-789",
"o3-api-uri": "/open-finance/payment/v2.1/payments",
"o3-api-operation": "POST",
"o3-ozone-interaction-id": "ozone-xyz",
"o3-consent-id": "cac2381a-7111-4c5f-bc2f-4319a93da7c5",
"o3-psu-identifier": "eyJwczoi...",
"x-fapi-interaction-id": "0f4d3a16-9e27-4f8d-9a5a-3a2f7e9c1b22",
"x-fapi-auth-date": "Tue, 18 Apr 2026 10:14:22 GMT",
"x-idempotency-key": "idem-2026-04-18-001"
},
"tpp": {
"clientId": "1675793e-d6e3-4954-96c8-acb9aaa83c53",
"orgId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789",
"tppId": "fdd6e0ac-ba7a-4bc4-a986-c45c5daaaf00",
"tppName": "Example TPP",
"softwareStatementId": "XvAjPeeYZAdWwrFF..",
"decodedSsa": {
"client_id": "1675793e-d6e3-4954-96c8-acb9aaa83c53",
"client_name": "Example TPP",
"roles": ["BSIP"]
}
},
"supplementaryInformation": {}
}
Reading the PII at payment time
The payment-time PII follows a different shape from the consent-time PII:
Initiation.Creditoris a single object, not an array.DebtorAccountis absent.- The schema is
AEBankServiceInitiation.AEDomesticPaymentPIIProperties.
async function decryptAndValidatePaymentPII(piiJwe: string) {
const pii = await decryptPii(piiJwe) // shared decrypt helper
validatePaymentPiiSchema(pii) // AEDomesticPaymentPIIProperties
return pii
}
If decryption fails, reject with 400 JWE.DecryptionError. If schema validation fails, reject with 400 Body.InvalidFormat.
Matching the PII against the consent
Per POST /payments Requirements rule 2, the submitted creditor MUST exactly match the single creditor that was on the consent at consent time. Mismatch → 400 Consent.FailsControlParameters.
Pattern A — LFI persisted the decrypted creditor at consent time
async function matchPaymentCreditorToConsent(
consentId: string,
paymentPii: { Initiation: { Creditor: ConsentTimeCreditor } }
): Promise<void> {
const consentCreditor = await consentStore.getCreditor(consentId)
if (!consentCreditor) {
throw httpError(400, 'Consent.Invalid', `No stored consent for ${consentId}.`)
}
if (!isExactMatch(consentCreditor, paymentPii.Initiation.Creditor)) {
throw httpError(400, 'Consent.FailsControlParameters',
'Payment creditor does not match the creditor authorised on the consent.')
}
}
function isExactMatch(consentCreditor: any, paymentCreditor: any): boolean {
return (
consentCreditor.CreditorAccount.SchemeName === paymentCreditor.CreditorAccount.SchemeName &&
consentCreditor.CreditorAccount.Identification === paymentCreditor.CreditorAccount.Identification &&
consentCreditor.CreditorAccount.Name?.en === paymentCreditor.CreditorAccount.Name?.en &&
consentCreditor.CreditorAccount.Name?.ar === paymentCreditor.CreditorAccount.Name?.ar &&
consentCreditor.CreditorAgent?.SchemeName === paymentCreditor.CreditorAgent?.SchemeName &&
consentCreditor.CreditorAgent?.Identification === paymentCreditor.CreditorAgent?.Identification
)
}
Pattern B — LFI did not persist the consent-time PII
If your LFI did not persist the decrypted PII at consent time, fetch the consent from the API Hub via GET/consents/{consentId}, decrypt the consent's PersonalIdentifiableInformation field, and run the same isExactMatch comparison against Initiation.Creditor[0].
Duplicate-in-flight check
Per POST /payments Requirements rule 6, Fixed On Demand payments are subject to a duplicate-in-flight check that on-demand consent types carry but one-off and scheduled payments do not. Before the payment record is created, your LFI MUST check whether another payment under the same consent, with the same creditor and the same instructed amount, is currently in Pending status. If so, reject the new request with 409 Payment.DuplicateInFlight.
This is distinct from x-idempotency-key handling: the idempotency key catches TPP retries of the same HTTP request, while this rule catches genuinely separate payment intents that happen to duplicate a still-in-flight one.
async function rejectIfDuplicateInFlight(
consentId: string,
paymentCreditor: any,
amount: { Amount: string; Currency: string }
): Promise<void> {
const inFlight = await paymentStore.findInFlight({
consentId,
status: 'Pending',
creditorIban: paymentCreditor.CreditorAccount.Identification,
amount: amount.Amount,
currency: amount.Currency,
})
if (inFlight) {
throw httpError(409, 'Payment.DuplicateInFlight',
'A payment with the same creditor and amount is already in flight under this consent.')
}
}
Once the prior payment has left Pending (reached AcceptedSettlementCompleted, AcceptedCreditSettlementCompleted, AcceptedWithoutPosting, or Rejected), a subsequent identical payment is permitted.
Response
Content-Type: application/json
Return 201 on successful payment record creation.
| Field | Type | Required | Description |
|---|---|---|---|
data.id | string | Yes | The LFI-assigned PaymentId |
data.consentId | string | No | The consent under which the payment was created |
data.paymentTransactionId | string | No | End-to-end identifier from the rail |
data.status | string | Yes | One of Pending, AcceptedSettlementCompleted, AcceptedCreditSettlementCompleted, AcceptedWithoutPosting, Rejected, Received |
data.statusUpdateDateTime | string | Yes | ISO 8601 timestamp |
data.creationDateTime | string | Yes | ISO 8601 timestamp |
data.instruction.Amount | object | No | The payment amount and currency |
data.paymentPurposeCode | string | Yes | The purpose code from the request |
data.openFinanceBilling.Type | string | Yes | The billing type from the request |
Example — successful initiation
{
"data": {
"id": "5ff155ea-853f-480c-ac74-1eaed7c1201f",
"consentId": "cac2381a-7111-4c5f-bc2f-4319a93da7c5",
"status": "Pending",
"statusUpdateDateTime": "2026-04-18T10:14:23Z",
"creationDateTime": "2026-04-18T10:14:23Z",
"instruction": {
"Amount": { "amount": "49.00", "currency": "AED" }
},
"paymentPurposeCode": "SUBS",
"openFinanceBilling": { "Type": "Collection" }
},
"meta": {}
}
Error responses
Only return an error when the request is invalid. The errorCode values are drawn from the POST /payments OpenAPI schema Error400 / Error403 / Error409 enums.
400 — Bad request
errorCode | When to use |
|---|---|
Body.InvalidFormat | Body fails schema validation |
Resource.InvalidFormat | A field is present but not syntactically valid |
Consent.Invalid | The consent referenced is unknown to the LFI or has been revoked |
Consent.FailsControlParameters | The payment-time creditor does not match the consent-time creditor |
Consent.BusinessRuleViolation | An LFI-side business rule blocks the payment |
JWE.DecryptionError | PII JWE cannot be decrypted with any registered Enc1 key |
JWE.InvalidHeader | PII JWE header is malformed |
JWS.InvalidSignature / JWS.Malformed / JWS.InvalidClaim / JWS.InvalidHeader | PII inner JWS fails verification |
GenericRecoverableError / GenericError | Other validation errors |
403 — Forbidden
errorCode | When to use |
|---|---|
AccessToken.InvalidScope | The Hub's token does not include the required scope |
Consent.AccountTemporarilyBlocked | Debtor account is Inactive, Dormant, or Suspended |
Consent.PermanentAccountAccessFailure | Debtor account is Closed, Deceased, or Unclaimed |
Consent.TransientAccountAccessFailure | Debtor account temporarily inaccessible |
409 — Conflict
errorCode | When to use |
|---|---|
Payment.DuplicateInFlight | Another payment with the same creditor and amount under the same consent is still Pending |
Example error response
{
"errorCode": "Payment.DuplicateInFlight",
"errorMessage": "A payment with the same creditor and amount is already in flight under this consent."
}
After returning 201
The 201 means the payment record exists at your LFI; it does not mean the payment has settled. The lifecycle from here is asynchronous and is the LFI's responsibility:
| Stage | LFI behaviour | Reference |
|---|---|---|
| Screening | Run fraud / sanctions / AML controls. SHOULD complete within 3 seconds. On failure, PATCH the payment to Rejected with an LFI.-namespaced reject reason | Screening Checks |
| Rail submission | Submit to AANI as primary. Fall back to UAEFTS automatically if AANI is unavailable | Rail Submission |
| Status propagation | On every rail status change, call PATCH/payment-log/{id} | Payment Status |
| Rail rejection | If the rail rejects the payment, PATCH paymentResponse.status: Rejected with RejectReasonCode namespaced as AANI. or FTS. | Rail Submission |
| Status retrieval | Continue serving GET/payments/{paymentId} for at least 1 year | GET /payments/{paymentId} rules below |
Update payment status on the API Hub
PATCH/payment-log/:id updates the payment status on the API Hub. The Hub uses the update to send asynchronous notifications to TPPs and to maintain accurate state for billing and limit calculations.
Request body
Uses literal flat-key JSON (the dots are part of the key, not nested objects):
| Field | Type | Required | Description |
|---|---|---|---|
paymentResponse.status | string | Yes | Pending, AcceptedSettlementCompleted, AcceptedCreditSettlementCompleted, AcceptedWithoutPosting, or Rejected |
paymentResponse.paymentTransactionId | string | Conditional | The end-to-end identifier assigned by the rail. Once set, MUST NOT change |
paymentResponse.OpenFinanceBilling.numberOfSuccessfulTransactions | integer | No | Number of successful transactions (typically 1 per payment) |
paymentResponse.RejectReasonCode[] | array | Conditional | Required when status is Rejected |
paymentResponse.RejectReasonCode[].Code | string | Yes (in array) | Pattern: ^(LFI|AANI|FTS)\.[A-Za-z0-9]+$ |
paymentResponse.RejectReasonCode[].Message | string | Yes (in array) | Sanitised, customer-relayable description |
Example — successful settlement
{
"paymentResponse.status": "AcceptedSettlementCompleted",
"paymentResponse.paymentTransactionId": "de857816-3016-4567-86b6-8f418e36fb27"
}
Example — rail rejection
{
"paymentResponse.status": "Rejected",
"paymentResponse.paymentTransactionId": "de857816-3016-4567-86b6-8f418e36fb27",
"paymentResponse.RejectReasonCode": [
{
"Code": "AANI.AM04",
"Message": "Payment request cannot be executed as insufficient funds at debtor account."
}
]
}
Example — LFI screening rejection
{
"paymentResponse.status": "Rejected",
"paymentResponse.RejectReasonCode": [
{
"Code": "LFI.ScreeningRejected",
"Message": "Payment rejected by LFI screening controls."
}
]
}
Response
A successful PATCH returns 204 No Content with no body.
Serve payment status to the TPP
GET/payments/:paymentId backs the TPP request GET https://rs1.LFICODE.apihub.openfinance.ae/open-finance/payment/v2.1/payments/{PaymentId}. The TPP polls this to observe screening outcomes, rail settlement, and any subsequent rejection.
Response
The response shape mirrors the POST/payments 201 response.
{
"data": {
"id": "5ff155ea-853f-480c-ac74-1eaed7c1201f",
"consentId": "cac2381a-7111-4c5f-bc2f-4319a93da7c5",
"paymentTransactionId": "de857816-3016-4567-86b6-8f418e36fb27",
"status": "AcceptedSettlementCompleted",
"statusUpdateDateTime": "2026-04-18T10:14:31Z",
"creationDateTime": "2026-04-18T10:14:23Z",
"instruction": {
"Amount": { "amount": "49.00", "currency": "AED" }
},
"paymentPurposeCode": "SUBS",
"openFinanceBilling": { "Type": "Collection" }
},
"meta": {}
}
Behavioural rules
| # | Rule |
|---|---|
| 1 | Sustain period — Serve GET/payments/{paymentId} for at least 1 year from the payment's creation date |
| 2 | Status consistency with the API Hub — The Status returned MUST exactly match the most recent value PATCHed to the API Hub |
| 3 | paymentTransactionId consistency — Once the rail has assigned the end-to-end identifier, this endpoint MUST return the same value |
Errors
| Status | errorCode | When to use |
|---|---|---|
404 | Resource.NotFound | No payment exists for the supplied paymentId |
403 | Consent.AccountTemporarilyBlocked / Consent.PermanentAccountAccessFailure | Debtor account inaccessible |
500 | GenericRecoverableError / GenericError | Transient or unrecoverable server error |
