Date & Time Handling Across the Standard
Every date-time field in the standard identifies an instant in time, not a wall-clock reading. The same instant has many valid spellings, and all of them must behave identically. Getting this wrong produces errors that are invisible at long time horizons and severe at short ones.
This article is a draft attached to OFP-014. It sets out how to produce and consume date-times across the standard, and publishes to the knowledge base if that proposal is agreed. The date-range query parameters on the transaction and statement list endpoints are specified separately and are not covered here.
A date-time is an instant, not a wall clock
Date-time fields across the standard — ExpirationDateTime, CreationDateTime, TransactionDateTime, BookingDateTime, ValueDateTime and the rest — are defined as type: string, format: date-time: an ISO 8601 / RFC 3339 date-time carrying a mandatory timezone offset. Three rules follow, and everything else here is a consequence of them.
- A date-time is an instant. The offset is part of the value, not decoration.
- Equivalent representations MUST behave identically. Two encodings of the same moment are the same value.
- The offset is mandatory. A value without one is not valid against the standard and MUST NOT be produced.
2027-07-22T00:00:00Z
2027-07-22T00:00:00.000Z
2027-07-22T00:00:00+00:00
2027-07-22T00:00:00.000+00:00
2027-07-22T04:00:00+04:00
# Five encodings. One instant. A conforming implementation
# resolves all five to the same moment.Why this matters more than it looks. The UAE is UTC+04:00. An implementation that reads the digits and ignores the offset resolves a UTC value to an instant four hours earlier than intended. At a thirty-day consent expiry that error is invisible. At a one-hour expiry the consent is already expired on arrival. The bug does not announce itself — it ships, and surfaces later as an unreproducible customer complaint.
Normalise at your own boundary, on the way in. Treat every date-time you receive as a string that must be parsed into an instant before it is used, stored, or compared.
What to send, and how to read what comes back
Sending
- SHOULD emit UTC —
Zor+00:00. Both are equally valid; pick one and be consistent. - MUST include an offset. A value with no offset is rejected by the API Hub on consent creation, with a
format - date-timeerror. - MUST NOT use
-00:00. ISO 8601 prohibits a negative zero offset; RFC 3339 permits it but assigns it “UTC, local offset unknown” semantics, which is not what you mean. - Fractional seconds are optional. Send them or omit them — but do not rely on the value coming back at the same precision.
Receiving
- Parse into an instant. Apply the offset. Never read the wall-clock digits.
- Never default a missing offset to local time. Treat it as an error, or at minimum default to UTC. Defaulting to local is what turns a missing offset into a silent four-hour error.
- Never compare date-times as strings.
2027-07-22T00:00:00Zand2027-07-22T00:00:00.000Zare the same instant and different strings. Anything built on string equality — deduplication keys, idempotency hashes, change detection — breaks the moment a counterparty adjusts its serialiser. Key on identifiers such asTransactionId, or on the parsed instant at a fixed precision. - A single payload may legitimately mix offsets. A consent record can carry the TPP's
ExpirationDateTimeasZalongside the platform's ownCreationDateTimeas+04:00. Both are correct.
Different institutions serialise differently, and legitimately so. Record what each counterparty emits and alert when the shape changes — a serialiser change upstream produces no error, only different bytes.
Apply the offset, and check your own stack first
Receiving from the API Hub
- Parse the offset. This is the single most important line in this article. A consent expiry of
2027-07-22T00:00:00Zis2027-07-22T04:00:00UAE time — not2027-07-22T00:00:00UAE time. - Do not re-implement validation the platform already performs. The API Hub validates consent date-times on creation: it rejects values with no offset, and rejects an
ExpirationDateTimethat is not in the future. Re-validating in the LFI adds a second, divergent implementation of a centralised check. - Beware your own framework. In most reported cases of “we received it without an offset”, the offset was present on the wire and removed by a deserialiser, an ORM column mapping, or a log formatter.
When diagnosing a missing offset, capture the raw request body before any JSON parsing. A value read back from your own database or logs has already passed through the layer most likely to be at fault, so it cannot tell you what arrived.
Emitting
- SHOULD emit UTC —
Zor+00:00— on every date-time you produce. - MUST include an offset on every date-time in every response.
- Where a transaction's local context matters, carry it in the dedicated
LocalTimeZonefield (formatUTC+04:00, including theUTCprefix — a bare+04:00fails that field's pattern). Do not express local context by shifting the offset onTransactionDateTime.
A two-minute self-test
2027-07-22T00:00:00Z
2027-07-22T04:00:00+04:00
# The same moment, written two ways. If your system behaves
# differently for these two, it is not applying the offset.If your system behaves differently for those two values, your implementation is not applying the offset — and you have reproduced the defect without instrumenting anything.
Verify your normalisation
Epoch is seconds since 1970-01-01T00:00:00Z.
| Value | Epoch | UAE local time |
|---|---|---|
2027-07-22T00:00:00Z | 1816214400 | 2027-07-22T04:00:00+04:00 |
2027-07-22T00:00:00.000Z | 1816214400 | 2027-07-22T04:00:00+04:00 |
2027-07-22T00:00:00+00:00 | 1816214400 | 2027-07-22T04:00:00+04:00 |
2027-07-22T00:00:00.000+00:00 | 1816214400 | 2027-07-22T04:00:00+04:00 |
2027-07-22T04:00:00+04:00 | 1816214400 | 2027-07-22T04:00:00+04:00 |
2027-07-22T00:00:00-05:00 | 1816232400 | 2027-07-22T09:00:00+04:00 |
2027-04-05T10:43:07+00:00 | 1806921787 | 2027-04-05T14:43:07+04:00 |
The first five rows are one instant. If your implementation produces more than one distinct value for them, it is not conforming. The sixth row is a genuinely different instant, five hours later — included so the test distinguishes “applies the offset” from “ignores the offset and happens to agree”.
