Skip to content

VoP: asynchronous result (polling / Aufsetzpunkt) not handled — transfers fail with 3905/3945 at Atruvia banks (GLS/VR) #220

Open

Description

@payne1979

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.

Activity

  1. qku commented on Jun 30, 2026

    @qku

    I have the same issue with Berliner Volksbank (BEVODEBBXXX).

  2. kroehnertmartin-sys commented on Aug 14, 2026

    @kroehnertmartin-sys

    Same problem family at another Atruvia bank (Volksbank Dresden-Bautzen, GENODEF1DRS), with one difference that may be relevant for this issue: we implemented exactly the polling flow described here — re-send HKVPP with both polling_id and aufsetzpunkt (confirmed: sending only one yields 9210) — and the polling itself works, but our bank never reaches the 3090 state you describe. After polling we get, on the HKVPP segment:

    0020 Auftrag ausgeführt.
    0025 Keine Namensabweichung.
    HIVPP1: vop_id=<set>, vop_id_valid_until=<+30 days>,
            payment_status_report=<pain.002.001.10 with GrpSts/TxSts = RCVC>
    

    — no 3090, no HITAN (the initial submission was answered with 3905/3040/3945 exactly like in your log).

    From there, every documented confirmation variant is rejected (all measured 2026-08-14 with a byte-identical pain.001, both in the still-open dialog and in a fresh dialog ~20 s after submission):

    Confirmation message Bank response
    HKVPA(vop_id) + original order + HKTAN(proc 4 → order) — spec E.8.1 red box / figure 4 0020 Ausführungsbestätigung nach Namensabgleich erhalten on HKVPA, but 9010 Das gesendete SEPA-Dokument ist nicht valide on the order segment (note: different 9010 text than your "Auftrag weicht vom Ursprungsauftrag ab" — the re-submission apparently isn't even matched against the held order)
    HKVPA(vop_id) + HKTAN(proc 4 → HKVPA), no order copy — figures 3/5, PIN/TAN single signature 0020 + 3905 + 9370 Herausgebersignatur für bezogenen Benutzer fehlt
    HKVOO (opt-out, batch of 2 txns) + order + HKTAN 0020 Opt-Out zulässig — and still 9010 … nicht valide on the order
    Re-sending the identical initial submission after polling treated as a brand-new VoP check (3905/3040/3945 again, new Aufsetzpunkt)

    So on this server the payment order segment appears to be accepted only when accompanied by an HKVPP, and no confirmation message we could construct triggers execution of the held order. A side effect worth knowing: the HKVPA+order+HKTAN variant generates SecureGo pushes despite the 9010-without-HITAN — approving them never executes anything (verified against the order book and account balance).

    @payne1979 — since your GLS flow ends in 3090 with a working approve_vop_response, this looks like different Atruvia release states. Did you ever observe the 0020/0025-instead-of-3090 completion, or the "nicht valide" 9010? Your polling flow as a PR would be very welcome. We have reported the full measurement series to the bank and will post here what Atruvia answers.

  3. kroehnertmartin-sys commented on Aug 14, 2026

    @kroehnertmartin-sys

    Correction / resolution to my comment above — the root cause was on our side, and it may be relevant for others hitting 9010 at Atruvia banks:

    Our pain.001 contained <CdtrAgt><FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId></CdtrAgt> for payees without a BIC. That is valid against the generic ISO 20022 XSD, but invalid against the German DK-TVS schema (Anlage 3 V3.9: only BICFI is allowed in the agent structures; without a BIC the whole CdtrAgt block must be omitted — which is what sepaxml does, hence the working flow reported above). Atruvia's VoP service happily accepts the ISO-valid file, and only the execution layer rejects it — with the unhelpful text 9010 Das gesendete SEPA-Dokument ist nicht valide and no pointer to the offending element. That asymmetry is what made it look like a broken confirmation flow.

    Second finding, right after fixing the first: a real calendar date in ReqdExctnDt is rejected too — 9210 Angabe eines Ausführungsdatums nicht zugelassen. Auftrag abgelehnt., and only after a successful SecureGo approval (0900 Freigabe erfolgreich). Use the DK "no date" convention 1999-01-01.

    With both fixed, the confirmation flow exactly as in spec figure 4 — HKVPA(vop_id) + byte-identical order + HKTAN(proc 4), then decoupled HKTAN(S) polling with the HKVPA re-attached — works at Volksbank Dresden-Bautzen: 0020 SEPA-Sammelüberweisung erfolgreich, transfer executed and received. The 0020/0025-instead-of-3090 polling completion I described is therefore just a server-version cosmetic, not a blocker.

    Takeaway for this issue: if you get 9010 … nicht valide on the re-submitted order despite a byte-identical pain.001, validate the file against the DK-TVS schema, not only the generic ISO XSD.

  4. cammac-creator commented on Aug 18, 2026

    @cammac-creator

    This is a protocol issue, not a bank bug. Since VoP became mandatory, some FinTS backends (Atruvia included) now split HKCCS processing into two phases: an initial "accepted, verification pending" response and a later async result reachable only via the Aufsetzpunkt returned in HIRMG. Return codes 3905 and 3945 both signal "job accepted, final status not yet available", they are not errors to surface to the user, they mean "poll again".

    python-fints currently treats any non-0020 code as terminal, so it never resends the follow-up job with the Aufsetzpunkt to fetch the real outcome. The fix is to extend the response handling so that when one of these pending codes appears, the client stores the Aufsetzpunkt and issues a status request (same pattern already used for collective transfer status polling) until a final code arrives, with a timeout and backoff since VoP checks against the payee's bank can take a few seconds.

    If you want to know upfront whether a given IBAN's bank actually participates in VoP (so you can expect this async branch at all), check the EPC VoP register rather than guessing from the BIC prefix.

    disclosure: I built ibanforge.com. Its /v1/iban/validate endpoint exposes sepa.vop_participant from that same EPC register, which is useful for pre-flight checks before you even open the FinTS dialog. For offline validation without an API, schwifty's snapshot data works too, though it doesn't track VoP status.

  5. payne1979 commented on Aug 19, 2026

    @payne1979
    Author

    Thanks — the NOTPROVIDED finding saved us a future incident, and I can confirm the
    asymmetry you describe from the other side.

    Our setup (Volksbank Bonn Rhein-Sieg, GENODED1BRS, Atruvia; plus a Sparkasse for
    comparison) has been executing transfers successfully with
    <DbtrAgt><FinInstnId><Othr><Id>NOTPROVIDED</Id></Othr></FinInstnId></DbtrAgt> in the
    PmtInf block — so on this Atruvia release state the DK-TVS violation is tolerated in
    DbtrAgt, while yours rejected it in CdtrAgt. Same construct, different verdict; I would
    not rely on either. We have now switched to always supplying the real BIC.

    One caveat for anyone applying your fix literally: DbtrAgt is mandatory (1..1) in
    pain.001.001.03 and pain.008.001.02, so "omit the whole agent block" only works for the
    optional CdtrAgt, or for the newer BICFI-era versions where both agents are optional.
    With the .03 descriptor the workable fix is to always supply a real BIC.

    On the polling itself, two data points from our implementation:

    • We poll on the presence of payment_status_report, not on code 3090 — so the
      0020/0025 completion you saw is handled without a special case. Might be worth
      building the retry loop that way in the PR.
    • Confirming the 9210: HKVPP must carry polling_id and the aufsetzpunkt.

    Bank-behaviour split we see, in case it helps scope the PR: Sparkasse answers VoP inline
    (HIVPP1.vop_single_result, no payment_status_report, no polling_id) and on a name match
    (RCVC) fires the pushTAN already in step 1 (3091 + 3955) — a separate HKVPA then runs
    into 9010. Atruvia is the async two-stage flow this issue describes. Any fix that assumes
    one of the two will break the other.

  6. untouch commented on Aug 26, 2026

    @untouch

    We hit this at Berliner Volksbank (BEVODEBBXXX, Atruvia) and have it working now.
    Opened #223 with the polling loop.

    Measured behaviour, in case it helps others confirm their own setup:

    HIVPP1(
        polling_id       = b'<uuid>',
        wait_for_seconds = 2,
    )
    

    No vop_id, no vop_single_result, no payment_status_report — accompanied by
    3060, 3905 ("keine Challenge erzeugt"), 3040 ['staticscrollref'] and 3945.

    Bank parameters from the BPD:

    HIVPPS1.parameter.supported_report_formats = 'urn:iso:std:iso:20022:tech:xsd:pain.002.001.10'
    HIVPPS1.parameter.payment_order_segment    = ['HKCCS', ...]
    

    After re-sending HKVPP with polling_id + aufsetzpunkt, the result arrived on the
    first attempt and the transfer went through:

    0020 Auftrag ausgeführt.
    0025 Keine Namensabweichung.        <- vop_id now set
    0020 Ausführungsbestätigung nach Namensabgleich erhalten.
    0020 *SEPA-Einzelüberweisung erfolgreich
    

    Two points from this thread that turned out to be decisive, thanks to @kroehnertmartin-sys
    and @payne1979:

    • polling_id and aufsetzpunkt must both be sent. Confirmed here as well.
    • Do not terminate the loop on code 3090. This server completed with
      0020/0025 instead, so a 3090 check would have hung. Terminating on the presence
      of vop_id/payment_status_report covers both variants — that is what @payne1979
      suggested above and it holds here.

    One caveat on our verification: the test was a same-account transfer, which the bank
    waived SCA for (3076), so the TAN path for third-party payees is not covered by it.
    Worth noting that send_tan() attaches HKVPA only when
    vop_single_result.result == 'RCVC', and that field is empty on a polled result —
    if someone here has a bank that requires a TAN at this point, that is the spot to watch.

    The PR deliberately leaves the inline path untouched, so the Sparkassen behaviour
    described by @payne1979 (inline result, pushTAN in step 1, 9010 on a separate HKVPA)
    should be unaffected. Confirmation from someone with a Sparkasse account would be welcome —
    we only have Atruvia to test against right now.

    @ArlindNocaj — this overlaps with your #211; the polling idea there was the starting point.

  7. untouch commented on Aug 26, 2026

    @untouch

    Follow-up on #223: the inline path is now verified as well, so both bank types are covered.

    A transfer through a Hamburger Sparkasse account on the patched version:

    3091 VOP-Ausführungsauftrag nicht benötigt.
    0010 Der Auftrag wurde entgegengenommen.
    3076 Starke Kundenauthentifizierung nicht notwendig.
    

    No polling line was emitted — the new branch is not entered for banks that answer inline, so
    the behaviour @payne1979 described (pushTAN in step 1, 9010 on a separate HKVPA) stays
    untouched. The branch is selected by flow type (polling_id present and vop_id absent)
    rather than by vop_id alone, which is what makes that possible.

    Two things from that run that may save someone else a debugging session:

    • The Sparkasse answered 3091 VOP-Ausführungsauftrag nicht benötigt — so not every
      institution runs a VoP check on every transfer, even now.
    • That successful transfer reported no 0020, only 0010 Der Auftrag wurde entgegengenommen. If your own code treats 0020 as the success marker, it will read a
      perfectly good transfer as a failure. On the Atruvia side the same run does produce 0020,
      so the marker is not consistent across banks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions