Skip to content

feat: HKEKA — electronic account statements (Elektronischer Kontoauszug) - #32

Merged
robocode13 merged 2 commits into
robocode13:mainfrom
phkoenig:feat/hkeka-electronic-statements
Aug 1, 2026
Merged

feat: HKEKA — electronic account statements (Elektronischer Kontoauszug)#32
robocode13 merged 2 commits into
robocode13:mainfrom
phkoenig:feat/hkeka-electronic-statements

Conversation

@phkoenig

Copy link
Copy Markdown
Contributor

What this adds

HKEKA fetches the electronic account statement (Elektronischer Kontoauszug) — the document the bank files in the customer's electronic mailbox, usually a PDF. Not a transaction list: the bank's own periodic statement, the same one a customer would otherwise download by hand.

if (client.canGetElectronicStatements(accountNumber)) {
  let response = await client.getElectronicStatements(accountNumber);
  // response.statements[0].document  — the PDF
  // response.nextOffset              — pass back to fetch the successor
}

It follows the existing shape: can* capability check, getX / getXWithTan pair, README table rows, segments registered in registry.ts.

Why it is worth having: it is an independent source. Transactions come from the bank's transaction endpoint; the statement comes from the bank's document archive. Comparing the two catches the failure mode where a sync reports success and silently returns nothing — which is exactly what led me here.

Files

HKEKA / HIEKA / HIEKAS request, response, bank parameters
electronicStatementInteraction.ts orchestration, offset handling
electronicStatement.ts public types
client.ts, index.ts, README.md, registry.ts wiring
HKEKA.test.ts 17 tests

129 tests pass (112 before).

Two details worth your attention

Version gates. Only version 5 carries date / year / number; up to version 4 the document follows the time range directly, and version 1 has no iban / bic / name. Decoding an older response with the v5 layout does not fail — it shifts every field by three positions and hands out the advertisement text as the document. The same applies to the request: KTV3 up to v3, KTVInt from v4, statement year only from v3. Both are gated by minVersion.

Also note booked sits after date/year/number in HIEKA, while HIEKP v2 orders the same fields the other way round. The order is per-segment and cannot be carried over.

Binary.decode now honours the declared length. This touches a shared primitive, so it is the change I would scrutinise first.

A binary value arrives as @<length>@<data>. The old implementation returned everything after the second @. For a PDF that is wrong: the payload is full of bytes that look like separators (+, :, ') and escape characters, and the declared length is the only thing that tells data from what follows it. Without this, statement documents come out with trailing garbage.

There is a guard for values that are not length-prefixed — they are returned unchanged, which is what the existing tests feed in.

Deliberately not implemented: acknowledgement (HKQTG)

Banks that set receiptRequired in their HIEKAS parameters keep offering a statement until it is acknowledged with its receipt. The receipt is parsed and exposed on the response, but nothing is sent back.

That is intentional and I would rather it be a conscious gap than a silent one: acknowledging is destructive from the customer's point of view — the statement stops being offered, and depending on the bank leaves the mailbox. A library should not do that as a side effect of a read call. If you want it, I would suggest an explicit separate method rather than a flag on the fetch.

Consequence for the current state: against a bank that requires acknowledgement, repeated calls will keep returning the same statement. My bank does not set the flag, so this path is untested against a real bank.

Scope of verification — please read

Verified against one bank: Berliner Volksbank (GAD backend), HIEKA v5. 55 statement documents across 8 accounts, fetched without a TAN, PDFs opened and checked against the accounts they belong to.

Untested against a real bank: versions 1–4, receiptRequired, formats other than PDF (format 1 = MT940, 2 = ISO, 3 = PDF). Those follow the specification and are covered by unit tests only.

Relation to #31

Independent in code — no shared files, either can merge first. In practice they belong together: statement documents are large, so a bank will routinely spread them over several messages, and without #31 the response arrives empty. That is how I found #31 in the first place.


Written with AI assistance (noted in the commit trailers); the measurements and the review are my own. Happy to adjust anything to your preference — naming, comment density, or splitting the Binary change out into its own PR if you would rather look at it separately.

phkoenig and others added 2 commits July 28, 2026 18:23
…auszug)

Fetches the statement document the bank files in the customer's electronic
mailbox (a PDF for most banks) instead of a list of transactions. For accounts
where the bank has withdrawn DKKKU this is the only remaining automated way to
get at credit card statements.

  HKEKA  v5  request        (SEG "Kontoauszug5")
  HIEKA  v5  response       (SEG "KontoauszugRes5")
  HIEKAS v5  parameters     (SEG "KontoauszugPar5")

Element order follows the FinTS 3.0 specification. It is worth spelling out
because it is easy to get wrong: in HIEKA v5 `booked` sits SIXTH, after
format/TimeRange/date/year/number — whereas in HIEKP v2 the same field comes
first. Ordering it wrongly makes the segment silently unparseable.

The request element is deliberately named `offset` and not `continuationMark`:
for HKEKA the bank does not split one oversized response across messages, it
announces with code 3040 that a *further document* is waiting. Reusing the
`continuationMark` name would enlist the generic parted-message handling, which
splices two complete HIEKA segments into one corrupt segment. Callers page
through the mailbox with `nextOffset` instead.

Verified against a live bank (Berliner Volksbank, HKEKA v5, tanRequired=false):
six credit card statements and two current account documents retrieved, every
PDF intact from %PDF header to %%EOF, paging terminating on the bank's last
document.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two defects found by adversarial review of the HKEKA feature, both of the silent
kind: no exception, success: true, wrong data.

Version gating. The definitions described version 5 only, while
getMaxSupportedTransactionVersion happily negotiates whatever the bank announces
below that. Per the FinTS 3.0 segment catalogue, versions 1-3 of the request carry
the national account connection (KTV3) rather than the international one, `year`
does not exist before version 3, only version 5 of the response carries
date/year/number, and version 1 has no iban/bic/name. A version 4 response decoded
with the version 5 layout shifts every field by three positions and hands out the
advertisement text as the statement document. The elements are now gated the way
HKSAL and HKKAZ do it.

Binary length. Binary.decode returned everything after the second '@' instead of
the declared number of characters. The length is the only thing that tells payload
apart from the separators and escape characters a PDF is full of. Fields without a
length prefix keep their previous behaviour.

That second fix also settled an open question: the fetched statements carry one
0x00 after their %%EOF, and it survives length-exact decoding — so the byte is
inside the length the bank declares. It is the bank's, not ours.

Tests cover encoding version 3 vs version 5, decoding version 1 and version 4,
a payload containing +, ', @ and ?, a padded binary field, and the interaction
itself: mapping, nextOffset, several statements per response, and the base64
unwrapping in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@robocode13

Copy link
Copy Markdown
Owner

Nice contribution, thanks. I could test it successfully with one of my banks, the others don't support it unfortunately.

@robocode13
robocode13 merged commit 4c45522 into robocode13:main Aug 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants