Skip to main content
Every market role publishes its communication data to its partners as a PARTIN (Partnerstammdaten). For the BKV role that is application case 37003 “Kommunikationsdaten des BKV Strom” (PARTIN AHB ch. 5.2), sent BKV → NB, BIKO and ÜNB — the mirror image of the datasheets those partners send us.

What the message carries

The configured company datasheet: name and address (NAD+Z35 — Z35 is the BKV role code), bank details (FII+BK), website, VAT ID, weekday service hours (Monday–Friday all mandatory, German legal time), and contact blocks for Übertragungsweg/Datenaustausch (Z10) and Bilanzierungsprozesse/Bilanzkreismanagement (Z21). Towards the ÜNB a Rahmenverträge contact (Z11) is added, as the AHB requires only there. Two AHB rules worth knowing:
  • Contacts name an organisational unit, never a person (rule [501]) — “Marktkommunikation”, not a colleague’s name.
  • The message is versioned (RFF+AGK, starting at 1). A changed datasheet must be re-sent with the version incremented; the receiver treats the higher version as the update.

Format version

Since the 2026-10 market release the bridge sends PARTIN 1.1 (UNH …+PARTIN:D:20B:UN:1.1, MIG/AHB 1.1 valid from 2026-10-01). For application case 37003 one structural change came with it: the sender contact (CTA/COM right after NAD+MS) no longer exists and is not written. Partners on 1.1 reject a 1.0f message with CONTRL error 12 on UNH S009 (the version). PARTIN_MESSAGE_VERSION can still be set back to 1.0f for a partner-side comparison; the builder then restores the old sender contact.

Sending

The console has a PARTIN panel: pick the receiver role (NB from the BDEW dropdown; BIKO/ÜNB by MP-ID), set the version, Vorschau shows the exact interchange, Senden delivers after a confirmation. Over the API:
receiver_role is NB (default), BIKO or UNB; it selects the role-dependent blocks. Every send lands in the audit trail with the exact interchange and emits a partin_sent event.

Answering incoming PARTINs

Partners send their datasheets to us too. The bridge scans the MakoFlow inbox (GET /edifact/received-files, filtered to PARTIN and to the BKV MP-ID as recipient — the similarly named received-messages endpoint stays empty for this tenant) and answers each sender with our 37003 — once per sender and datasheet version. The sender’s role is read from its message’s Prüfidentifikator: 37001 → NB, 37004 → BIKO, 37005 → ÜNB. Senders whose role has no BKV application case (LF, MSB, ESA) are skipped and listed with the reason. In the console: Prüfen (dry run — reports without sending) and Prüfen & antworten. Over the API:
The ledger of answered partners lives in the audit trail (partin_replies.json). Bumping PARTIN_DATASHEET_VERSION invalidates it on purpose: a changed datasheet goes out to everyone again with the higher RFF+AGK version.

Resending after a rejection

A negative CONTRL/APERAK names the refused interchange in RFF+ACE; once the cause is fixed (a builder correction on our side, a directory update on theirs) the current datasheet can go out again:
The bulk variant binds each rejection to the referenced outgoing message via RFF+ACE looked up in the MakoFlow outbox: PARTIN-bound rejections (and failed AS4 deliveries) are resent, UTILMD-bound ones are skipped with a pointer to the Zuordnung panel. A resend refreshes the reply ledger, so the answer loop stays quiet afterwards. In the console: the ↻ button on a rejected row, and “Abgelehnte ↻” in the Marktpartner header (dry-run preview, then confirmation). Deliberately manual — resending unchanged content into an unchanged validator only loops the rejection. Every send checks its content-derived interchange reference against the MakoFlow outbox: a deliberate re-send of unchanged content goes out as a new file (attempt counter in the response) instead of repeating the reference — partner gateways answer a repeated reference with CONTRL error 26 (duplicate transmission file) rather than deduplicating it. The same guard applies to the Zuordnungsermächtigung. Timer: with AUTO_ANSWER_PARTIN=true (console-mutable, off by default) the reply loop runs on every orchestrator call — every 15 minutes. Without MakoFlow credentials it skips silently rather than producing a 15-minute error stream; a real failure lands as a partin_answer_failed error event and reaches the alerting channel.

Marktpartner overview

GET /v1/process/market-partners/overview?lookback_days=90 derives a communication status for every Netzbetreiber in the BDEW registry, grouped by control area. Five facts per partner, each with the latest date, read live from MakoFlow: PARTIN sent, AS4 delivery receipt (NRR, from /transmissionReport/as4 scoped to our sender — an entry without as4Error means the counterpart returned its signed receipt), acknowledgment (CONTRL/APERAK) — classified positive or negative: a CONTRL whose UCI action code is 4 or an APERAK with BGM+313/ERC is a rejection and carries the error text — PARTIN received, and Zuordnungsermächtigung (UTILMD) sent. A CONTRL names its error where the partner put it. An envelope error sits in the UCI and reads CONTRL abgelehnt (Syntaxfehler 23). A message error leaves the UCI without a code; the bridge then reads the UCM (or its UCS/UCD) and names code, message type, version and position, e.g. CONTRL abgelehnt (Syntaxfehler 12 in PARTIN 1.0f, UNH 3:5) — the message version a partner on the new format refused. Each row also carries letzter_kontakt_at and letzter_kontakt: the newest sign of life on either channel, in either direction — one of the EDIFACT dates above, or the latest e-mail from the campaign ledgers (Erstkontakt, Quittungs-Nachfass, Korrektur, Stammdaten, Auskunft, hand-sent mails) and the inbound evidence (a reply that moved the Erstkontakt status, a Wiedervorlage). A bounce is not a contact. Timestamps from the different sources carry different offsets, so the newest is decided on instants. ?sort=kontakt orders every control area (and other_partners) by that date, newest first, partners without any contact last; the default sort=status keeps the red-first-then-name order the console and the sheet script rely on. Acknowledgments are bound to the message they answer via the interchange reference (RFF+ACE in APERAK, the UCI reference in CONTRL) looked up in the MakoFlow outbox — every reference in the file, since one APERAK interchange may carry many messages answering several of our interchanges (Westfalen Weser Netz acknowledged the PARTIN and both Zuordnungen in one file of nine messages) — and tracked per stream: the PARTIN answer pair and the Zuordnungsermächtigung answer pair are independent — a partner can confirm the Zuordnung while rejecting the PARTIN (Blomberg Netz did), and neither verdict hides the other. Acks whose reference lies outside the window fold into the partner’s dominant stream. The derived status: rejected when a rejection stands unanswered inside its own stream — a negative acknowledgment not followed by a newer positive one in the same stream (for PARTIN, the partner’s own PARTIN also counts), or an AS4 transport error not followed by a newer successful delivery. Timestamp ties surface the rejection. Else zugeordnet when the Zuordnungsermächtigung itself is positively acknowledged — the deliverable, and the number the rollout is steered by; complete when the channel works and the Zuordnung went out but is not yet acknowledged; then established / pending / none as before. Partners with traffic but no registry entry — BIKO, ÜNB, test partners — are listed separately; DSOs the BDEW list registers with a GLN instead of a BDEW code (Netze BW, Westnetz, MITNETZ, …) are regular registry entries and match under their GLN. In the console this is the Marktpartner tab: status chips (rejections first, in red), per-area lists with flag badges (P→ NRR Q ←P Z→ NRR-Z QZ — Q answers the PARTIN, QZ the Zuordnungsermächtigung, NRR-Z is the AS4 receipt for the latest Zuordnungsermächtigung — bound exactly when the transport row names the file, otherwise counted in the 24 h after the send, the tooltip says which; a badge turns red when its latest signal is negative, with the error reason in the tooltip), a name/MP-ID filter, and partners without any activity hidden unless toggled on.

Per-DSO BRP activation status

GET /v1/process/market-partners/{key}/brp-status?lookback_days=90 answers for a single partner what the overview answers for all of them — made for another system to poll one DSO. {key} is either the Netzbetreiber’s MP-ID or any of its Bilanzierungsgebiet (MGA) EICs; the response says which one matched (queried.resolved_by). It carries the same flags and the same derived status as the overview — grouped as erstkontakt, partin, zuordnung and nrr blocks — plus a bilanzierungsgebiete list with, per MGA, the Zuordnungsermächtigung the automatic loop sent (sent_at, valid_from, zeitreihentypen; null where none went out yet). Scope report suffices — the endpoint reads, it cannot trigger anything. A key that is neither a registry partner nor a partner with traffic in the lookback window is a 404; a MakoFlow failure is a 502.

PARTIN data per market party

GET /v1/process/market-partners/{key}/partin?lookback_days=90 returns the PARTIN exchange with one party, parsed for humans: every PARTIN the partner sent us in the window (application case, datasheet version, company block with address, VAT id, website, and all contact blocks with names, e-mail addresses and phone numbers — the same addresses the Korrektur mails write to), plus the outbox entries of our own PARTINs to them. {key} is the MP-ID or any of the partner’s MGA EICs, as in brp-status; fields the partner did not fill are empty strings. Scope report suffices. The parser honors the EDIFACT release character and is exercised round-trip against our own PARTIN builder.

Korrektur mails for mixed verdicts

A partner can accept one message and reject the other — PARTIN accepted but the Zuordnungsermächtigung rejected, or the reverse (the Blomberg pattern). That is a partner-side processing issue no resend fixes. POST /v1/process/market-partners/korrektur-mails writes each such partner a mail explaining the split verdict — what stands accepted, what was rejected when, and their own rejection reason — and asks them to correct it or say what we should change. The mail goes to the contact address from their own PARTIN (COM+…:EM), deliberately not to the BDEW list address: the PARTIN names the working Marktkommunikation desk. Partners without a PARTIN of their own are reported as skipped, never silently dropped — unless the call sets bdew_fallback: true, an explicit operator decision to write to the BDEW code-list address instead (kontakt_quelle says which address was used); mp_ids restricts a run to named partners. One mail per standing rejection (korrektur_mails ledger); a newer rejection triggers a new mail. dry_run lists targets and addresses without sending; a failed send is reported (korrektur_mail_failed) and retried on the next call.

Quittungs-Nachfass for silent partners

A message can be delivered (the AS4 transport receipt, NRR, confirms it) and still never be acknowledged. Since 01.10.2025 a CONTRL is only sent for a syntactically broken file (GPKE Teil 1 Kap. 4 b, read with the BDEW Umsetzungsfragenkatalog GPKE_C027); a clean file gets no CONTRL, and every Geschäftsvorfall in it must be answered by APERAK — by the next Werktag 12:00 German legal time, and for UTILMD (our Zuordnungsermächtigung) within 45 minutes (APERAK AHB Kap. 2.4.1). The chase therefore waits for the APERAK alone; the mail names the rule and says that no CONTRL syntax rejection arrived either. POST /v1/process/market-partners/quittungs-nachfass mails every partner whose delivered PARTIN or Zuordnungsermächtigung is past its deadline plus the configured Karenz (QUITTUNG_NACHFASS_KARENZ_STUNDEN, default 24 h — it also absorbs holidays, which the Werktag arithmetic does not model) without any acknowledgment in either direction. The mail names each outstanding message with its delivery time and deadline, cites the AHB rules, says how long the oldest of those deadlines has been up, and offers to transmit the message again if the partner’s processing fails on master data they still have to create — the two lines the hand-written round of 21.09.2026 added over the template, because “we never got it” and “we cannot process it yet” need different answers. The age is given in calendar days, not Werktage: the federal holiday calendar is not modelled, and a figure overstated towards a partner is the one direction that must not happen. The mail goes to the contact address from their own PARTIN (received via AS4 or as a mail attachment). Partners who never sent a PARTIN are reported as skipped, not mailed — without one there is no EDIFACT desk on record — unless the call sets bdew_fallback: true, an explicit operator choice to write to the BDEW list contact instead (kontakt_quelle tells which address was used). mindestalter_tage adds a floor in days since the send on top of the deadline, so a run can leave the messages of the last few days (an Ergänzung wave, say) to their own time. One mail per partner covers all outstanding streams; the quittung_nachfass ledger keys (partner, stream, sent_at), so repeated silence after a chase is a human follow-up, never a mail loop. dry_run lists targets first; limit caps mails per run. A PARTIN whose partner has already accepted the Zuordnungsermächtigung is not chased: the Zuordnung is the deliverable, the partner has proven they know us, and the chase only draws “but we did send an APERAK” replies pointing at the UTILMD’s acknowledgment. Replies cross-checked (GET /v1/process/mail/antworten-abgleich?lookback_days=14): partners answer a Nachfass or Korrektur mail with the acknowledgment they did send (“per Anerkennungsmeldung bestätigt: APERAK__…_D0000000667948.txt”), and it usually answers the other message. The endpoint reads every such reply, extracts file names and DAR-like references, checks each against our inbox (found or not, which of our messages it acknowledges, verdict), lists the partner’s streams still open, and gives a one-line assessment: “zitierte Quittung liegt vor und beantwortet: zuordnung; offen bleibt: partin”, or “zitierte Quittung nicht in unserem Posteingang” (the partner’s AS4 route to us failed — the Itzehoe pattern). Each entry carries the reply’s own text (text, quoted history stripped, 600 characters), so the replies that name no reference can be read from the list. Read-only; the morning triage lists it and a person decides whether a pointer reply is worth sending. Google-Sheet sync: scripts/google_sheet_status.gs is an Apps Script for the Cloover DSO tracking sheet — it pulls the overview with a read-only REPORT_API_KEY (scope report: it can trigger nothing) and fills the status column, colored by status, with the per-partner dates and rejection reasons as a cell note. A time-driven trigger keeps it current; the setup steps are in the script header.

Finding a message, or a partner’s whole exchange

Partners cite references when they refer to a message — “die Kontaktdaten entnehmen Sie bitte der Partin (DAR: C3AAAAAAAJWFJR)”, “wir haben folgende CONTRL an Sie versendet”. GET /v1/process/edifact/suche answers both kinds of question:
  • ?referenz=<ref> matches the Datenaustauschreferenz (UNB 0020), the message reference (UNH 0062) and, for a CONTRL or APERAK, the interchange it answers (UCI / RFF+ACE) — so the reference from a partner’s file name finds the acknowledgment, and our own message’s DAR finds every acknowledgment it got.
  • ?partner=<MP-ID> lists everything exchanged with that partner, both directions, each acknowledgment with bezug (the message it answers), verdict (positiv/negativ) and grund — the fastest way to check a “we did send a CONTRL” against what actually arrived. Combine both to narrow.
  • ?alles=true returns the whole corpus instead of a search. That is what an evaluation over all partners needs — how long an acknowledgment took, and where none ever came. Add ?kompakt=true to leave out the edifact bodies, which are most of the answer’s size; everything the evaluation reads (date, edi_type, bezug, verdict, grund) stays.

Everything that went out under our MP-ID

GET /v1/process/makoflow/uebertragungen?lookback_hours=48 lists the AS4 transport log under our BKV ID, one row per delivery attempt: time, recipient (with name and market role where the BDEW lists know it), the file name and the message type its first part names, the transport error, and bekannt — whether the file is one the bridge itself sent (null when the row names no reference). ?nur_fremde=true keeps only files the bridge did not send; ?empfaenger=<MP-ID> narrows to one recipient. This is how traffic under our MP-ID from elsewhere — a platform broadcast, a co-tenant of the shared gateway account — shows up by name. Read-only; a report key is enough. The gateway’s own AS4 connectivity tests (as4Service/as4Action TEST, a 132-byte dummy payload) are flagged verbindungstest: true and typed AS4-TEST. They carry no market message, so the partner overview and the acknowledgment chase ignore them: a failed ping is no rejection of ours and a successful one no receipt for our PARTIN. After the 2026-10 format switch the gateway pinged 202 partners once each under our certificate; without this rule 74 BKVs reached only by that ping showed up as “other partners”, 27 of them rejected. The deadlines such an evaluation measures against are, for Sparte Strom: a CONTRL is sent only on a syntax error, within 6 hours, and within 15 minutes for a UTILMD or ORDERS (CONTRL AHB 2.4.1); an APERAK is owed for every Übertragungsdatei by noon of the next Werktag, and within 45 minutes for a UTILMD or ORDERS (APERAK AHB 2.4.1). A PARTIN therefore falls under the noon rule, a Zuordnungsermächtigung under the 45-minute one. The search covers the inbox of our market parties, our own sent files and PARTINs that arrived as mail attachments (case-insensitive); durchsucht says how many files were scanned. A PARTIN hit surfaces the contact address it carries. lookback_days (default 180) widens or narrows the window; MakoFlow refuses very wide windows on the outbox, so stay at or below 180.

Configuration

The datasheet lives in PARTIN_COMPANY (JSON, console-mutable), prefilled from the Kontaktdatenblatt Cloover Energy GmbH: address Hussitenstraße 32, 13355 Berlin, USt-ID, Deutsche Bank IBAN/BIC, contact mako@cloover.com / +4915129429989 under the unit “Marktkommunikation”.
Two fields are not in the datasheet and use defaults that need confirming before the first send: the service hours (Mo–Do 09:00–17:00, Fr 09:00–15:00) and the Handelsregister entry (omitted until court and registry_number are set). The sender is BKV_MP_ID = 9985803000004, as stated on the datasheet.

Erstkontakt per E-Mail

The “Regelungen zum Übertragungsweg” (Kap. 2) require a plain first contact over the address published in the BDEW code database before EDIFACT traffic starts. POST /v1/process/erstkontakt mails it — body from the packaged template, the Kontaktdatenblatt as XLSX attachment — to every registry Netzbetreiber with no market traffic and no prior Erstkontakt (dry_run lists the recipients first; mp_ids targets explicitly; capped per run by ERSTKONTAKT_MAX_PER_RUN). A ledger guarantees one mail per partner, ever; mails sent outside the bridge are imported via POST /v1/process/erstkontakt/import so the tracking is complete. The one way around the ledger is a hand-picked address: {"mp_ids": ["<mp-id>"], "email": "<address>"} re-sends the Erstkontakt to that address — the partner’s standard MaKo address from its own Kontaktdatenblatt once the BDEW-listed contact bounced — and keeps the earlier attempt in the ledger entry under vorher. Contacted partners show as angeschrieben in the Marktpartner overview until real traffic starts. POST /v1/process/mail/check tests the mailbox (“Mail testen” in the console config drawer) without sending anything — including the IMAP side and its folder names when configured. Mailbox reconciliation (POST /v1/process/erstkontakt/mailbox, “Postfach abgleichen” in the console; dry_run first): reads INBOX, Archive and Sent over IMAP. Erstkontakt mails sent by hand from the same mailbox are adopted into the ledger with their real send date. Bounces (“unzustellbar”) become Wiedervorlagen; an Abwesenheitsnotiz naming a Vertreter gets the original mail (with Kontaktdatenblatt) forwarded to that address — but only while the Erstkontakt is still outstanding. A note that answers a Quittungs-Nachfass or Korrektur mail, or comes from a partner already past Erstkontakt, is filed as a Wiedervorlage due the day the person is back (faellig_am, read from the note); nothing is forwarded, the answered mail is already in their inbox. A note without a Vertreter becomes a Wiedervorlage too. A reply confirming our Stammdaten (“Ihre Marktrolle wurde angelegt …”) starts the market process: PARTIN goes out if ours never did, then the standing Zuordnungsermächtigung selection for every Bilanzierungsgebiet (recorded in the zoe_auto ledger, so the timer never doubles a grant). Handled inbox messages are filed into the archive folder (MAILBOX_ARCHIVE_PROCESSED). Ordinary replies are only marked answered and stay in the inbox for a human. Every processed message is remembered, so reruns are idempotent. The open list: GET /v1/process/erstkontakt/wiedervorlagen — and in the console under the WV tab (count badge, filter, newest first, with the Beleg sentence and kandidaten hints per entry). A handled entry is closed with the tab’s Erledigt button (DELETE /v1/process/erstkontakt/wiedervorlagen/{key}); closing is final for that occurrence only — the reconciliation files a fresh entry if the same situation arises again. Nachversand after registration bounces: a DSO that rejects our PARTIN or Zuordnungsermächtigung with CONTRL error 23 / APERAK Z29 (“Absender unbekannt”) usually just hadn’t registered us yet — often their own PARTIN or a positive acknowledgment arrives right after. POST /v1/process/market-partners/nachversand (dry_run first; mp_ids restricts the sweep to the partners named; anfrage: true with mp_ids resends the Zuordnung to those partners even without a standing rejection — they asked for it in writing because their system failed to process it, and the resend carries the current standing selection and updates the zoe_auto ledger so the Ergänzung sweep does not double up) re-delivers the bounced message with a fresh interchange reference, once per partner and stream, ever (ledger), and only once positive evidence newer than the rejection proves the partner knows us — evidence that speaks for the rejected stream: for a bounced PARTIN their own PARTIN or a positive PARTIN acknowledgment (a positive Zuordnung ack does not count — many partners accept the UTILMD while their PARTIN validator keeps answering Z29, which is a Korrektur-mail case); for a bounced Zuordnung any of the three. CONTRL error 26 (duplicate transmission) re-sends immediately, since that fix is ours. Business rejections (Z18 etc.) are never re-sent unchanged — they stay with the Korrektur mails. If a re-sent message bounces again, the new rejection is skipped as a case for a human, whatever it says and whatever evidence follows; the sweep never loops. Acknowledgments that arrive by mail: when a partner forwards a CONTRL/APERAK that never reached our AS4 inbox, POST /v1/process/edifact/quittung-import (edifact) takes it in: bound to the message it answers (UCI / RFF+ACE) like an inbox acknowledgment, so the partner book shows the stream acknowledged, a Nachversand pre-listing it answers is settled on the next sweep, and the search lists it as a mail attachment. Keyed by its Datenaustauschreferenz — re-importing is a no-op. A confirmation given only as prose (“wir bestätigen hiermit bilateral den Empfang der PARTIN, die Daten sind hinterlegt”) goes in through POST /v1/process/edifact/quittung-manuell (mp_id, bezug = the DAR of our message, verdict, grund, optional datum): stored as a synthesized APERAK with the mail’s gist as reason text and the source marked as manually recorded, so the book treats it like any acknowledgment. Pre-listing by hand: when the rejection evidence arrives outside AS4 — a partner forwards the CONTRL that never reached our inbox — the book cannot see it. PUT /v1/process/nachversand/vormerkungen/{mp_id} with stream and reason lists the partner for the sweep as if the rejection stood in the book, dated at the pre-listing; a registration-class reason then waits for newer evidence that the partner knows us, re-delivers once and clears the pre-listing (GET lists, DELETE removes). Written registration evidence: the mirror case. A registration-class rejection (CONTRL 23 / APERAK Z29) is re-delivered only once newer AS4 evidence shows the partner accepts us — their own PARTIN, or a positive acknowledgment. A partner who fixes their master data and simply writes (“wir haben Sie erst am 26.08. bei uns aufgebaut, bitte schicken Sie die Nachricht neu raus”) sends no such message, so the sweep would skip them forever. PUT /v1/process/nachversand/belege/{mp_id} with reason — the partner’s own words — and stream (partin, zuordnung or beide, the default) records their statement as the evidence it stands in for, dated now; the sweep then re-delivers that stream once with a fresh Datenaustauschreferenz (GET /v1/process/nachversand/belege lists, DELETE withdraws). The entry stays after the resend: should the same stream be rejected again later, the newer rejection outdates the evidence and the sweep waits again, as it should. Hand-written replies to such partners go out from the Erstkontakt mailbox via POST /v1/process/mail/senden (to, subject, body, mp_id) — put [Ref. <MP-ID>] in the subject so the mailbox reconciliation attributes the answer; every such mail is kept in the manuelle_mails ledger. “We cannot find your message” (nachversand_gewuenscht): a recurring answer deserves its own reading. Stadtwerke Forchheim, 10.09.2026: “Wir können leider keine UTILMD in unserem System finden. Könnten Sie uns bitte diese erneut übermitteln?” — the resend was acknowledged fourteen minutes later. Stadtwerke Wachenheim wrote the same thing the next morning. Read as a plain Rückfrage, such a request drowns among a hundred others. The classifier labels it, and the reconciliation files a Wiedervorlage that names the instrument: “Nachversand erbeten - Partner findet unsere Nachricht nicht”. Nothing is resent by itself. A resend is a real market message with a fresh Datenaustauschreferenz, and whether the partner’s house simply lost it or never had the market partner set up is a judgement the book cannot make — so the run names the answer and a human calls POST /v1/process/market-partners/nachversand with anfrage: true. Answering a question the book already answers (auskunft): a large share of the replies to a Quittungs-Nachfass ask something we can look up — “leider finden wir keine Nachricht mit Ihren angegebenen Daten”, or “unsere Anerkennungsmeldung haben wir bereits übermittelt: DAR …”. POST /v1/process/mail/auskunft (dry_run, limit, mp_ids) walks the open Wiedervorlagen of kind Rückfrage and answers each from the exchange itself: the Datenaustauschreferenzen we sent with their Prüfidentifikator and timestamp, whether the acknowledgment the partner cites is in our inbox, and what is still unanswered. Every figure comes from edifact_suche, so nothing is asserted that cannot be shown; the mail asks no business question and makes no claim about their systems. One reply per Wiedervorlage, ever (ledger auskunft_antwort). Deliberately skipped: a partner with a standing rejection. A CONTRL 23 or an APERAK Z29 needs a master-data request and a human decision, not an information reply. Exclusions, Zurückstellungen and partners whose MP-ID cannot be resolved are skipped too, and the overriding contact address above wins over the address the reply came from. Asking a partner to create us (stammdaten): a CONTRL with syntax error 23 (Unbekannter Absender der Übertragungsdatei) and an APERAK Z29 (Segment SG2/NAD, MP-ID Absender) say one and the same thing — our MP-ID is not registered at the partner. The Erstkontakt asks for the setup once, in a subordinate clause, and never again, so a rejection that arrives afterwards is answered by nothing at all. POST /v1/process/mail/stammdaten (dry_run, limit, mp_ids, lookback_days) writes the request that names it: the rejected interchanges with their Datenaustauschreferenz, the error code and the date, both of our MP-IDs with their roles (BKV and LF), the explicit ask to open the sender for the Zuordnungsprozess as well (UTILMD 55071/55072 — at several partners the record existed after the setup while the system behind the Zuordnungsermächtigung still rejected the same sender ID as unknown, Mühlheim and Neuffen on 29./30.09.2026), and the deadline from Regelungen zum Übertragungsweg für AS4, Kapitel 2 — the data are to be exchanged within three Werktage of the first contact and set up in all systems within three further Werktage, an obligation that attaches to the contact and not to an existing supply relationship. The mail closes on § 20a EnWG: with the Übertragungsweg missing, the 24-hour Lieferantenwechsel cannot be met by either side. It asks for nothing beyond the setup and promises the resend. Only those two rejection classes are addressed. A Z18 (Zuordnung nicht plausibel) means the partner knows us perfectly well and needs a human; a CONTRL 26 (duplicate Datenaustauschreferenz) is ours to fix by resending. Exclusions and Zurückstellungen are skipped — a partner who told us they create market partners only once a supply exists has answered this request already, in the negative. One request per partner (ledger stammdaten_anfrage); a second round is a human decision. Overriding the contact address: the BDEW Marktpartnerliste often names a person, while the house publishes functional mailboxes on its Kommunikationsdatenblatt — so a partner chased at the list address rightly objects (Greifswald, 09.09.2026: “bitte wenden Sie sich ausschließlich an Mailadressen, welche wir auf unseren Kommunikationsdatenblättern veröffentlichen”). Without somewhere to keep the correction, the next automatic round writes to the person again. PUT /v1/process/market-partners/{mp_id}/kontakt with email and reason (where the address comes from) records it; Erstkontakt, Erinnerung, Quittungs-Nachfass and Korrektur mail use it from then on, ahead of the BDEW contact and of the desk named in the partner’s own PARTIN — it is what the partner asked for, in writing. The kontakt_quelle field on a target says which source won: manuell (a one-off address passed on the call), hinterlegt / manuell (this override), partin, or bdew. GET /v1/process/market-partners/kontakte lists, DELETE drops one. A deliberate second round: one message, one reminder — the ledger keys (partner, stream, send) so nobody gets the same letter week after week. That same rule froze out the 75 contract partners whose chase had gone out before the framework contract was on record: a better letter existed, and the run would not send it. mp_ids narrows a run to named partners, and wiederholen: true lets those be chased again for the ledgered message. It requires mp_ids and is refused without them (422) — a blanket repeat is exactly the mail loop the rule exists to prevent. The ledger keeps a runde count, so the book shows how often one message was chased. A repeat also skips every partner with an open Wiedervorlage, whatever its kind: that partner is in a conversation with a human, and a machine chasing in parallel talks past it. Stadtwerke Oranienburg had answered on 04.09.2026 that their APERAK had gone out; the entry was open, and the repeat of 14.09. asked for the acknowledgment once more. A first round is not affected — an old absence notice must not block the first chase. Lieferantenrahmenvertrag: a signed framework contract changes what a chase may assume. The partner is not a stranger who might reasonably ask who we are — the two houses have a contract, and the acknowledgment we are missing belongs to a relationship they entered deliberately. Of the 860 Netzbetreiber in the VNB campaign, 570 hold such a contract, and 78 of those owed us an acknowledgment on 11.09.2026. PUT /v1/process/market-partners/{mp_id}/rahmenvertrag with reason (where the contract is recorded) files the fact; the Quittungs-Nachfass then names it and asks, in the same breath, for the master-data setup should it still be missing. GET /v1/process/market-partners/rahmenvertraege lists, DELETE withdraws — and a withdrawn contract is never claimed again. It is a standing fact about the relationship, not a phrase in one mail, which is why it lives in its own ledger rather than in a run’s parameters: the Nachversand can lean on the same entry as evidence that the partner knows us, recorded once. Manual exclusions (ausgenommen): some registry DSOs need no German BRP process at all — the typical case is an exterritorial grid connected to a foreign control block (Austria), where the supplier delivers but no German BKV is required. PUT /v1/process/market-partners/{key}/ausnahme (body: {"reason": "…"}, key = MP-ID or MGA EIC) excludes the partner from every automatic step: Erstkontakt, Erinnerung, the PARTIN/ZOE fan-out after a mail confirmation, the auto-Zuordnung, the PARTIN auto-answer and the Korrektur mails. The overview shows the row as ausgenommen with the reason (own totals bucket, sorted last); GET /market-partners/ausnahmen lists all standing exclusions; DELETE …/{key}/ausnahme lifts one, and the automation covers the partner again from the next run. Explicit manual sends (POST /partin, POST /zuordnungsermaechtigungen) stay possible — the exclusion binds the automation, not the operator. Deferrals (zurueckstellung): a weaker instrument for the partner that is willing but not yet — “we register market partners only once a supply is imminent and a Lieferantenrahmenvertrag exists” is a common answer from small grids. PUT /v1/process/market-partners/{key}/zurueckstellung (body: {"reason": "…"}) stops only the Quittungs-Nachfass from chasing that partner; nothing is excluded, PARTIN and Zuordnung stay sent, and the overview keeps showing the real status rather than ausgenommen, so the partner reappears in the funnel the moment it acknowledges. The chase reports it under skipped with zurückgestellt: <the partner's own words>. GET /v1/process/market-partners/zurueckstellungen lists them; DELETE …/{key}/zurueckstellung lifts one — do that when a customer moves into that grid. Choose an exclusion when the partner never needs a German BKV, a deferral when the answer is “not yet”. PARTIN as a mail attachment: some partners answer the Erstkontakt with their PARTIN as a file instead of (or before) the AS4 message. The reconciliation detects EDIFACT PARTIN attachments, validates them (the message must be addressed to one of our MP-IDs — a forwarded foreign PARTIN is left alone), and imports them into the partin_email ledger; files inside already-handled mails are picked up too. Imported PARTINs appear in GET /market-partners/{key}/partin with via: "email" and provide the Korrektur-mail contact address when no AS4 PARTIN carries one (the AS4 message stays authoritative when both exist). The attached PARTIN’s own envelope is also the strongest attribution evidence for the carrying mail — ahead of the [Ref. …] token and the sender address. Reply attribution — the [Ref. …] token: every outgoing partner mail (Erstkontakt, Erinnerung, Vertreter forwarding, Korrektur) carries the recipient’s MP-ID as [Ref. 9901234000005] in the subject and at the end of the body. A reply that keeps the subject (or embeds our mail, as ticket systems do) is attributed by that reference — it wins over sender-address matching, because one shared MaKo desk answers for several market parties from a single address (the Syna case). Safety gates stay on: a reference naming an unknown MP-ID is ignored, and a text quoting several different references counts as no reference — those replies fall back to address matching and, failing that, become Wiedervorlagen with kandidaten hints. Replies to mails sent before the token existed keep working via address matching. Handled inbox mails that the gates left for a human can be filed afterwards with POST /v1/process/erstkontakt/mailbox/ablegen (by sender and/or IMAP UID; dry_run lists the matches). quelle and ziel name the folders by role — inbox, wiedervorlage, zugeordnet, archiv, gesendet — and default to inbox → archiv; mails whose Wiedervorlage is done go wiedervorlage → zugeordnet (partner attributed) or → archiv the same way. Outgoing mail is filed too. SMTP submission and IMAP storage are two separate operations: a mail client sends over the one and files a copy over the other, and for a long time the bridge only did the first. The Sent folder stayed empty, so the mailbox showed the partners’ replies but not what they were answering — and anyone else looking in saw half a conversation. Every outgoing mail now goes through one path that sends and then appends the exact bytes that left the house to IMAP_FOLDER_SENT (default Sent), flagged \Seen. That covers the hand-written mails as well as the automatic ones: Erstkontakt, reminders, Quittungs-Nachfass, Korrektur and Stammdatenanfrage. The filing never fails the send — at that point the mail is already delivered, so a failure emits a mail_ablage_fehlgeschlagen warning naming the recipient and the folder, and the correspondence stays readable in the manuelle_mails ledger of the audit trail. An installation without an IMAP host files nothing and warns about nothing. Closing with filing: DELETE /v1/process/erstkontakt/wiedervorlagen/{key} removes the entry from the open list; with ?ablage=zugeordnet or ?ablage=archiv it also moves the mail behind the entry (same sender, the entry’s subject) out of the Wiedervorlage folder, so that folder keeps saying what is still open. A failed move does not undo the close — the result carries ablage_fehler instead of abgelegt. Forwarding an attachment: GET /v1/process/erstkontakt/mailbox/peek lists a folder’s newest mails with their IMAP uid, subject, body snippet and attachment names — the uid is what addresses a single mail below. folder takes either a role (inbox, wiedervorlage, zugeordnet, archiv, gesendet) or the IMAP folder name itself, so the same word works here as in the filing calls. POST /v1/process/mail/senden takes anhaenge — {folder, uid, name} per file, the folder by role — and sends attachments of mails already in the mailbox along with the reply. A provider’s AS4 error response reaches the network operator whose endpoint produced it in that operator’s own words, rather than retyped. Only textual payloads travel: the mailbox keeps an attachment’s text, not its bytes, so a binary file is refused (422) instead of arriving empty. Outgoing attachments now carry the media type their filename implies; the Kontaktdatenblatt stays an xlsx. Reading a Wiedervorlage in full: the entry keeps sender, subject and a 220-character Beleg; the peek shows 400 characters of body. Neither is enough to decide a case — Stadtwerke Strausberg’s explanation of their Z29 broke off mid-sentence at exactly the point that mattered. GET /v1/process/erstkontakt/wiedervorlagen/{key} returns the entry plus the newest mail from its sender with its subject, wherever the filing has put it: full text, every textual part, attachment names. Nothing is moved or closed by reading. Filing by outcome: everything the reconciliation handled used to land in one archive, so the mailbox said nothing about what was left to do. POST /v1/process/erstkontakt/mailbox/sortieren (dry_run, limit, lookback_days) files each handled mail where its case stands today: a mail whose Wiedervorlage is still open goes to IMAP_FOLDER_WIEDERVORLAGE (default Wiedervorlage), a mail from a partner whose PARTIN and Zuordnung are both acknowledged goes to IMAP_FOLDER_ZUGEORDNET (default Zugeordnet), and everything else stays in the archive. Because the rule reads the current state rather than a stored decision, one sweep both files new mail and corrects old: a Wiedervorlage answered by the Auskunft leaves the work folder on the next run, without anyone tracking IMAP UIDs across a move. lookback_days: 0 walks the whole mailbox — the one-off run over the backlog; a window keeps a routine sweep cheap. The orchestrator runs the sweep on every pass, right after the reconciliation: a mail handled at 09:15 is in its folder at 09:15, and a Wiedervorlage closed at 09:15 leaves the work folder in the same minute. The routine window is short on purpose — MAILBOX_SORTIEREN_TAGE (default 3) — because the sweep reads every message in it: three days cost about ten seconds, the whole mailbox a minute and a half, which is no business for a 15-minute timer. AUTO_MAILBOX_SORTIEREN=false switches the automatic sweep off without touching the endpoint. A bare receipt - “wir haben die Datei erhalten, bitte sehen Sie diese Mail als Eingangsbestätigung an” - is filed as soon as the sender can be attributed to a partner, even when it changes no state. Saalfelder Energienetze sent exactly that on 11.09.2026 for a partner who had confirmed the Erstkontakt weeks earlier: nothing to record, so the reconciliation left it in the inbox, where the sweep may not touch it. A receipt from a partner we can name holds nothing for a human to decide. An unattributable sender still waits in the inbox. Two guards. Unhandled inbox mail is never moved: the reconciliation deliberately leaves replies it could not attribute where a human sees them, and filing those away would hide the very thing that needs a person. And the reconciliation reads the outcome folders as well as Inbox and Archive — otherwise a partner’s answer would drop out of sight the moment it was filed, and the next run would treat them as never having replied. A folder that cannot be written is reported and skipped, never raised: filing is a courtesy, processing is the duty. Missing folders show up in POST /v1/process/mail/check as imap.missing_folders. With AUTO_MAILBOX=true (default) the reconciliation rides on the 15-minute orchestrator once IMAP_HOST is configured. The per-partner e-mail state shows as the @ badge in the Marktpartner overview: @ gesendet, @… geantwortet ohne Prozess, @✓ bestätigt (PARTIN/ZOE ausgelöst), @↷ an Vertreter weitergeleitet, @! unzustellbar/ Wiedervorlage. Reminder: the “Regelungen zum Übertragungsweg” (Kap. 2.1) require the communication parameters to be exchanged within three Werktage of the first contact (and entered in all systems three Werktage after that). A partner with no reply and no market traffic three full Werktage (Mon–Fri; holidays not modelled) after the Erstkontakt gets one reminder — to the Vertreter if the contact was forwarded, otherwise to the original address — with the Kontaktdatenblatt attached and reminded_at recorded.