VoP: asynchronous result (polling / Aufsetzpunkt) not handled — transfers fail with 3905/3945 at Atruvia banks (GLS/VR)
python-fints version: 5.0.0
Bank: Atruvia-hosted (GLS Bank / Volksbanken Raiffeisenbanken), e.g. BIC GENODEM1GLS / GENODED1BRS
Context: Verification of Payee (VoP) became mandatory in the EU on 2025-10-09.
Summary
A normal client.sepa_transfer(...) (HKCCS, and likewise multiple=True/HKCCM) cannot be
completed at these banks. The library does send the VoP request segment HKVPP
(_find_vop_format_for_segment returns the pain.002.001.10 descriptor and
_need_twostep_tan_for_segment is True), but the bank does not return the VoP result
inline. Instead it returns:
- an empty
HIVPP1 (vop_single_result → empty EVPE(), result is None),
- response code
3040 with an Aufsetzpunkt/touchdown token (e.g. 'staticscrollref') on the HKVPP segment,
- code
3905 ("Es wurde keine Challenge erzeugt"),
- code
3945 ("Freigabe ohne VOP-Bestätigung nicht möglich").
Because _send_pay_with_possible_retry expects the VoP result inline in the first
response, it never produces a usable NeedVOPResponse and the transfer dead-ends on 3945.
Root cause
For these banks the VoP result is delivered asynchronously. The first HKVPP only
acknowledges the request and returns a polling_id, a wait_for_seconds hint and a touchdown
(3040). The actual result must be polled: re-send HKVPP with both polling_id
and the aufsetzpunkt token, honoring wait_for_seconds, until the response's HIVPP1
carries a payment_status_report (a pain.002.001.10) plus a vop_id — signalled by response
code 3090 ("Ergebnis des Namensabgleichs prüfen").
Important details discovered:
- The poll must include
polling_id and aufsetzpunkt; sending only one yields
9210 ("VOP-Auftrag ungültig").
- The per-transaction result lives in the
pain.002 TxInfAndSts/TxSts
(RCVC/RVMC/RVNM/RVNA), not in EVPE. For a close match (RVMC) the bank's
on-file name is in the per-transaction TxInfAndSts/StsRsnInf/AddtlInf (the group-level
StsRsnInf only contains the static legend). HIVPPS.parameter.report_complete == 'V'.
- Confirmation works via
approve_vop_response → HKVPA1(vop_id) (this part is fine once a
correct vop_id is available). The re-submitted order must be byte-identical to the original
(same pain.001), otherwise 9010 ("Auftrag weicht vom Ursprungsauftrag ab").
- Minor robustness bug: when
HIVPP1.vop_single_result is empty, the current
if vop_result.result in (...) path risks an AttributeError on None.
Sanitized log (initial send + one poll)
3905 - Es wurde keine Challenge erzeugt.
3040 - Es liegen weitere Informationen vor. (['staticscrollref'])
3945 - Freigabe ohne VOP-Bestätigung nicht möglich.
HIVPP1: vop_id=None polling_id=<set> wait_for_seconds=2 payment_status_report=0 bytes vop_single_result=EVPE() # empty
# after sleeping wait_for_seconds and re-sending HKVPP(polling_id=..., aufsetzpunkt='staticscrollref'):
3090 - Ergebnis des Namensabgleichs prüfen.
HIVPP1: vop_id=<set> polling_id=None payment_status_report=<~3.6 kB pain.002.001.10> # result ready
Suggested fix
In _send_pay_with_possible_retry (and the VoP branch generally): when the first HIVPP1
has no inline single result but provides a polling_id / wait_for_seconds (and/or a 3040
touchdown), poll HKVPP (with polling_id and aufsetzpunkt) until a
payment_status_report + vop_id are present, then return a NeedVOPResponse carrying that
vop_id (and ideally the parsed pain.002 per-transaction results). Also guard against an
empty EVPE (vop_single_result is None).
I have a working implementation of exactly this polling flow against a GLS/Atruvia account
(single HKCCS and batch HKCCM, real transfers executed) and am happy to turn it into a PR if
the approach looks right.
VoP: asynchronous result (polling / Aufsetzpunkt) not handled — transfers fail with 3905/3945 at Atruvia banks (GLS/VR)
python-fints version: 5.0.0
Bank: Atruvia-hosted (GLS Bank / Volksbanken Raiffeisenbanken), e.g. BIC
GENODEM1GLS/GENODED1BRSContext: Verification of Payee (VoP) became mandatory in the EU on 2025-10-09.
Summary
A normal
client.sepa_transfer(...)(HKCCS, and likewisemultiple=True/HKCCM) cannot becompleted at these banks. The library does send the VoP request segment
HKVPP(
_find_vop_format_for_segmentreturns thepain.002.001.10descriptor and_need_twostep_tan_for_segmentisTrue), but the bank does not return the VoP resultinline. Instead it returns:
HIVPP1(vop_single_result→ emptyEVPE(),resultisNone),3040with an Aufsetzpunkt/touchdown token (e.g.'staticscrollref') on the HKVPP segment,3905("Es wurde keine Challenge erzeugt"),3945("Freigabe ohne VOP-Bestätigung nicht möglich").Because
_send_pay_with_possible_retryexpects the VoP result inline in the firstresponse, it never produces a usable
NeedVOPResponseand the transfer dead-ends on 3945.Root cause
For these banks the VoP result is delivered asynchronously. The first
HKVPPonlyacknowledges the request and returns a
polling_id, await_for_secondshint and a touchdown(
3040). The actual result must be polled: re-sendHKVPPwith bothpolling_idand the
aufsetzpunkttoken, honoringwait_for_seconds, until the response'sHIVPP1carries a
payment_status_report(apain.002.001.10) plus avop_id— signalled by responsecode
3090("Ergebnis des Namensabgleichs prüfen").Important details discovered:
polling_idandaufsetzpunkt; sending only one yields9210("VOP-Auftrag ungültig").pain.002TxInfAndSts/TxSts(
RCVC/RVMC/RVNM/RVNA), not inEVPE. For a close match (RVMC) the bank'son-file name is in the per-transaction
TxInfAndSts/StsRsnInf/AddtlInf(the group-levelStsRsnInfonly contains the static legend).HIVPPS.parameter.report_complete == 'V'.approve_vop_response→HKVPA1(vop_id)(this part is fine once acorrect
vop_idis available). The re-submitted order must be byte-identical to the original(same pain.001), otherwise
9010("Auftrag weicht vom Ursprungsauftrag ab").HIVPP1.vop_single_resultis empty, the currentif vop_result.result in (...)path risks anAttributeErroronNone.Sanitized log (initial send + one poll)
Suggested fix
In
_send_pay_with_possible_retry(and the VoP branch generally): when the firstHIVPP1has no inline single result but provides a
polling_id/wait_for_seconds(and/or a3040touchdown), poll
HKVPP(withpolling_idandaufsetzpunkt) until apayment_status_report+vop_idare present, then return aNeedVOPResponsecarrying thatvop_id(and ideally the parsedpain.002per-transaction results). Also guard against anempty
EVPE(vop_single_result is None).I have a working implementation of exactly this polling flow against a GLS/Atruvia account
(single HKCCS and batch HKCCM, real transfers executed) and am happy to turn it into a PR if
the approach looks right.