Files
myep/docs/bpn-xml-api-reference.md
T

20 KiB
Raw Blame History

BusProNet XML API — working reference

Condensed from the vendor PDF docs/bpn_docs.pdf (41 pages, German), keeping what this project uses or plausibly could. The PDF stays authoritative; a few of its sentences are mangled by a broken font encoding, so anything unclear there is flagged rather than guessed at.

The PDF lags the live API. The vendor has shipped elements since it was written — <ausstiege> (return boarding points) is one confirmed example. So absent from the PDF means "not in this snapshot", never "unsupported". Treat the document as a floor on what the API accepts, not a ceiling, and prefer a live buchungsart=Anfrage probe over an argument from silence.

Read this alongside buspro-database-reference.md (what BusPro actually stores) — the two answer different questions, and where they disagree the schema is the record of what happened.

Use it for: what an element is called and what it may contain; which codes a failure can return; which documented capabilities we are not using; and which elements we send that the vendor never documented — those are the ones that can break without warning.

Envelope

Every request is <anfrage> with <user>, <key> and <satz typ="…"/>.

key = md5( user + password + date("YYYYMMDD") + satz-typ )

Per satz type and per day. ApiClient::createKey() implements it, and sendRawXml() regenerates the key before sending, so a hand-built payload can carry a placeholder.

Satz types

Documented: ADRESSFELDER, ADRESSFELDERGUTSCHEIN, ADRESSFELDERKATALOGANFRAGE, AGENTUREN, AGENTURKONTO, AKTIONSGUTSCHEIN, BUCHUNG, BUCHUNGAENDERUNG, CRM, CRMKATALOGANFRAGE, GUTSCHEINBUCHUNG, GUTSCHEINDRUCK, GUTSCHEINPRUEFUNGEINLOESUNG, GUTSCHEINVORLAGEN, GUTSCHEINVORSCHAU, GUTSCHEINZAHLUNGSARTEN, KATALOGANFRAGE, KUNDENKONTO, KUNDENLISTE, MOEGLICHEAENDERUNGEN, PRODUKTDATEN, PRODUKTE, SITZPLATZ, STAMMHOTELS, STAMMLAENDER, STAMMVERSICHERUNGEN, STAMMZUSTIEGE, VERFUEGBARKEIT, VERFUEGBARKEITHOTEL, ZAHLUNG, ZAHLUNG2, ZAHLUNGSARTEN, ZAHLUNGSARTEN2. SITZPLATZAUSWAHL and ZAHLUNG4 are named in prose without their own section.

Used here but undocumented: VERFUEGBARKEIT2 (ApiClient::TYPE_AVAILABILITY_EXTENDED) — the extended availability call returning buchungstatusmoeglich and per-service pflicht/frei. Its contract is observed, not specified.

Documented but unused here: SITZPLATZ/SITZPLATZAUSWAHL, KUNDENLISTE, ZAHLUNG2, ADRESSFELDER, STAMMHOTELS, STAMMVERSICHERUNGEN, STAMMZUSTIEGE, KATALOGANFRAGE.

BUCHUNG — the create contract

<buchungsart> decides what the call does:

Value Effect
Anfrage Enquiry. Validates and returns the full price breakdown. Nothing is stored.
Buchung Stores the booking.

<status> at the root: F Festbuchung · O Optionsbuchung · A Anfragebuchung. Gated by error 962 ("Buchungsstatus #1# kann nicht verarbeitet werden, nur Status #2# möglich"), and by 982/983 when the travel forbids options or enquiries outright.

Two accommodation models

The base payload books accommodation as an ordinary service:

<unterbringungen>
  <unterbringung idleistung="22341" anzahl="1" zuordnung="1,2"/>
</unterbringungen>

For the Ferienziel domain — what this app books — unterbringungen is replaced by ferienzielunterbringungen, and a root <idpartner> added:

<ferienzielunterbringungen>
  <ferienzielunterbringung idzimmer="2" kategorie="Ik" idverpflegung="2"
                           anreise="30.01.2018" abreise="06.02.2018"
                           anzahl="1" zuordnung="1,2"/>
</ferienzielunterbringungen><idpartner>78687</idpartner>

<idpartner> is introduced as the element you add when switching to the Ferienziel model, and 914 Partner-ID fehlt guards it. Note it is not confined to bookings that have a room: a teamer booking with zero BuchungTNFerienziel rows still returns <idpartner>222925</idpartner> from Vorgang_Details. Where that comes from is unresolvedBuchung has no partner FK, so either it is stored on Buchung.IDAdresse_FS_ExtraFerienzielPartner, or it is derived from the travel. That booking's product has exactly one hotel, so the response cannot distinguish the two; one read-only query settles it.

Accommodation is mandatory913 Unterbringungen fehlen / 916 Ferienziel-Unterbringungen fehlen. Confirmed empirically on the test system 2026-09-02, not just documented: see "Validation gate order" below. Room dates are cross-checked against the transport legs (921, 923).

Full documented request shape

<anfrage>
  <user/><key/><satz typ="BUCHUNG"/>
  <buchungsart>Anfrage|Buchung</buchungsart>
  <status>F|O|A</status>
  <idagentur/> <idcrm/> <idreise/> <idpartner/>
  <fremdvorgang/>            <!-- your own reference, stored as Buchung.FremdVorgang -->
  <datenschutztfmp/>         <!-- ad consent, combinable: T Telefon, F Telefax, M EMail, P Post -->
  <anmelder>
    name, vorname, anrede, titel, geschlecht, nationalitaet, geburtsdatum,
    <anschrift>strasse, plz, ort, ortsteil, land</anschrift>
    <kommunikation>telefonmobil, email, telefonprivat</kommunikation>
    <bemerkung/>
  </anmelder>
  <teilnehmerliste>
    <teilnehmer id="1">
      …same personal fields…
      <wünsche><unterbringungswunsch/><beförderungswunsch/></wünsche>
      <aktionscode/>
    </teilnehmer>
  </teilnehmerliste>
  <beförderungen><beförderung idleistung anzahl zuordnung/></beförderungen>
  <ferienzielunterbringungen></ferienzielunterbringungen>
  <zusatzleistungen><zusatzleistung idleistung anzahl zuordnung/></zusatzleistungen>
  <zustiege><zustieg idzustieg anzahl zuordnung/></zustiege>
  <sitzpläne><sitzplan idleistung idsitzplan sitzplätze="1B;1A"/></sitzpläne>
  <versicherungen><versicherung idversicherung anzahl zuordnung/></versicherungen>
  <gutscheine><gutschein gutscheinnr einloesecode/></gutscheine>
  <zuschlaege>
    <zuschlag text betrag anzahl zuordnung
              provision altersrabatt gruppenrabatt frühbucherrabatt bzberechnung/>
  </zuschlaege>
  <zahlung idzahlungsart bezeichnung art>
    <bankverbindung kreditinstitut bic iban kontoinhaber/>
  </zahlung>
</anfrage>

zuordnung is a comma-joined list of 1-based teilnehmer/@id values throughout.

Responses

buchungsart=Anfrage<buchung>möglich</buchung> plus <preise>: one <preis> per line with position, art, unterart, bezeichnung, terminvon/terminbis, anzahl, zuordnung, preis, gesamtpreis, id; then <gesamtpreis> and <zahlungsbedingungen> with <anzahlung> and <restzahlung> (each betrag + termin). art values seen: BEF transport, UNT accommodation, SON other, ERM discount, VER insurance.

buchungsart=Buchung<buchung>erfolgt</buchung>, <vorgang>, <rechnung>, <idbuchung>, <email>, <pdfpasswort>.

Validation gate order (measured)

Established with buchungsart=Anfrage against the test system on 2026-09-02, by mirroring a real room-less teamer booking (Vorgang 110628) element for element:

  1. Accommodation. No ferienzielunterbringungen916, regardless of everything else being valid. Nothing further is evaluated.
  2. Personal data, once a room is present → 650 with the field list "Fehlende Anmelder/Teilnehmer Daten … Nachname, Vorname, Strasse, Plz, Ort, Telefon-Mobil, Mail-Adresse, Geburtsdatum, Nationalität", for the Anmelder and every participant.

So sending idadresse/idadresseperson alone is not enough on create — BusPro validates the personal fields whether or not it could resolve them from the ids. Echoing the record back (idadresse + idadresseperson + full data) narrows the complaint to whatever that record is genuinely missing, which is a useful way to audit an address: the placeholder account 19465:41003 fails on Telefon-Mobil and Nationalität because its Vorgang_Details record has only telefonprivat and no nationalitaet.

Note 650 again: both of those are hard rejections delivered under the code Notification::isError() treats as success.

BUCHUNGAENDERUNG — the edit contract

Structurally identical to BUCHUNG plus <idbuchung>, and the responses follow buchungsart the same way. Ask MOEGLICHEAENDERUNGEN first (<idreise>), which answers:

<änderungen>
  <änderung art="anzahl_teilnehmer" möglich="False"/>
  <änderung art="beförderung"       möglich="False"/>
  <änderung art="zustieg"           möglich="False"/>
  <änderung art="sitztplatz"        möglich="False"/>   <!-- vendor's typo -->
  <änderung art="unterbringung"     möglich="False"/>
  <änderung art="zusatzleistung"    möglich="True" möglichbiszum="23.02.2020"/>
  <änderung art="teilnehmerdaten"   möglich="False"/>
</änderungen>

How this app maps onto the contract

BookingPayloadBuilder::buildCreatePayload() builds create; buildBasePayload() + the addTo…Payload() helpers build edit; ApiClient::createBooking()/updateBooking() wrap them.

Documented element Sent on create Sent on edit Notes
buchungsart yes yes Edit hardcodes Buchung (ApiClient.php:233)
status yes yes Edit replays the value read back from BusPro — see the caveat below
idreise, idpartner, idagentur yes yes idpartner sent unconditionally, even with no room
anmelder yes yes Create builds it from participant[0]; edit uses PersonalData::toPayload()
anmelder/bemerkung yes Via PersonalData::toPayload()
teilnehmerliste yes yes
wünsche (unterbringungswunsch, beförderungswunsch) yes yes
aktionscode yes yes Promo code, paired with AKTIONSGUTSCHEIN
beförderungen, zusatzleistungen, zustiege yes yes Built by ServiceMappingCollector
ferienzielunterbringungen yes yes Create omits the node entirely when no room maps; edit always emits the empty skeleton
versicherungen yes yes
gutscheine yes yes Paired with GUTSCHEINPRUEFUNGEINLOESUNG
zahlung + bankverbindung yes yes
idcrm no no
fremdvorgang no no Would give our own bookings a stable external reference
datenschutztfmp no no Advertising consent — we track newsletter consent locally instead
sitzpläne no no See Seats
zuschlaege no no Free-text manual surcharge or discount with its own amount

Newer than the PDF

These are absent from the document but work against the live API — vendor additions that post-date it. Listed so nobody mistakes the PDF's silence for a contract violation, and so the gaps in this reference are known.

Element Where Note
<ausstiege><ausstieg idzustieg …/></ausstiege> create and edit Return boarding points. Confirmed as a recent vendor addition; the PDF documents only <zustiege>.
<status> inside <teilnehmer> create (hardcoded F) and edit Not in the PDF in either direction, though Vorgang_Details returns status_teilnehmer.
<login>J</login> on KUNDENKONTO probe command Passwordless read escape hatch; see bpn-login-j-passwordless-reads.
VERFUEGBARKEIT2 availability path Whole satz type post-dates the PDF. Returns buchungstatusmoeglich and per-service pflicht.

When one of these needs pinning down, the authoritative move is a live Anfrage call, not a reading of this file.

Caveat: the edit flow replays a derived status

buildBasePayload() sends 'status' => $bookingData->status, and Booking::$status is parsed from the response's root <status> (BookingParser.php:38). But Buchung has no status column — the root value is an aggregate over BuchungTN.Status, the only real store (<status>F</status> beside <status_teilnehmer>F/F/…/F</status_teilnehmer> in tests/Resources/booking_data.xml). So on a booking whose participants do not all share one status, the edit flow sends an aggregate back as a directive. Worth confirming against a mixed booking before relying on it.

Gap: edit never dry-runs

Create is two-phase — Anfrage to validate and price, then Buchung. Edit sends buchungsart=Buchung directly (ApiClient.php:233), even though BUCHUNGAENDERUNG supports Anfrage identically. An enquiry pass would surface 913/924/962 and return the recalculated price before mutating a live booking.

Seats

SITZPLATZ asks BusPro to pick seats:

<satz typ="SITZPLATZ"/>
<idreise/><idleistung_hin/><idleistung_rueck/><anzahl_personen/>
<wunsch_vornemittehinten>V|M|H</wunsch_vornemittehinten>
<wunsch_linksrechts>L|R</wunsch_linksrechts>
<wunsch_fenstergang>F|G</wunsch_fenstergang>

Response: one <sitzplan> per direction with sitzplaetze="0D;1D;1C", idsitzplan, richtung="H|R", bezeichnung, idleistung, and grafik (base64 PNG of the seat map). Where seats carry surcharges the response adds <zuschläge> (id, bezeichnung, preis, colours) and the request must then carry idzuschlag. SITZPLATZAUSWAHL returns the raw plan structure instead of a picture.

Chosen seats go back into a booking via <sitzpläne>, landing in BuchungTNLeistungZusatz.Sitzplatz / IDSitzplan_FS. Guarded by 933 Sitzplan mit ID #1# bei Leistung #2# nicht freigegeben and 304 Es konnte nicht für alle Teilnehmer ein Sitzplatz ermittelt werden.

KUNDENKONTO

<art>: Adressdaten, Adressdaten_Ändern, Adresse_Neu, SelektionCRM, SelektionCRM_Ändern, Vorgänge, Vorgangdruck, Voucherdruck, Dokumentdruck, Passwort_Anfrage, Newsletter, Adressdokument, Vorgang_Details, Prüfung, PrüfungEmail, Ausweisdaten_Ändern, Stornoanfrage, Storno, Versicherungspolice.

Stornoanfrage answers stornierung_moeglich + stornopreis; Storno answers storniert — the documented way to cancel, including cleaning up a test booking. Ausweisdaten_Ändern takes a <personen> list with <ausweis> data and answers änderung. Versicherungspolice returns per-policy document URLs.

AGENTURKONTO

<art>: Vorgänge, Vorgangdruck, Rechnungdruck, Hotelgutscheine, Hotelgutscheindruck, OptionInFest — the last converts a held option into a firm booking, answering <optioninfestumgewandelt>true</optioninfestumgewandelt>. Vorgangdruck takes idbuchung, Rechnungdruck takes idrechnung, Hotelgutscheindruck takes idgutschein; print arts return base64 PDFs.

Availability and payment

VERFUEGBARKEIT takes <idreise> plus a <leistungen><leistung id="…"/></leistungen> filter and answers <reise id status> plus per-service status ("buchbar"), frei, preis, preisfaellig. VERFUEGBARKEIT2, which we actually use, additionally reports buchungstatusmoeglich and pflicht but is undocumented.

ZAHLUNGSARTEN2 lists payment methods; ZAHLUNG2 records a payment against a booking (vorgang, idbuchung, datum, betrag, idzahlungsart, zahlungstext — all mandatory). ZAHLUNG reports a credit-card authorisation after booking; the amount must equal the confirmed anzahlung or errors 1011-1018 follow.

Error codes

Returned as <satz typ="HINWEIS"><nr>…</nr><text>…</text></satz>. #1#/#2# are substituted at runtime.

650 is not a success code. Its documented text is a bare #1# — a generic message carrier whose entire content is substituted. Notification::isError() (src/BusProNet/Model/Notification.php:33-36) treats it as success, which happens to hold on the KUNDENKONTO paths but not on BUCHUNG/BUCHUNGAENDERUNG, where the same code delivers real rejections such as "Es wurde nicht jedem Teilnehmer eine Pflichtleistung zugeordnet". BookingEditSubmitter.php:81 therefore reports some genuine failures as INFO.

Envelope / auth — 100 Anfrageknoten fehlt · 101 Satzknoten fehlt · 102 Ungültiger Satztyp · 103/104 User fehlt/fehlerhaft · 105/106 Key fehlt/fehlerhaft · 999 Systemfehler

Lookup — 200 Keine Einträge · 201/202 Partner · 203 Ungültiger Termin · 300 IDReise fehlt · 301 Reise nicht gefunden · 302 IDLeistung (Hin & Rück) fehlt · 303 IDLeistung passt nicht zu IDReise · 304 Kein Sitzplatz für alle Teilnehmer ermittelbar · 305 Anzahl Personen fehlt · 306/307 Leistungen · 308 Keine Zahlungsarten · 400 IDPartner fehlt · 401 Partner nicht gefunden · 402 Bis-Termin fehlt · 403 Keine Unterbringungsleistungen gefunden

Customer / address — 500 Art fehlt · 502 Keine Kunden · 840-842 Personen · 850 IDAdresse und EMail fehlen · 851 Passwort fehlt · 852 Art fehlt · 853 ID, EMail oder Passwort falsch · 855 Keine Daten für Adresse · 860 Adressdaten fehlen · 862-867 Selektionsmerkmale / CRM-Aktionen · 870-875 Newsletter · 880-882 Dokumente · 885-891 Pflichtfelder (Name, Vorname, Geschlecht, EMail, PLZ, Straße) · 889 Kunde bereits angelegt (ID #1#)

Booking (create and edit) — 900 Buchungsart fehlt · 901 Buchungsart falsch · 902 Status fehlt · 903 Status falsch · 904/905 Agentur · 906 Reise-ID fehlt · 907 Reise nicht gefunden · 908 Reise ist storniert · 909 Beförderungen fehlen · 910 Leistung gehört nicht zur Reise · 911 Leistung gehört nicht zum Produkt · 912 Beförderungsleistung fehlt · 913 Unterbringungen fehlen · 914 Partner-ID fehlt · 915 Partner nicht gefunden · 916 Ferienziel-Unterbringungen fehlen · 917 Zimmer-IDZ fehlt · 918 Kategorie fehlt · 919 Verpflegungs-ID fehlt · 920 Anreise fehlt · 921 Anreise passt nicht zur Beförderungsleistung (Hinfahrt) · 922 Abreise fehlt · 923 Abreise passt nicht zur Beförderungsleistung (Rückfahrt) · 924 Ferienziel: Keine Preise zu den Daten gefunden · 925 Preis zu den Daten nicht gefunden · 930 Zustiege fehlen · 931 Zustieg nicht gefunden · 932 Zustieg bei Leistung nicht freigegeben · 933 Sitzplan bei Leistung nicht freigegeben · 935-938 Versicherungen · 940-942 Zahlungsart · 950 Anmelder fehlt · 951 Teilnehmerliste fehlt · 952 Teilnehmer-ID fehlt · 953 Keine Teilnehmer übergeben · 960 Preisfehler · 961 Buchung konnte nicht gespeichert werden · 962 Buchungsstatus #1# kann nicht verarbeitet werden, nur Status #2# möglich · 970 CRM-Merkmal nicht gültig · 980 Reise ist fürs Internet gesperrt · 981 Reise ist nicht mehr buchbar (#1#) · 982 Optionsbuchungen nicht zugelassen · 983 Anfragebuchung nicht zugelassen · 984 Leistung nicht fürs Internet buchbar · 985 Zustieg nicht fürs Internet buchbar · 986/987 Rechnung · 990-994 Agenturkonto · 995/996 Gutschein · 997/998 Buchung-ID · 1001 Reisedatum fehlerhaft · 1002 Buchungsdatum fehlerhaft · 1003 Status im Vorgang nicht korrekt · 1011-1018 Zahlungsabgleich

Vouchers — 600-608 Gutscheine · 610/611 Buchungsart · 612-621 Gutscheinart / Stamm / Kapazität · 622 Einlösecode fehlt · 630-632 Aktionsgutschein

Applying this: the teamer placeholder booking

Answered 2026-09-02: BUCHUNG cannot create it. A payload mirroring the real teamer booking (travel 12429, hotel 222925, agency 77876, both bus legs, ski pass, Ortstaxe, Keycard-Pfand, pickup and drop-off, root and participant status O, ids-only identity, no room) returns 916 Ferienziel-Unterbringungen fehlen. Adding a room to the identical payload clears that gate and moves on to 650, which proves accommodation was the only structural objection.

Two blockers that were not the problem:

  • Status O is fine here. Travel 12429 reports buchungstatusmoeglich="F, O, A". The 982 refusal seen earlier is per travel, not a global setting.
  • NurFahrt never came up — the request never got far enough for it to matter.

What the real booking shows about how the office builds one, worth reproducing if the work moves to a direct write:

  • <idpartner> is present despite there being no accommodation — 222925 is the ordinary hotel of the travel (Adresse.IDAdresse, "NEU Sportclub Weissfluh Midweeks und Wochenreisen", the sole hotel of product 1429, with real bookable rooms), not a special no-accommodation partner. Whether the booking stores it or the response derives it is still open.
  • Each teamer seat is its own AdressePerson (407725, 407726) under the shared address 41003, with an empty <anschrift/>.
  • <wünsche><beförderungswunsch> carries the role label — "Reiseleitung + Busbegleitung".
  • Per participant, <optionsdatum> sets the option expiry.
  • The total is driven to zero with a manual <zuschlag>: Ortstaxe 44,00 + Keycard-Pfand 10,80
    • Abfahrtsortzuschlag 25,80 = 80,60, offset by an unnamed 80,60 line. <zuschlaege> is part of the documented BUCHUNG payload, so this part is expressible over XML.

See .ddev/plans/teamer-placeholder-booking-plan.md.