Introduction
REST-Schnittstelle für Landingpages zur Angebots- und Buchungsanfrage von Fahrten bei Primos.
Diese Schnittstelle erlaubt es Landingpages (LPs), bei Primos ein Angebot für eine Fahrt anzufragen (verfügbare Fahrzeuge, Distanz, Preise) und anschließend eine Buchung anzulegen. Eine Buchung erzeugt im Primos-CRM eine Fahrt (Status „Anfrage") und löst dort die üblichen Automatismen aus.
Regulärer Ablauf – verbindlich in dieser Reihenfolge:
GET /api/v1/me– Token prüfen und die enthaltenen Berechtigungen (quote,book,estimate) sehen.GET /api/v1/airportsundGET /api/v1/vehicles– Kataloge abrufen. Die hier gelieferteniata-Codes (Flughäfen) undvehicle_ids sind die einzig gültigen Werte für alle Folgeanfragen; unverändert übernehmen.- (optional)
GET /api/v1/pricing-rules– Zuschläge und Rabatte als Zahlen, um Preisbestandteile schon vor dem ersten Angebot zu erklären. - (optional – Marketing/Marktanalyse)
POST /api/v1/estimates– unverbindliche Preisschätzung. Speichert nichts und gibt kein Token aus. Das Ergebnis ist nicht buchbar. POST /api/v1/quotes– verbindliches, zeitlich begrenztes Angebot anfragen. Antwort enthältquote_idsowie ein einmalig sichtbaresquote_token.POST /api/v1/bookings– mitquote_id,quote_tokenund gewähltemvehicle_idbuchen.
Wichtig: Eine Buchung (Schritt 6) ist nur mit einem zuvor erstellten, noch gültigen Angebot (Schritt 5) möglich – es gibt keinen Direkt-Buchungsweg. Preise sind im Angebot eingefroren; die Buchung verwendet ausschließlich den serverseitig berechneten Preis.
Berechtigungen
Jedes Token gehört zu genau einer Auftragsquelle und trägt eine feste Liste von Berechtigungen
(abilities). GET /api/v1/me zeigt sie an. Fehlt eine Berechtigung, antwortet der Endpunkt mit 403 –
das ist kein vorübergehender Fehler, sondern muss von Primos am Token geändert werden.
| Endpunkt | Benötigte Berechtigung |
|---|---|
GET /api/v1/health |
– (kein Token nötig) |
GET /api/v1/me |
quote |
GET /api/v1/vehicles |
quote |
GET /api/v1/airports |
quote |
GET /api/v1/pricing-rules |
quote oder estimate |
POST /api/v1/estimates |
estimate |
POST /api/v1/quotes |
quote |
GET /api/v1/quotes/{id} |
quote |
POST /api/v1/bookings |
book |
GET /api/v1/bookings/{id} |
book |
Eine typische Landingpage mit Buchungsstrecke braucht quote und book. estimate ist für
Preisrecherche gedacht und für eine Buchungsstrecke nicht nötig.
Rate-Limits
| Endpunktgruppe | Limit |
|---|---|
POST /api/v1/quotes |
60 Anfragen / Minute |
POST /api/v1/estimates |
60 Anfragen / Minute |
POST /api/v1/bookings |
20 Anfragen / Minute |
Katalog-Endpunkte (me, vehicles, airports, pricing-rules) |
60 Anfragen / Minute |
Die Limits gelten pro Auftragsquelle, nicht pro Endnutzer. Alle Besucher einer Landingpage teilen sich dasselbe Kontingent. Daraus folgt:
- Kataloge (
vehicles,airports,pricing-rules) zwischenspeichern statt bei jedem Seitenaufruf zu laden. - Kein Angebot bei jedem Tastendruck anfragen. Erst anfragen, wenn die Eingaben vollständig sind.
- Beim Überschreiten kommt
429mit dem HeaderRetry-After(Sekunden). Diesen Wert abwarten und danach höchstens ein- bis zweimal erneut versuchen – nicht in einer Schleife weiterfeuern.
Angebots-Lebenszyklus
Ein Angebot (quote) ist ein eingefrorener Preis mit Verfallsdatum:
- Erstellen –
POST /api/v1/quotesliefertquote_id,expires_atund immeta.quote_tokenein Token, das nur in dieser einen Antwort sichtbar ist. Es lässt sich nie wieder abrufen. - Gültigkeit – 30 Minuten ab Erstellung. Der genaue Zeitpunkt steht in
expires_at; immer diesen Wert auswerten, nicht die 30 Minuten selbst nachrechnen. - Einmalverwendung – ein Angebot lässt sich genau einmal buchen. Danach ist es verbraucht.
- Ende –
POST /api/v1/bookingsauf ein abgelaufenes oder bereits gebuchtes Angebot liefert409mit derselben Meldung. Ein409sagt nicht, welcher der beiden Fälle vorliegt. Wer das unterscheiden muss, prüft vorherexpires_atbzw. merkt sich, ob im eigenen System schon gebucht wurde.
Richtiges Verhalten bei 409: dem Gast sagen, dass der Preis nicht mehr gültig ist, mit denselben
Eingaben ein neues Angebot anfragen und den neuen Preis zur Bestätigung anzeigen. Niemals stillschweigend
neu anfragen und direkt buchen – der Preis kann sich geändert haben.
GET /api/v1/quotes/{id} liefert ein Angebot erneut (gleiche Preise, gleicher breakdown), aber ohne
das Token. Das Token muss also serverseitig aufbewahrt werden, solange der Checkout läuft.
Preise verstehen
Jeder Fahrzeugpreis enthält price.breakdown – eine Liste der Bestandteile, aus denen der Preis besteht:
"price": {
"amount": 189.0,
"currency": "EUR",
"roundtrip_included": true,
"breakdown": [
{"code": "base_fare", "label": "Fahrpreis", "amount": 179.0},
{"code": "night_surcharge", "label": "Nachtzuschlag", "amount": 15.0},
{"code": "roundtrip_discount", "label": "Rabatt Hin- und Rückfahrt", "amount": -10.0},
{"code": "child_seats", "label": "Kindersitze", "amount": 5.0}
]
}
Regeln dazu:
- Die Summe aller
amountimbreakdownist exaktprice.amount. Darauf darf man sich verlassen. - Der
codeist stabil und darf im Code gematcht werden; daslabelist fertiger Anzeigetext und kann sich ändern. - Bestandteile, die nicht angefallen sind, fehlen in der Liste.
base_fareist immer dabei. - Rabatte sind negativ.
- Mögliche Codes:
base_fare,night_surcharge,roundtrip_discount,airport_fee,sign_service,child_seats. Neue Codes können jederzeit dazukommen – unbekannte Codes einfach mit ihremlabelausgeben statt auf eine feste Liste zu prüfen.
Preise niemals selbst berechnen. Zuschläge, Mindestpreise und Rundungen ändern sich im Primos-Adminbereich.
Wer Beträge schon vor dem ersten Angebot anzeigen will (z. B. „Namensschild +55 €" am Kontrollkästchen),
holt sie über GET /api/v1/pricing-rules – und zeigt als Endpreis trotzdem immer price.amount aus dem Angebot.
Sicherheit
Das Bearer-Token gehört ausschließlich auf den Server. Es berechtigt zum Anlegen echter Fahrten im Primos-CRM. Konkret:
- Niemals in JavaScript, HTML, ein Frontend-Bundle oder eine
.envmitVITE_/NEXT_PUBLIC_-Präfix legen. Alle API-Aufrufe laufen über den eigenen Server; der Browser spricht nur mit der eigenen Anwendung. - Das
quote_tokenist genauso geheim wie das Bearer-Token. Wer es besitzt, kann zum eingefrorenen Preis buchen. Es gehört in die Server-Session (oder eine eigene Tabelle) – nicht in ein<input type="hidden">, nicht inlocalStorage, nicht in die URL. - Preise nie aus dem Browser übernehmen. Ein im Buchungs-Request gesendeter Preis wird ignoriert; es zählt ausschließlich der Preis aus dem Angebot. Das ist Absicht und die zweite Verteidigungslinie.
- Origin-Beschränkung: Für eine Auftragsquelle kann eine Liste erlaubter Origins hinterlegt sein. Ist sie
gesetzt, werden Anfragen mit
Origin-Header nur von diesen Origins akzeptiert. Server-zu-Server-Aufrufe senden keinenOrigin-Header und sind nicht betroffen. - Fehlerantworten nicht ungefiltert an den Gast durchreichen und Request-Header nicht mitloggen – sonst landet das Token im Logfile.
- Gastdaten (Name, E-Mail, Telefon) nur so lange speichern, wie der Checkout läuft.
Fehlerkatalog
| Status | Bedeutung | Richtige Reaktion |
|---|---|---|
401 |
Token fehlt, ist falsch oder wurde widerrufen. | Konfiguration prüfen. Kein Retry. |
403 |
Zugriff verweigert – drei verschiedene Ursachen, siehe unten. | Meldung auswerten. Kein Retry. |
404 |
Angebot/Buchung existiert nicht oder gehört zu einer anderen Auftragsquelle. | Wie „nicht vorhanden" behandeln. |
409 |
Angebot abgelaufen oder bereits gebucht. | Neues Angebot anfragen, neuen Preis bestätigen lassen. |
422 |
Validierungsfehler. Feldliste steht in errors. |
Eingaben korrigieren. Kein Retry mit denselben Daten. |
429 |
Rate-Limit erreicht. | Retry-After abwarten, dann begrenzt erneut versuchen. |
5xx |
Störung auf Primos-Seite. | Ein- bis zweimal mit Abstand erneut versuchen, dann Gast um Kontaktaufnahme bitten. |
Die drei Ursachen für 403
Alle drei liefern denselben Status, aber unterschiedliche message – deshalb im Fehlerfall immer die
Meldung auswerten und mitloggen:
message |
Ursache | Lösung |
|---|---|---|
Invalid ability provided. |
Dem Token fehlt die für diesen Endpunkt nötige Berechtigung (z. B. book). |
Primos um ein Token mit der passenden Berechtigung bitten. |
Die Auftragsquelle ist deaktiviert. |
Die Auftragsquelle wurde im CRM abgeschaltet. Betrifft alle Endpunkte. | Primos kontaktieren. |
Origin ist für diese Auftragsquelle nicht erlaubt. |
Die Anfrage trug einen Origin-Header, der nicht auf der Erlaubnisliste steht – typischerweise ein Aufruf aus dem Browser statt vom Server. |
Aufruf serverseitig ausführen oder die Origin freischalten lassen. |
Eine vierte 403-Antwort kommt nur bei der Buchung vor: Ungültiges Angebots-Token. bedeutet, dass
quote_token nicht zum quote_id passt – meist ein vertauschtes oder abgeschnittenes Token.
Alle Fehlerantworten haben denselben Aufbau: ein message-Feld, bei 422 zusätzlich errors mit den
betroffenen Feldnamen als Schlüssel.
Integration einer Landingpage
Der Ablauf in Kürze: Der Browser spricht nur mit der eigenen Anwendung, die eigene Anwendung spricht mit Primos. Zwei Schritte, zwei Request-Zyklen:
Schritt 1 – Preise zeigen. Gast füllt das Formular aus → eigener Server fragt POST /api/v1/quotes an →
quote_id und quote_token in die Session, Fahrzeuge samt Preisen an den Browser.
Schritt 2 – Buchen. Gast wählt ein Fahrzeug und gibt seine Daten ein → eigener Server holt quote_id
und quote_token aus der Session und ruft POST /api/v1/bookings auf.
Was gehört in welchen Schritt? Ins Angebot gehört ausschließlich, was den Preis bestimmt: Strecke, Zeitpunkt, Personen, Kindersitze, Hin- und Rückfahrt sowie die Zusatzleistungen als Schalter. Alles Übrige – Kontaktdaten, Rechnungsanschrift, Notizen und der Name auf dem Namensschild – gehört in die Buchung. Das ist kein Stilfrage, sondern verhindert eine ganze Fehlerklasse: Ein Rechner fragt bei jeder preisrelevanten Änderung ein neues Angebot an. Läge ein nicht preisrelevantes Feld im Angebots-Payload, müsste ausgerechnet dieses eine Feld von der Neuberechnung ausgenommen werden – sonst erzeugt jeder Tastendruck ein neues Angebot oder verwirft das vorhandene.
Konkret beim Schilderservice: options.sign_service (preisrelevant) beim Angebot, sign_name
(nicht preisrelevant) bei der Buchung. options.sign_name beim Angebot bleibt möglich, ist aber
nur sinnvoll, wenn der Name ohnehin schon vor der Preisanzeige feststeht.
Lauffähiges Beispiel (PHP/Laravel; in jeder anderen Sprache dasselbe Muster):
use Illuminate\Support\Facades\Http;
$primos = fn () => Http::baseUrl('https://primos-fahrservice.de/api/v1')
->withToken(config('services.primos.token'))
->acceptJson()
->timeout(20);
// Schritt 1: Angebot anfragen. place_id stammt aus Google Places Autocomplete.
$response = $primos()->post('quotes', [
'type' => 'airport',
'pickup' => ['airport_iata' => 'fra'],
'destination' => [
'place_id' => $request->string('place_id')->toString(),
'formatted_address' => $request->string('formatted_address')->toString(),
'street' => $request->string('street')->toString(),
'house_number' => $request->string('house_number')->toString(),
],
'pickup_time' => '2026-07-10T14:30:00+02:00',
'passengers' => 2,
'child_seats' => 1,
'flight_number' => 'LH123',
// Nur der Schalter – der Name fürs Schild folgt erst bei der Buchung.
'options' => ['sign_service' => true],
]);
if ($response->status() === 422) {
return back()->withErrors($response->json('errors', []));
}
$response->throw();
// Das Token nur serverseitig ablegen – niemals an den Browser geben.
session([
'primos.quote_id' => $response->json('data.quote_id'),
'primos.quote_token' => $response->json('meta.quote_token'),
'primos.expires_at' => $response->json('data.expires_at'),
]);
// An den Browser gehen nur Anzeigedaten: Fahrzeuge, Preise, Preisbestandteile.
$vehicles = $response->json('data.vehicles');
// ---------------------------------------------------------------------------
// Schritt 2: Buchen (eigener Request, nachdem der Gast ein Fahrzeug gewählt hat)
// ---------------------------------------------------------------------------
$booking = $primos()
// Schützt vor Doppelbuchung bei Doppelklick oder Timeout-Retry.
->withHeader('Idempotency-Key', session()->getId())
->post('bookings', [
'quote_id' => session('primos.quote_id'),
'quote_token' => session('primos.quote_token'),
'vehicle_id' => $request->integer('vehicle_id'),
'customer' => [
'firstname' => $request->string('firstname')->toString(),
'lastname' => $request->string('lastname')->toString(),
'email' => $request->string('email')->toString(),
'phone' => $request->string('phone')->toString(),
],
// Erst hier, weil der Name den Preis nicht beeinflusst. Ohne Angabe
// druckt Primos den Namen des Bestellers aufs Schild.
'sign_name' => $request->string('sign_name')->toString(),
'notes' => $request->string('notes')->toString(),
'reference' => 'LP-'.$order->id,
]);
if ($booking->status() === 409) {
// Angebot abgelaufen oder schon gebucht: neu anfragen und den neuen Preis bestätigen lassen.
return redirect()->route('checkout.restart')
->with('warning', 'Der Preis ist nicht mehr gültig. Bitte bestätige den aktuellen Preis.');
}
$booking->throw();
$bookingId = $booking->json('data.booking_id');
Checkliste vor dem Livegang
- [ ]
GET /api/v1/meliefert200und die erwartetenabilities. - [ ] Token steht nur in der Server-Konfiguration, nicht im Frontend-Bundle.
- [ ]
quote_tokenliegt in der Session, nicht im HTML. - [ ] Der Angebots-Payload enthält ausschließlich preisrelevante Felder.
- [ ]
Idempotency-Keywird bei jeder Buchung gesendet. - [ ]
409,422und429haben je eine eigene, für den Gast verständliche Behandlung. - [ ] Kataloge werden zwischengespeichert.
- [ ] Angezeigt wird
price.amountaus dem Angebot – kein selbst gerechneter Betrag.
Google Places: die häufigste Fehlerquelle
Adress-Seiten müssen mit einer Google-place_id aus Google Places Autocomplete gesendet werden.
Koordinaten oder Freitext werden abgelehnt (422). Grund: Primos routet ausschließlich über die place_id,
damit Distanz und Preis exakt dem Website-Rechner entsprechen.
Der klassische Fallstrick: Ort statt Adresse. Google Places liefert auch place_ids für Orte, Regionen
und Sehenswürdigkeiten – „Frankfurt am Main" hat eine place_id, genau wie „Zeil 1, Frankfurt am Main".
Beide werden von der API akzeptiert, ergeben aber völlig verschiedene Strecken: Eine Orts-place_id routet
auf den geografischen Mittelpunkt, nicht auf die Haustür des Gasts. Der Gast bekommt dann einen Preis für
eine Fahrt, die so nie stattfindet.
So vermeidet man das:
- Autocomplete auf Adressen einschränken (
types: ['address']) und auf Deutschland begrenzen. - Die Auswahl erst akzeptieren, wenn die Place-Details eine Hausnummer (
street_number) enthalten. Fehlt sie, hat der Gast einen Ort gewählt – dann zur Eingabe der vollständigen Adresse auffordern. streetundhouse_numberaus denaddress_componentsder Place-Details entnehmen, nicht aus dem eingetippten Text. Beide Felder sind für Adress-Seiten Pflicht.place_id,formatted_address,streetundhouse_numberzusammen aus derselben Auswahl senden. Eineplace_idmit einer nachträglich vom Gast bearbeiteten Adresse ergibt eine falsche Fahrt.
Best Practice – eigenen place_id-Bestand pflegen: Für wiederkehrende Ziele (Hotels, Messegelände,
Firmensitze) die place_id dauerhaft speichern und bei Folgeanfragen wiederverwenden, statt sie jedes
Mal neu zu bestimmen. Das spart Google-Maps-Aufrufe und hält Distanz und Preis über alle Anfragen stabil.
Flughäfen brauchen gar kein Google Places: Dort genügt der airport_iata aus GET /api/v1/airports.
Changelog
Änderungen an dieser Schnittstelle sind additiv: Neue Request-Felder sind optional, neue Antwortfelder kommen hinzu, ohne bestehende zu entfernen oder umzubenennen. Unbekannte Antwortfelder sollten Clients ignorieren, statt auf einer exakten Feldliste zu bestehen.
2026-08-16
- Gelockert:
options.sign_nameist beim Angebot nicht mehr Pflicht, auch wennoptions.sign_service= true gesetzt ist. Preisrelevant ist allein der Schalter; der Name kann daher erst dann erfragt werden, wenn der Gast ohnehin seine Daten eingibt. - Neu:
sign_nameim Buchungs-Request (POST /api/v1/bookings). Reihenfolge: dieser Wert schlägt einoptions.sign_nameaus dem Angebot; fehlt beides, wird der Name des Bestellers gedruckt. Ein bezahlter Schilderservice erreicht den Fahrer damit nie ohne Namen. - Geändert: In
GET /api/v1/pricing-rulesstehtsign_service.requires_sign_namejetzt auffalse; neu danebensign_service.sign_name_fallbackmit dem Wertcustomer_name.
Nicht breaking. Wer options.sign_name weiterhin beim Angebot mitschickt, bekommt unverändertes
Verhalten – der Name wird gespeichert und gedruckt, sofern die Buchung keinen eigenen sendet. Es
entfällt lediglich eine Validierungsregel, die bisher zu 422 führen konnte.
2026-08-14
- Neu:
GET /api/v1/pricing-rules– Zuschläge, Rabatte und Grenzwerte als Zahlen, für Preishinweise vor dem ersten Angebot. - Neu:
price.breakdownin Angebot und Schätzung – die Bestandteile jedes Preises. Ihre Summe ist exaktprice.amount. - Neu:
image_urlbei Fahrzeugen (in Katalog, Angebot und Schätzung) – absolute, direkt einbindbare Bild-URL. Das bisherigeimage(roher Speicherpfad) bleibt erhalten. - Neu:
return_flight_numberbei der Angebotsanfrage – die Flugnummer der Rückfahrt. Wird bis in den Fahrtauftrag durchgereicht. - Neu:
options.sign_namebei der Angebotsanfrage – der Name auf dem Namensschild. Bisher wurde der Schilderservice berechnet, ohne dass der Name jemals bei Disposition und Fahrer ankam. (War zunächst Pflicht bei gebuchtem Schilderservice; seit 2026-08-16 optional, siehe oben.) - Korrektur: Der Kindersitz-Zuschlag wird jetzt auch über die API berechnet – wie im Website-Rechner
je Kindersitz und Fahrtrichtung. Bei gesetztem Kindersitzpreis steigen dadurch die Preise für
Anfragen mit
child_seats> 0.
Migrationshinweis (überholt durch 2026-08-16): Diese Fassung verlangte zusätzlich options.sign_name,
sobald options.sign_service = true gesendet wurde. Die Regel ist entfallen; es besteht kein
Anpassungsbedarf mehr. Alle übrigen Änderungen dieser Fassung erforderten von Anfang an keine Anpassung.
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer {YOUR_AUTH_KEY}".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
Jeder Zugang gehört zu einer Auftragsquelle. Das Bearer-Token wird von Primos im Admin-Bereich erstellt und verwaltet. Token besitzen Berechtigungen: quote (Angebote), book (Buchungen) und estimate (unverbindliche Preisschätzung). Welche Berechtigung welcher Endpunkt braucht, steht in der Einführung unter „Berechtigungen".
Das Token gehört ausschließlich auf den Server – es berechtigt zum Anlegen echter Fahrten. Niemals in JavaScript, ein Frontend-Bundle oder eine öffentlich ausgelieferte Umgebungsvariable legen. Siehe Kapitel „Sicherheit".
Origin-Beschränkung (optional): Für eine Auftragsquelle kann eine Liste erlaubter Origins hinterlegt sein. Ist sie gesetzt, werden Browser-Anfragen nur von einer dieser Origins akzeptiert (sonst 403); Anfragen ohne Origin-Header (Server-zu-Server) sind nicht betroffen. Ist keine Liste hinterlegt, gilt keine Einschränkung. Bei Bedarf teilt Primos die freigeschalteten Origins mit.
Authentifizierung
Eigene Auftragsquelle
requires authentication
Gibt die zum Token gehörende Auftragsquelle samt Berechtigungen zurück – nützlich als Token-Check
beim Deployment: Wer hier 200 samt der erwarteten abilities bekommt, hat ein gültiges Token
für eine aktive Auftragsquelle. Erfordert selbst die Berechtigung quote.
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/me" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/me"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"data": {
"name": "LP Flughafentransfer FFM",
"slug": "lp-ffm",
"contact_email": "[email protected]",
"is_active": true,
"abilities": [
"quote",
"book"
]
}
}
Example response (401):
{
"message": "Unauthenticated."
}
Example response (403):
{
"message": "Die Auftragsquelle ist deaktiviert."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
name
string
Interner Name der Auftragsquelle im Primos-CRM.
slug
string
Eindeutiger Bezeichner der Auftragsquelle.
contact_email
string
Technischer Ansprechpartner dieser Auftragsquelle.
is_active
boolean
Ob die Auftragsquelle aktiv ist. Bei false antwortet jeder Endpunkt mit 403.
abilities
string[]
Berechtigungen genau dieses Tokens: quote, estimate, book. Fehlt eine, liefern die zugehörigen Endpunkte 403.
Flughäfen
Flughäfen
requires authentication
Liefert die aktuell bedienten Flughäfen. Der zurückgegebene iata-Wert ist exakt der Wert,
der bei type=airport-Angeboten als pickup.airport_iata bzw. destination.airport_iata
gesendet werden muss. Bitte diese Liste als einzige Quelle für gültige IATA-Codes nutzen und
die Werte unverändert übernehmen (der Vergleich erfolgt case-sensitive) – so entstehen keine
Abweichungen zwischen Landingpage und Angebotsanfrage.
Die mitgelieferten Koordinaten (lat/lng) und die Google-place_id entsprechen dem, was
Primos serverseitig für die Distanzberechnung verwendet, und können für ein eigenes
Google-Maps-Autocomplete genutzt werden.
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/airports" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/airports"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"data": [
{
"iata": "fra",
"name": "Frankfurt International Airport",
"lat": 50.0353788,
"lng": 8.5518391,
"place_id": "ChIJeflCVHQLvUcRMfP4IU3YdIo"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
iata
string
IATA-Code, unverändert als pickup.airport_iata bzw. destination.airport_iata senden (case-sensitive).
name
string
Anzeigename des Flughafens.
lat
number
Breitengrad, wie von Primos für die Distanzberechnung verwendet.
lng
number
Längengrad, wie von Primos für die Distanzberechnung verwendet.
place_id
string
Google-Place-ID des Flughafens. Für eine Angebotsanfrage nicht nötig – dort genügt airport_iata.
Fahrzeuge
Fahrzeugkatalog
requires authentication
Liefert die aktuell angebotenen Fahrzeuge (ohne Preise – Preise entstehen erst im Angebot).
Gedacht für Fahrzeugübersichten und für Eingabegrenzen im Formular (Personen, Gepäck, Kindersitze). Welche Fahrzeuge für eine konkrete Fahrt tatsächlich zur Wahl stehen, entscheidet erst das Angebot – dort können je nach Personenzahl und Kindersitzen weniger Fahrzeuge erscheinen als hier.
Der Katalog ändert sich selten; einmal täglich abrufen und zwischenspeichern genügt.
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/vehicles" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/vehicles"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"data": [
{
"vehicle_id": 3,
"name": "Business Van",
"description": "Bis zu 7 Personen",
"passengers": 7,
"luggage": 7,
"child_seats": 3,
"image": "vehicles/van.jpg",
"image_url": "https://primos-fahrservice.de/storage/vehicles/van.jpg"
},
{
"vehicle_id": 1,
"name": "Business Limousine",
"description": "Mercedes E-Klasse o. ä.",
"passengers": 3,
"luggage": 3,
"child_seats": 1,
"image": "vehicles/limo.jpg",
"image_url": "https://primos-fahrservice.de/storage/vehicles/limo.jpg"
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
vehicle_id
integer
Kennung des Fahrzeugs. Einzig gültiger Wert für vehicle_id in Folgeanfragen.
name
string
Anzeigename des Fahrzeugs.
description
string
Kurzbeschreibung für die Anzeige.
passengers
integer
Maximale Personenzahl.
luggage
integer
Maximale Gepäckstücke.
child_seats
integer
Maximal mitführbare Kindersitze.
image
string
Roher Speicherpfad des Bilds – ohne Basis-URL nicht verwendbar. Für die Anzeige image_url nehmen.
image_url
string
Absolute, direkt in ein <img src> einsetzbare URL. null, wenn kein Bild hinterlegt ist.
Preise
Preisregeln
requires authentication
Liefert die Zuschläge, Rabatte und Grenzwerte, die Primos in jeden Angebotspreis einrechnet – als Zahlen, nicht als Text. Gedacht für Frontends, die den Preis vor dem ersten Angebot erklären oder Eingaben begrenzen müssen (z. B. „Namensschild +55 €" am Kontrollkästchen, maximale Kindersitze je Personenzahl).
Diese Werte sind keine Preisberechnung. Verbindlich ist immer der Preis aus
POST /api/v1/quotes; dessen breakdown weist dieselben Bestandteile mit den
tatsächlich berechneten Beträgen aus. Werte hier höchstens für die Anzeige nutzen,
nie um einen Endpreis selbst zusammenzurechnen.
Die Werte werden im Primos-Adminbereich gepflegt und können sich ändern. Nicht fest im eigenen Code hinterlegen, sondern abrufen und höchstens kurz zwischenspeichern (Empfehlung: eine Stunde).
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/pricing-rules" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/pricing-rules"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"data": {
"currency": "EUR",
"quote_ttl_minutes": 30,
"components": [
{
"code": "base_fare",
"label": "Fahrpreis"
},
{
"code": "night_surcharge",
"label": "Nachtzuschlag"
},
{
"code": "roundtrip_discount",
"label": "Rabatt Hin- und Rückfahrt"
},
{
"code": "airport_fee",
"label": "Flughafenentgelt"
},
{
"code": "sign_service",
"label": "Namensschild-Service"
},
{
"code": "child_seats",
"label": "Kindersitze"
}
],
"roundtrip_discount": {
"type": "value",
"value": 10,
"percentage": 10,
"applies_to": "roundtrip"
},
"sign_service": {
"fee": 55,
"applies_to": "airport",
"charged_per": "booking",
"requires_sign_name": false,
"sign_name_fallback": "customer_name"
},
"child_seats": {
"price_per_seat": 5,
"charged_per": "leg",
"max_seats": 4
},
"night_surcharge": {
"enabled": true,
"threshold": 80,
"intervals": [
{
"start": "01:00:00",
"end": "05:00:00",
"multiplier_below_threshold": 1.25,
"multiplier_from_threshold": 1.25
}
]
},
"airport_fee": {
"enabled": true,
"amount": 20,
"threshold": 100,
"applies_to": "airport",
"charged_per": "leg"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
currency
string
ISO-4217-Währung aller Beträge dieser Antwort und aller Preise der API.
quote_ttl_minutes
integer
Gültigkeitsdauer eines Angebots in Minuten (siehe Angebots-Lebenszyklus).
components
object[]
Alle Bestandteile, die im breakdown eines Angebots auftauchen können, mit stabilem code und deutschem Label.
roundtrip_discount
object
Nachlass bei gemeinsamer Buchung von Hin- und Rückfahrt. type=value bedeutet: value Euro pauschal; type=percentage: percentage Prozent.
sign_service
object
Zuschlag für die Abholung mit Namensschild. Preisrelevant ist allein options.sign_service; der Name auf dem Schild ist nie Pflicht (requires_sign_name=false) und kann sowohl beim Angebot als auch bei der Buchung mitgegeben werden. Fehlt er überall, greift sign_name_fallback – customer_name bedeutet: es wird der Name des Bestellers gedruckt.
child_seats
object
Kindersitz-Zuschlag. charged_per=leg bedeutet: bei roundtrip=true fällt er zweimal an. max_seats ist die größte Kindersitz-Zahl der Flotte.
night_surcharge
object
Nachtzuschlag-Fenster (Ortszeit Europe/Berlin). Fahrpreise unter threshold werden mit multiplier_below_threshold multipliziert, ab threshold mit multiplier_from_threshold.
airport_fee
object
Flughafenentgelt. Wird nur auf Fahrpreise unterhalb von threshold aufgeschlagen.
Preis schätzen
requires authentication
Berechnet für die angegebene Strecke die verfügbaren Fahrzeuge samt Distanz und Preisen –
ohne ein Angebot zu speichern und ohne ein buchbares Token auszugeben. Gedacht als
Marketing-/Marktanalyse-Werkzeug, um Preise durchzurechnen. Die zurückgegebenen Preise sind
unverbindlich und können nicht gebucht werden; dafür muss ein Angebot über
POST /api/v1/quotes erstellt werden.
Eingabe- und Locator-Regeln sind identisch zu POST /api/v1/quotes (Adress-Seite:
place_id + formatted_address + street + house_number; Flughafen-Seite: airport_iata).
Dadurch entspricht der geschätzte Preis exakt dem späteren Angebotspreis.
Ablauf-Hinweis: Dieser Endpunkt ersetzt nicht den Buchungsweg. Wer buchen will, muss zuerst
POST /api/v1/quotes (Angebot) und dann POST /api/v1/bookings aufrufen – siehe Einführung.
Empfehlung für Marktanalysen: Lege dir einen eigenen Bestand an place_ids je Stadt/Adresse an
(z. B. einmal aus GET /api/v1/airports bzw. Google Places ermittelt) und sende diese place_ids bei jeder
Preisabfrage wieder. So bestimmt der aufrufende Client (z. B. Claude Code) die Orte einmalig und fragt danach
aus dem eigenen Bestand ab, statt bei jeder Anfrage neue Google-Maps-Aufrufe auszulösen. Das spart Kosten und
liefert stabile, reproduzierbare Preise.
Erfordert ein Token mit der Berechtigung estimate.
Example request:
curl --request POST \
"https://primos-fahrservice.de/api/v1/estimates" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"airport\",
\"pickup\": {
\"airport_iata\": \"fra\",
\"place_id\": \"ChIJ7cv00DwsDogRAMDACa2m4K8\",
\"formatted_address\": \"Bahnhofstraße 1, 65552 Limburg an der Lahn, Deutschland\",
\"street\": \"Bahnhofstraße\",
\"house_number\": \"1\",
\"postal_code\": \"65552\",
\"city\": \"Limburg an der Lahn\"
},
\"destination\": {
\"airport_iata\": \"fra\",
\"place_id\": \"ChIJeflCVHQLvUcRMfP4IU3YdIo\",
\"formatted_address\": \"Zeil 1, 60313 Frankfurt am Main, Deutschland\",
\"street\": \"Zeil\",
\"house_number\": \"1\",
\"postal_code\": \"60313\",
\"city\": \"Frankfurt am Main\"
},
\"pickup_time\": \"2026-07-10T14:30:00+02:00\",
\"roundtrip\": false,
\"passengers\": 1,
\"child_seats\": 0,
\"flight_number\": \"LH123\",
\"return_flight_number\": \"LH124\",
\"options\": {
\"sign_service\": false,
\"sign_name\": \"Familie Mustermann\"
}
}"
const url = new URL(
"https://primos-fahrservice.de/api/v1/estimates"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "airport",
"pickup": {
"airport_iata": "fra",
"place_id": "ChIJ7cv00DwsDogRAMDACa2m4K8",
"formatted_address": "Bahnhofstraße 1, 65552 Limburg an der Lahn, Deutschland",
"street": "Bahnhofstraße",
"house_number": "1",
"postal_code": "65552",
"city": "Limburg an der Lahn"
},
"destination": {
"airport_iata": "fra",
"place_id": "ChIJeflCVHQLvUcRMfP4IU3YdIo",
"formatted_address": "Zeil 1, 60313 Frankfurt am Main, Deutschland",
"street": "Zeil",
"house_number": "1",
"postal_code": "60313",
"city": "Frankfurt am Main"
},
"pickup_time": "2026-07-10T14:30:00+02:00",
"roundtrip": false,
"passengers": 1,
"child_seats": 0,
"flight_number": "LH123",
"return_flight_number": "LH124",
"options": {
"sign_service": false,
"sign_name": "Familie Mustermann"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (200):
{
"data": {
"estimate": true,
"type": "airport",
"distance_km": 63,
"roundtrip": false,
"currency": "EUR",
"vehicles": [
{
"vehicle_id": 1,
"name": "Limousine / Kombi",
"passengers": 3,
"luggage": 3,
"child_seats": 2,
"image": "uploads/vehicles/limo.jpg",
"image_url": "https://primos-fahrservice.de/storage/uploads/vehicles/limo.jpg",
"price": {
"amount": 95,
"currency": "EUR",
"roundtrip_included": false,
"breakdown": [
{
"code": "base_fare",
"label": "Fahrpreis",
"amount": 75
},
{
"code": "airport_fee",
"label": "Flughafenentgelt",
"amount": 20
}
]
}
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
estimate
boolean
Immer true – kennzeichnet die Antwort als unverbindliche Schätzung ohne Angebot.
type
string
Fahrttyp (transfer oder airport).
distance_km
integer
Ermittelte Strecke in Kilometern.
roundtrip
boolean
true, wenn der Preis Hin- und Rückfahrt enthält.
currency
string
ISO-4217-Währung aller Beträge.
vehicles
object[]
Fahrzeuge samt geschätztem Preis. Aufbau identisch zum Angebot – aber nicht buchbar, es gibt weder quote_id noch quote_token.
quote_id noch quote_token.
price
object
amount
number
Geschätzter Endpreis. Entspricht dem Angebotspreis, solange sich Eingaben und Preisregeln nicht ändern.
breakdown
object[]
Bestandteile des Preises; die Summe der amount ist exakt price.amount. Codes wie beim Angebot.
image_url
string
Absolute, direkt einbindbare Bild-URL (null ohne hinterlegtes Bild).
Angebote
Angebot anfragen
requires authentication
Berechnet für die angegebene Strecke die verfügbaren Fahrzeuge samt Distanz und Preisen und legt ein zeitlich begrenztes Angebot an.
Locator-Regeln (identisch zum Website-Rechner): Jede Seite (pickup/destination) ist
entweder eine Adresse oder ein Flughafen.
- Adress-Seite:
place_id(Pflicht, aus Google Places Autocomplete) plusformatted_address,streetundhouse_number. Es wird ausschließlich über dieplace_idgeroutet – reine Koordinaten oder Freitext-Adressen werden nicht akzeptiert, damit Distanz und Preis exakt mit der Website übereinstimmen und keine Abweichungen entstehen. - Flughafen-Seite: ausschließlich
airport_iataausGET /api/v1/airports. place_id, Koordinaten und Anzeigename löst Primos serverseitig auf.
Bei type=airport muss genau eine Seite ein airport_iata enthalten (die andere ist eine
Adress-Seite); bei type=transfer sind beide Seiten Adress-Seiten und airport_iata ist nicht erlaubt.
Angebot ist Pflichtstation: Die Antwort enthält im meta.quote_token ein Token, das nur hier
einmalig sichtbar ist. Es gehört serverseitig gespeichert und darf niemals in den Browser gelangen –
wer es hat, kann zum eingefrorenen Preis buchen. Ein Angebot gilt 30 Minuten und ist genau einmal buchbar.
Preisbestandteile: Jeder Fahrzeugpreis enthält price.breakdown – die Zeilen, aus denen der Preis
besteht. Ihre Summe ist exakt price.amount; für die Anzeige also nie selbst rechnen, sondern die
Zeilen ausgeben und price.amount als Endpreis nehmen.
Example request:
curl --request POST \
"https://primos-fahrservice.de/api/v1/quotes" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"airport\",
\"pickup\": {
\"airport_iata\": \"fra\",
\"place_id\": \"ChIJ7cv00DwsDogRAMDACa2m4K8\",
\"formatted_address\": \"Bahnhofstraße 1, 65552 Limburg an der Lahn, Deutschland\",
\"street\": \"Bahnhofstraße\",
\"house_number\": \"1\",
\"postal_code\": \"65552\",
\"city\": \"Limburg an der Lahn\"
},
\"destination\": {
\"airport_iata\": \"fra\",
\"place_id\": \"ChIJeflCVHQLvUcRMfP4IU3YdIo\",
\"formatted_address\": \"Zeil 1, 60313 Frankfurt am Main, Deutschland\",
\"street\": \"Zeil\",
\"house_number\": \"1\",
\"postal_code\": \"60313\",
\"city\": \"Frankfurt am Main\"
},
\"pickup_time\": \"2026-07-10T14:30:00+02:00\",
\"roundtrip\": false,
\"passengers\": 1,
\"child_seats\": 0,
\"flight_number\": \"LH123\",
\"return_flight_number\": \"LH124\",
\"options\": {
\"sign_service\": false,
\"sign_name\": \"Familie Mustermann\"
}
}"
const url = new URL(
"https://primos-fahrservice.de/api/v1/quotes"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "airport",
"pickup": {
"airport_iata": "fra",
"place_id": "ChIJ7cv00DwsDogRAMDACa2m4K8",
"formatted_address": "Bahnhofstraße 1, 65552 Limburg an der Lahn, Deutschland",
"street": "Bahnhofstraße",
"house_number": "1",
"postal_code": "65552",
"city": "Limburg an der Lahn"
},
"destination": {
"airport_iata": "fra",
"place_id": "ChIJeflCVHQLvUcRMfP4IU3YdIo",
"formatted_address": "Zeil 1, 60313 Frankfurt am Main, Deutschland",
"street": "Zeil",
"house_number": "1",
"postal_code": "60313",
"city": "Frankfurt am Main"
},
"pickup_time": "2026-07-10T14:30:00+02:00",
"roundtrip": false,
"passengers": 1,
"child_seats": 0,
"flight_number": "LH123",
"return_flight_number": "LH124",
"options": {
"sign_service": false,
"sign_name": "Familie Mustermann"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (201):
{
"data": {
"quote_id": "9b1f2c34-1a2b-4c3d-8e9f-000000000000",
"type": "transfer",
"distance_km": 42,
"roundtrip": true,
"currency": "EUR",
"expires_at": "2026-07-15T14:30:00+02:00",
"vehicles": [
{
"vehicle_id": 3,
"name": "Business Van",
"passengers": 7,
"luggage": 7,
"child_seats": 3,
"image": "vehicles/van.jpg",
"image_url": "https://primos-fahrservice.de/storage/vehicles/van.jpg",
"price": {
"amount": 189,
"currency": "EUR",
"roundtrip_included": true,
"breakdown": [
{
"code": "base_fare",
"label": "Fahrpreis",
"amount": 189
},
{
"code": "roundtrip_discount",
"label": "Rabatt Hin- und Rückfahrt",
"amount": -10
},
{
"code": "child_seats",
"label": "Kindersitze",
"amount": 10
}
]
}
}
]
},
"meta": {
"quote_token": "einmalig-sichtbarer-token"
}
}
Example response (403):
{
"message": "Origin ist für diese Auftragsquelle nicht erlaubt."
}
Example response (422):
{
"message": "pickup.place_id ist erforderlich, sofern die Seite kein Flughafen (airport_iata) ist. Die place_id muss aus Google Places Autocomplete stammen – genau wie im Website-Rechner.",
"errors": {
"pickup.place_id": [
"pickup.place_id ist erforderlich, sofern die Seite kein Flughafen (airport_iata) ist. Die place_id muss aus Google Places Autocomplete stammen – genau wie im Website-Rechner."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
quote_id
string
Kennung des Angebots. Wird für POST /api/v1/bookings gebraucht.
type
string
Fahrttyp des Angebots (transfer oder airport).
distance_km
integer
Von Google ermittelte Strecke in Kilometern – Grundlage des Preises.
roundtrip
boolean
true, wenn der Preis Hin- und Rückfahrt enthält.
currency
string
ISO-4217-Währung aller Beträge der Antwort.
expires_at
string
Ablauf des Angebots (ISO 8601). Danach liefert eine Buchung 409.
vehicles
object[]
Die für diese Fahrt verfügbaren Fahrzeuge samt Preis. Nur diese vehicle_ids sind buchbar.
vehicle_ids sind buchbar.vehicle_id
integer
Kennung des Fahrzeugs, unverändert an die Buchung weiterreichen.
name
string
Anzeigename des Fahrzeugs.
passengers
integer
Maximale Personenzahl.
luggage
integer
Maximale Gepäckstücke.
child_seats
integer
Maximal mitführbare Kindersitze.
image
string
Roher Speicherpfad des Fahrzeugbilds. Für die Anzeige image_url verwenden.
image_url
string
Absolute, direkt einbindbare Bild-URL. null, wenn kein Bild hinterlegt ist.
price
object
Eingefrorener Preis dieses Fahrzeugs.
amount
number
Endpreis inklusive aller Zuschläge und Rabatte. Genau dieser Betrag wird abgerechnet.
currency
string
roundtrip_included
boolean
true, wenn der Betrag beide Fahrtrichtungen abdeckt.
breakdown
object[]
Die Bestandteile des Preises. Die Summe der amount ist exakt price.amount. Nicht angefallene Bestandteile fehlen; base_fare ist immer dabei. Mögliche code-Werte: base_fare, night_surcharge, roundtrip_discount, airport_fee, sign_service, child_seats (siehe GET /api/v1/pricing-rules).
amount ist exakt price.amount. Nicht angefallene Bestandteile fehlen; base_fare ist immer dabei. Mögliche code-Werte: base_fare, night_surcharge, roundtrip_discount, airport_fee, sign_service, child_seats (siehe GET /api/v1/pricing-rules).code
string
Stabiler Bezeichner des Bestandteils – darauf darf im Code gematcht werden.
label
string
Fertiges deutsches Label für die Anzeige.
amount
number
Betrag des Bestandteils; Rabatte sind negativ.
meta
object
quote_token
string
Nur in dieser Antwort sichtbar. Wird zum Buchen gebraucht, gehört serverseitig gespeichert und niemals in den Browser.
Angebot abrufen
requires authentication
Liefert ein zuvor erstelltes Angebot erneut – mit identischen Preisen und identischem
breakdown, aber ohne das einmalige quote_token. Nützlich, um eine Angebotsseite nach
einem Reload wieder aufzubauen, ohne ein neues Angebot (und damit einen neuen Preis) zu erzeugen.
Angebote fremder Auftragsquellen sind nicht sichtbar: Sie liefern 404, nicht 403.
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/quotes/architecto" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/quotes/architecto"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"data": {
"quote_id": "9b1f2c34-1a2b-4c3d-8e9f-000000000000",
"type": "transfer",
"distance_km": 42,
"roundtrip": true,
"currency": "EUR",
"expires_at": "2026-07-15T14:30:00+02:00",
"vehicles": [
{
"vehicle_id": 3,
"name": "Business Van",
"passengers": 7,
"luggage": 7,
"child_seats": 3,
"image": "vehicles/van.jpg",
"image_url": "https://primos-fahrservice.de/storage/vehicles/van.jpg",
"price": {
"amount": 189,
"currency": "EUR",
"roundtrip_included": true,
"breakdown": [
{
"code": "base_fare",
"label": "Fahrpreis",
"amount": 199
},
{
"code": "roundtrip_discount",
"label": "Rabatt Hin- und Rückfahrt",
"amount": -10
}
]
}
}
]
}
}
Example response (404):
{
"message": "No query results for model [App\\Models\\Quote]."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
quote_id
string
Kennung des Angebots.
expires_at
string
Ablauf des Angebots (ISO 8601). Ein abgelaufenes Angebot bleibt abrufbar, ist aber nicht mehr buchbar.
vehicles
object[]
Die eingefrorenen Fahrzeugpreise – identisch zur Antwort von POST /api/v1/quotes, inklusive price.breakdown.
Buchungen
Fahrt buchen
requires authentication
Bucht ein zuvor erstelltes Angebot und legt im CRM eine Fahrt an (Status „Anfrage"), wodurch die Automatismen (Benachrichtigungen, Kalender) ausgelöst werden. Der Preis wird immer aus dem Angebot übernommen – ein im Request gesendeter Preis wird ignoriert.
Optionaler Header Idempotency-Key: Wird derselbe Schlüssel erneut gesendet, wird die
bereits erstellte Buchung zurückgegeben (Status 200 statt 201), ohne eine zweite Fahrt anzulegen.
Dringend empfohlen – ohne ihn erzeugt ein Doppelklick oder ein wiederholter Request nach
Timeout eine zweite Fahrt. Als Wert eignet sich die eigene Bestellnummer oder eine UUID pro
Checkout-Vorgang; der Schlüssel gilt je Auftragsquelle.
Übernommene Angebotsdaten: Strecke, Zeiten, Personen, Kindersitze, Flugnummern und ein gebuchtes Namensschild stammen aus dem Angebot und lassen sich hier nicht mehr ändern. Wer etwas davon ändern will, fragt ein neues Angebot an. Aus dem Request stammen nur Gast, Zahlungsart, Rechnungsanschrift, Notizen und die eigene Referenz.
Example request:
curl --request POST \
"https://primos-fahrservice.de/api/v1/bookings" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Idempotency-Key: abc-123" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"quote_id\": \"9b1f2c34-1a2b-4c3d-8e9f-000000000000\",
\"quote_token\": \"einmalig-sichtbarer-token\",
\"vehicle_id\": 3,
\"customer\": {
\"firstname\": \"Max\",
\"lastname\": \"Mustermann\",
\"email\": \"[email protected]\",
\"phone\": \"+4915112345678\"
},
\"payment_method\": \"invoice\",
\"billing\": {
\"different\": false,
\"name\": \"Musterfirma GmbH\",
\"street\": \"Musterstraße\",
\"house_number\": \"1\",
\"postal_code\": \"60311\",
\"city\": \"Frankfurt am Main\"
},
\"sign_name\": \"Familie Mustermann\",
\"notes\": \"Bitte am Haupteingang warten\",
\"reference\": \"LP-ORDER-5567\"
}"
const url = new URL(
"https://primos-fahrservice.de/api/v1/bookings"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Idempotency-Key": "abc-123",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"quote_id": "9b1f2c34-1a2b-4c3d-8e9f-000000000000",
"quote_token": "einmalig-sichtbarer-token",
"vehicle_id": 3,
"customer": {
"firstname": "Max",
"lastname": "Mustermann",
"email": "[email protected]",
"phone": "+4915112345678"
},
"payment_method": "invoice",
"billing": {
"different": false,
"name": "Musterfirma GmbH",
"street": "Musterstraße",
"house_number": "1",
"postal_code": "60311",
"city": "Frankfurt am Main"
},
"sign_name": "Familie Mustermann",
"notes": "Bitte am Haupteingang warten",
"reference": "LP-ORDER-5567"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (201):
{
"data": {
"booking_id": 12345,
"status": "inquiry",
"type": "transfer",
"vehicle": {
"vehicle_id": 3,
"name": "Business Van"
},
"pickup_time": "2026-07-15T14:30:00+02:00",
"roundtrip": true,
"external_reference": "LP-ORDER-5567",
"price": {
"amount": 189,
"currency": "EUR"
},
"created_at": "2026-07-08T12:00:00+02:00"
}
}
Example response (403):
{
"message": "Ungültiges Angebots-Token."
}
Example response (404):
{
"message": "Angebot nicht gefunden."
}
Example response (409):
{
"message": "Angebot ist abgelaufen oder wurde bereits gebucht."
}
Example response (422):
{
"message": "Das gewählte Fahrzeug ist nicht Teil dieses Angebots.",
"errors": {}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
booking_id
integer
Kennung der angelegten Fahrt. Für GET /api/v1/bookings/{id} und für den Support.
status
string
Status der Fahrt. Direkt nach der Buchung immer inquiry (Anfrage). Weitere Werte: confirmed, booked, done, cancelled, declined, billed.
type
string
Fahrttyp (transfer oder airport), aus dem Angebot übernommen.
vehicle
object
Das gebuchte Fahrzeug.
vehicle_id
integer
Kennung des gebuchten Fahrzeugs.
name
string
Anzeigename des gebuchten Fahrzeugs.
pickup_time
string
Abholzeitpunkt (ISO 8601), aus dem Angebot übernommen.
roundtrip
boolean
true, wenn Hin- und Rückfahrt gebucht wurden.
external_reference
string
Die im Request gesendete reference, unverändert zurückgegeben.
price
object
Abgerechneter Preis – immer der eingefrorene Angebotspreis, nie ein vom Client gesendeter Wert.
amount
number
Endpreis der Fahrt.
currency
string
created_at
string
Zeitpunkt der Buchung (ISO 8601).
Buchungsstatus abrufen
requires authentication
Liefert den aktuellen Status einer Buchung (z. B. inquiry, confirmed, booked, done).
Zum Pollen geeignet, aber sparsam: Der Status ändert sich, wenn die Disposition die Anfrage
bearbeitet – ein Intervall von einigen Minuten reicht. Buchungen fremder Auftragsquellen
liefern 404, nicht 403.
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/bookings/3" \
--header "Authorization: Bearer {YOUR_AUTH_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/bookings/3"
);
const headers = {
"Authorization": "Bearer {YOUR_AUTH_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"data": {
"booking_id": 12345,
"status": "confirmed",
"type": "transfer",
"vehicle": {
"vehicle_id": 3,
"name": "Business Van"
},
"pickup_time": "2026-07-15T14:30:00+02:00",
"roundtrip": true,
"external_reference": "LP-ORDER-5567",
"price": {
"amount": 189,
"currency": "EUR"
},
"created_at": "2026-07-08T12:00:00+02:00"
}
}
Example response (404):
{
"message": "No query results for model [App\\Models\\Drive]."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
status
string
Aktueller Stand der Fahrt: inquiry (Anfrage eingegangen), confirmed (bestätigt), booked (fest eingeplant), done (durchgeführt), cancelled (vom Gast storniert), declined (abgelehnt), billed (abgerechnet).
price
object
amount
number
Abgerechneter Preis der Fahrt.
Status
Public liveness endpoint.
Example request:
curl --request GET \
--get "https://primos-fahrservice.de/api/v1/health" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://primos-fahrservice.de/api/v1/health"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
Show headers
cache-control: no-cache, private
content-type: application/json
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
access-control-allow-origin: *
{
"status": "ok",
"api": "primos-lp",
"version": "v1"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.