Skip to content

Fetch Holdings or Transactions from Account without IBAN #104

Open

Description

@maltere

I am trying to fetch my current balances of an Deka Depot at the Sparkasse.

For an Deka Depot does not exist an IBAN so it only appears using client.get_information() but not with client.get_sepa_accounts().
My problem is that I can not provide an SEPAAccount-objejct for the Depot to fetch information from .get_transactions() or .get_holdings().

Is there a way to fetch the balances of the Depot?

I tried passing other SEPAAccounts to client.get_holdings(account), but this does not work, as I retrieve following error:

fints.exceptions.FinTSClientError: Error during dialog initialization, could not fetch BPD. Please check that you passed the correct bank identifier to the HBCI URL of the correct bank.

The same accounts work fine with client.get_transactions() and show their own transactions as expected.

The Deka Depot has following attributes when fetched with client.get_information():

  • type: 60
  • iban: None
  • account_number: xxx
  • supported_informations: [many]

While researching for a solution, I found similar problems with credit cards, but as credit card object types are 50 I did not expect that this would solve my problem.
Never less I tried using client.get_credit_card_transactions() with my deka depot. Suprise: It did not work.

I appreciate every idea. If you need me to provide more information, please feel free to ask.

Activity

  1. maltere commented on Feb 10, 2020

    @maltere
    Author

    Maybe this is related to #41 and #24?

  2. forgeorge commented on May 17, 2020

    @forgeorge

    A solution is to fetch the account information from the accounts list obtained by get_information and pass in into a SEPAAcoount object. The function get_account_from_information() pasted below replaces the function get_sepa_account().
    I could get the balance from a Sparkasse Deka Depot, and also credit card transactions from the Sparkasse. Other accounts also seem to work, but are not completely tested.

    from fints.models import SEPAAccount

    def get_account_from_information(f):
        information = f.get_information()
        if isinstance(information, NeedTANResponse):
            information = ask_for_tan(information)
            
        accounts = information["accounts"]    
        if len(accounts) == 1:
            choice = 0
        else:
            print("Multiple accounts available, choose one")
            for i, mm in enumerate(accounts):
                print(i, mm["account_number"], mm["product_name"])
            choice = int( input("Choice: ").strip() )
              
        account = SEPAAccount(iban=accounts[choice]["iban"], bic="xxxxDExxxxx", # only [4:6] is used 
            accountnumber=accounts[choice]["account_number"], subaccount=accounts[choice]["subaccount_number"], 
                blz=f.bank_identifier.bank_code)
        return account
    
  3. lausser commented on Jan 30, 2021

    @lausser

    I am customer of Augsburger Aktienbank and faced the same problem. I modified the above code a bit so that it returns a list (in my case just 1) of depots.

    def get_depots_from_information(f):
        information = f.get_information()
        if isinstance(information, NeedTANResponse):
            information = ask_for_tan(information)
    
        for acc in information["accounts"]:
            if not accounts[choice].get("account_number", None):
                continue
            if acc["type"] == 30:
                # product_name (Depot) netbank Wertpapierdepo
                # all the other accounts (Tagesgeldkonto, Girokonto,...)
                # have a type of 1
                # blz is not copied from f (connection to bank2) but from acc.
                # when bank2 aquired my old bank1, my existing accounts were
                # transferred and still have the old blz.
                yield SEPAAccount(iban=accounts[choice]["iban"],
                    bic="xxxxDExxxxx", # only [4:6] is used 
                    accountnumber=accounts[choice]["account_number"],
                    subaccount=accounts[choice]["subaccount_number"],
                    blz=mm["bank_identifier"].bank_code)
    

    The type 30 might be different in your case, i haven't found a documentation which would confirm that 30 stands for a stock portfolio/Aktiendepot.
    First i wanted this function to return both accounts and stock portfolios, but that was no solution for my use case. Because the SEPAAccount does not have this type information. And i needed it because i wanted to run get_holdings(), which fails for all ordinary accounts.
    So now i have basically:

        accounts = f.get_sepa_accounts()
        for acc in accounts:
            balance = f.get_balance(acc)
            print(" bal =====================")
            pprint.pprint(balance)
        for dep in get_depots_from_information(f):
            print("dep ====================")
            pprint.pprint(dep)
            holdings = f.get_holdings(dep)
            for hol in holdings:
                print("hol =========")
                pprint.pprint(hol)
    
    
  4. forgeorge commented on Jun 6, 2021

    @forgeorge

    Some addtional information:
    Sparkasse Deka Depots are type 60, and MLP Bank Depots are type 30.
    Sparkasse Deka Depot do not support get_holdings().
    MLP Bank Depots require the full BIC for creating the SEPAAccount, not only characters [4:6].
    Calling get_balance() fails for MLP Bank Depots, even as it is supported.
    get_holdings() does work for MLP Bank Depots, but important information, i.e. the name of the holding, is missing.

  5. jmoeckel commented on Aug 24, 2021

    @jmoeckel

    @forgeorge: I had the same "problem" with MLP Depots and the important information. The reason for the missing information is, that the MT535_Miniparser class expects ISIN, but MLP delivers WKN numbers.

    Actually, it is quite a small workaround (quite dirty, because after this, it will no longer work for ISIN):

    In fints.utils, change the regex in line 143 to
    re_identification = re.compile(r"^:35B:(.*)\/(.*)\|(.*)")

    Additionally change the field for isin in line 165:
    isin = m.group(2)

    Note, that the correspondig Holding still has the attribute ISIN although its the WKN.

    I assume, for a more general fix, the corresponding field in the fints information has to be considered. Unfortunately in the case of WKN, there is no unique identifier string as in the case with ISIN (and I have no information, how it is for depots from other banks besides MLP).

    Maybe this helps.

  6. mhdgth commented on Feb 13, 2026

    @mhdgth

    Solution: Populate KTI1's optional fields for non-IBAN accounts

    We ran into this exact problem with DKB Visa credit cards (account type 50). The credit card appears in get_information() with GET_TRANSACTIONS: True but is absent from get_sepa_accounts() since it has no IBAN.

    Root cause

    KTI1.from_sepa_account() only populates iban and bic:

    @classmethod
    def from_sepa_account(cls, acc):
        return cls(
            iban=acc.iban,
            bic=acc.bic,
        )
    
    But KTI1 defines three additional optional fields that are never used:
    
    account_number = DataElementField(type='id', required=False)
    subaccount_number = DataElementField(type='id', required=False)
    bank_identifier = DataElementGroupField(type=BankIdentifier, required=False)
    
    When HKKAZ7 sends a KTI1 with only empty iban/bic, the bank responds with 9160 - Datenelement fehlt! (missing data element).
    
    Working solution
    
    Constructing KTI1 directly with the optional fields populated — using the account_number and subaccount_number from get_information() — works:
    
    from fints.segments.statement import HKKAZ7
    from fints.formals import KTI1, BankIdentifier
    
    # Values from get_information() — type 50 credit card
    seg = HKKAZ7(
        account=KTI1(
            iban='',           # no IBAN for credit cards
            bic='BYLADEM1001',
            account_number='493********3227',      # masked card number from get_information()
            subaccount_number='121*********==',  # subaccount hash
            bank_identifier=BankIdentifier(
                country_identifier='280',
                bank_code='12030000',
            ),
        ),
        all_accounts=False,
        date_start=start_date,
        date_end=end_date,
        touchdown_point=None,
    )
    
    This returns valid MT940 data (tested with DKB, 98 transactions over 90 days).
    
    Suggested fix
    
    KTI1.from_sepa_account() should fall back to the optional fields when IBAN is empty:
    
    @classmethod
    def from_sepa_account(cls, acc):
        if acc.iban:
            return cls(iban=acc.iban, bic=acc.bic)
        # Non-IBAN accounts (credit cards, depots): use account_number + bank_identifier
        return cls(
            iban='',
            bic=acc.bic,
            account_number=acc.accountnumber,
            subaccount_number=acc.subaccount,
            bank_identifier=BankIdentifier(
                country_identifier=BankIdentifier.COUNTRY_ALPHA_TO_NUMERIC.get(
                    acc.bic[4:6], '280'
                ) if acc.bic and len(acc.bic) > 5 else '280',
                bank_code=acc.blz,
            ),
        )
    
    This is backwards-compatible — IBAN accounts behave exactly as before. Non-IBAN accounts (credit cards, depots) now get proper identification via the optional KTI1 fields, which is what the FinTS spec   
    intended them for.
    
    Tested with
    
    - DKB (BLZ 12030000) — Visa credit card (type 50), HKKAZ7
    - python-fints 4.2.2
    
    ---
    Analysis and solution found with the help of https://claude.ai/code (Claude Opus 4.6, Anthropic).
  7. felixschndr commented on May 20, 2026

    @felixschndr

    I am currently trying to access bank accounts of Sparkasse Karlsruhe. Can you tell me which fints url you are using? I cannot find the correct combination of url and blz online
    @maltere @forgeorge

  8. kermitthefrogisback commented on May 21, 2026

    @kermitthefrogisback

    Hi @felixschndr, just to make sure, here's the full flow according to the documentation:

    1. register with the Deutsche Kreditwirtschaft according to https://python-fints.readthedocs.io/en/latest/quickstart.html#register-for-a-product-id
    2. you obtain a key + a monthly (quarterly?) update of all bank contacts in Germany via e-mail

    It is this mail that I now quote from for your request:

    BLZ | BIC | Institut | Ort | RZ | Organisation | PIN/TAN-Zugang URL | Version | Datum letzte Änderung
    66050101 | KARSDE66XXX | Sparkasse Karlsruhe Ettlingen | Karlsruhe | Finanz Informatik GmbH & Co. KG | DSGV | https://banking-bw4.s-fints-pt-bw.de/fints30 | FinTS V3.0 | 09.02.2022

    Best regards

  9. felixschndr commented on May 26, 2026

    @felixschndr

    Thank you very much. I got the login working with the public product id. And I found this repo for getting the fints url: https://github.com/dr-duplo/python-fints-url

    For anyone coming across this in the future, here is my script that is working as of May 26 2026

    import logging
    import os
    
    from dotenv import load_dotenv
    from fints.client import FinTS3PinTanClient, NeedTANResponse
    
    load_dotenv()
    
    logging.basicConfig(level=logging.INFO)
    
    client = FinTS3PinTanClient(
        bank_identifier="66050101",
        user_id=os.environ["SPARKASSE_USERNAME"],
        pin=os.environ["SPARKASSE_PASSWORT"],
        server="https://banking-bw4.s-fints-pt-bw.de/fints30",
        product_id="6151256F3D4F9975B877BD4A2",
    )
    
    # Pick the pushTAN mechanism by name — code differs per Sparkasse (often 942, sometimes other).
    client.fetch_tan_mechanisms()
    mechanisms = client.get_tan_mechanisms()
    print("Available TAN mechanisms:")
    for sec_func, mech in mechanisms.items():
        print(f"  {sec_func}: {mech.name}")
    
    push_entry = next(
        ((sec_func, mech) for sec_func, mech in mechanisms.items() if "push" in mech.name.lower()),
        None,
    )
    if push_entry is None:
        raise RuntimeError("No pushTAN mechanism advertised by this Sparkasse")
    push_sec_func, push_mech = push_entry
    print(f"Using TAN mechanism {push_sec_func}: {push_mech.name}")
    client.set_tan_mechanism(push_sec_func)
    
    # Some Sparkassen require selecting a TAN medium before opening the dialog.
    if getattr(push_mech, "description_required", None) and str(push_mech.description_required) == "MUST":
        media = list(client.get_tan_media())
        print(f"TAN media: {media}")
        if not media:
            raise RuntimeError("description_required=MUST but no TAN media returned")
        client.set_tan_medium(media[0])
    
    with client:
        if isinstance(client.init_tan_response, NeedTANResponse):
            if client.init_tan_response.decoupled:
                print("Waiting for Sparkasse pushTAN app approval...")
                while isinstance(client.init_tan_response, NeedTANResponse):
                    sleep(2)
                    client.init_tan_response = client.send_tan(client.init_tan_response, "")
            else:
                tan = input("Enter TAN: ")
                client.send_tan(client.init_tan_response, tan)
    
        accounts = client.get_sepa_accounts()
        print(f"\nGot {len(accounts)} account(s):")
        for account in accounts:
            try:
                balance = client.get_balance(account)
                currency = getattr(balance, "currency", "EUR")
                print(f"- {account.iban}: {balance.amount} {currency}")
            except Exception as e:
                print(f"- {account.iban}: failed to fetch balance: {e}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions