MENU navbar-image

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:

  1. GET /api/v1/me – Token prüfen und die enthaltenen Berechtigungen (quote, book, estimate) sehen.
  2. GET /api/v1/airports und GET /api/v1/vehicles – Kataloge abrufen. Die hier gelieferten iata-Codes (Flughäfen) und vehicle_ids sind die einzig gültigen Werte für alle Folgeanfragen; unverändert übernehmen.
  3. (optional) GET /api/v1/pricing-rules – Zuschläge und Rabatte als Zahlen, um Preisbestandteile schon vor dem ersten Angebot zu erklären.
  4. (optional – Marketing/Marktanalyse) POST /api/v1/estimates – unverbindliche Preisschätzung. Speichert nichts und gibt kein Token aus. Das Ergebnis ist nicht buchbar.
  5. POST /api/v1/quotes – verbindliches, zeitlich begrenztes Angebot anfragen. Antwort enthält quote_id sowie ein einmalig sichtbares quote_token.
  6. POST /api/v1/bookings – mit quote_id, quote_token und gewähltem vehicle_id buchen.

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:

Angebots-Lebenszyklus

Ein Angebot (quote) ist ein eingefrorener Preis mit Verfallsdatum:

  1. ErstellenPOST /api/v1/quotes liefert quote_id, expires_at und im meta.quote_token ein Token, das nur in dieser einen Antwort sichtbar ist. Es lässt sich nie wieder abrufen.
  2. Gültigkeit30 Minuten ab Erstellung. Der genaue Zeitpunkt steht in expires_at; immer diesen Wert auswerten, nicht die 30 Minuten selbst nachrechnen.
  3. Einmalverwendung – ein Angebot lässt sich genau einmal buchen. Danach ist es verbraucht.
  4. EndePOST /api/v1/bookings auf ein abgelaufenes oder bereits gebuchtes Angebot liefert 409 mit derselben Meldung. Ein 409 sagt nicht, welcher der beiden Fälle vorliegt. Wer das unterscheiden muss, prüft vorher expires_at bzw. 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:

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:

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

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:

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

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

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."
}
 

Request   

GET api/v1/me

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

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"
        }
    ]
}
 

Request   

GET api/v1/airports

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

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"
        }
    ]
}
 

Request   

GET api/v1/vehicles

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

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"
        }
    }
}
 

Request   

GET api/v1/pricing-rules

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

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_fallbackcustomer_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
                        }
                    ]
                }
            }
        ]
    }
}
 

Request   

POST api/v1/estimates

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

type   string     

Fahrttyp. Erlaubt: transfer (Adresse → Adresse) oder airport (Adresse ↔ Flughafen). Example: airport

Must be one of:
  • transfer
  • airport
pickup   object     

Abholort. Entweder eine Adresse (place_id + formatted_address + street + house_number) oder – nur bei airport – ein Flughafen (airport_iata).

airport_iata   string  optional    

IATA-Code aus GET /api/v1/airports, wenn der Abholort ein Flughafen ist. Nur bei type=airport und genau auf einer der beiden Seiten. Wert unverändert übernehmen (case-sensitive). Must match an existing stored value. Example: fra

place_id   string  optional    

Google-Place-ID des Abholorts, unverändert aus Google Places Autocomplete. Pflicht für Adress-Seiten. Nicht angeben, wenn die Seite ein Flughafen ist. This field is required when pickup.airport_iata is not present. Example: ChIJ7cv00DwsDogRAMDACa2m4K8

formatted_address   string  optional    

Von Google gelieferte, vollständige Adresse (Anzeige & CRM). Pflicht für Adress-Seiten. This field is required when pickup.airport_iata is not present. Must not be greater than 500 characters. Example: Bahnhofstraße 1, 65552 Limburg an der Lahn, Deutschland

street   string  optional    

Straße (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when pickup.airport_iata is not present. Must not be greater than 255 characters. Example: Bahnhofstraße

house_number   string  optional    

Hausnummer (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when pickup.airport_iata is not present. Must not be greater than 50 characters. Example: 1

postal_code   string  optional    

Optionale Postleitzahl. Must not be greater than 20 characters. Example: 65552

city   string  optional    

Optionaler Ort. Must not be greater than 255 characters. Example: Limburg an der Lahn

destination   object     

Zielort. Gleiche Regeln wie pickup: Adresse (place_id + formatted_address + street + house_number) oder – nur bei airport – ein Flughafen (airport_iata).

airport_iata   string  optional    

IATA-Code aus GET /api/v1/airports, wenn das Ziel ein Flughafen ist. Nur bei type=airport und genau auf einer der beiden Seiten. Must match an existing stored value. Example: fra

place_id   string  optional    

Google-Place-ID des Ziels aus Google Places Autocomplete. Pflicht für Adress-Seiten. Nicht angeben, wenn die Seite ein Flughafen ist. This field is required when destination.airport_iata is not present. Example: ChIJeflCVHQLvUcRMfP4IU3YdIo

formatted_address   string  optional    

Von Google gelieferte, vollständige Adresse. Pflicht für Adress-Seiten. This field is required when destination.airport_iata is not present. Must not be greater than 500 characters. Example: Zeil 1, 60313 Frankfurt am Main, Deutschland

street   string  optional    

Straße (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when destination.airport_iata is not present. Must not be greater than 255 characters. Example: Zeil

house_number   string  optional    

Hausnummer (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when destination.airport_iata is not present. Must not be greater than 50 characters. Example: 1

postal_code   string  optional    

Optionale Postleitzahl. Must not be greater than 20 characters. Example: 60313

city   string  optional    

Optionaler Ort. Must not be greater than 255 characters. Example: Frankfurt am Main

pickup_time   string     

Abholzeitpunkt (ISO 8601), muss in der Zukunft liegen. Must be a valid date. Must be a date after now. Example: 2026-07-10T14:30:00+02:00

roundtrip   boolean  optional    

true für Hin- und Rückfahrt. Example: false

return_time   string  optional    

Rückfahrt-Zeitpunkt (ISO 8601), Pflicht bei roundtrip=true, muss nach pickup_time liegen. This field is required when roundtrip is true. Must be a valid date. Must be a date after pickup_time.

passengers   integer     

Anzahl Passagiere (1–99). Must be at least 1. Must not be greater than 99. Example: 1

child_seats   integer  optional    

Anzahl Kindersitze (0–99). Must be at least 0. Must not be greater than 99. Example: 0

flight_number   string  optional    

Optionale Flugnummer der Hinfahrt. Bei type=airport mit Abholung am Flughafen sorgt sie dafür, dass der Fahrer die Landung überwacht. Must not be greater than 20 characters. Example: LH123

return_flight_number   string  optional    

Optionale Flugnummer der Rückfahrt (nur sinnvoll bei roundtrip=true). Must not be greater than 20 characters. Example: LH124

options   object  optional    

Optionale Zusatzleistungen.

sign_service   boolean  optional    

true bucht den Schilderservice (Abholung mit Namensschild). Kostenpflichtig – der Zuschlag ist im Angebotspreis enthalten und wird im breakdown als sign_service ausgewiesen. Wird nur bei type=airport berechnet. Nur dieses Feld ist preisrelevant, der Name auf dem Schild nicht. Example: false

sign_name   string  optional    

Optional: Name, der auf das Namensschild gedruckt wird. Ohne Angabe wird der Name des Bestellers aus der Buchung verwendet. Da der Name den Preis nicht beeinflusst, gehört er üblicherweise erst in POST /api/v1/bookings – ein dort gesendeter sign_name hat Vorrang vor diesem hier. Must not be greater than 255 characters. Example: Familie Mustermann

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.

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.

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."
        ]
    }
}
 

Request   

POST api/v1/quotes

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

type   string     

Fahrttyp. Erlaubt: transfer (Adresse → Adresse) oder airport (Adresse ↔ Flughafen). Example: airport

Must be one of:
  • transfer
  • airport
pickup   object     

Abholort. Entweder eine Adresse (place_id + formatted_address + street + house_number) oder – nur bei airport – ein Flughafen (airport_iata).

airport_iata   string  optional    

IATA-Code aus GET /api/v1/airports, wenn der Abholort ein Flughafen ist. Nur bei type=airport und genau auf einer der beiden Seiten. Wert unverändert übernehmen (case-sensitive). Must match an existing stored value. Example: fra

place_id   string  optional    

Google-Place-ID des Abholorts, unverändert aus Google Places Autocomplete. Pflicht für Adress-Seiten. Nicht angeben, wenn die Seite ein Flughafen ist. This field is required when pickup.airport_iata is not present. Example: ChIJ7cv00DwsDogRAMDACa2m4K8

formatted_address   string  optional    

Von Google gelieferte, vollständige Adresse (Anzeige & CRM). Pflicht für Adress-Seiten. This field is required when pickup.airport_iata is not present. Must not be greater than 500 characters. Example: Bahnhofstraße 1, 65552 Limburg an der Lahn, Deutschland

street   string  optional    

Straße (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when pickup.airport_iata is not present. Must not be greater than 255 characters. Example: Bahnhofstraße

house_number   string  optional    

Hausnummer (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when pickup.airport_iata is not present. Must not be greater than 50 characters. Example: 1

postal_code   string  optional    

Optionale Postleitzahl. Must not be greater than 20 characters. Example: 65552

city   string  optional    

Optionaler Ort. Must not be greater than 255 characters. Example: Limburg an der Lahn

destination   object     

Zielort. Gleiche Regeln wie pickup: Adresse (place_id + formatted_address + street + house_number) oder – nur bei airport – ein Flughafen (airport_iata).

airport_iata   string  optional    

IATA-Code aus GET /api/v1/airports, wenn das Ziel ein Flughafen ist. Nur bei type=airport und genau auf einer der beiden Seiten. Must match an existing stored value. Example: fra

place_id   string  optional    

Google-Place-ID des Ziels aus Google Places Autocomplete. Pflicht für Adress-Seiten. Nicht angeben, wenn die Seite ein Flughafen ist. This field is required when destination.airport_iata is not present. Example: ChIJeflCVHQLvUcRMfP4IU3YdIo

formatted_address   string  optional    

Von Google gelieferte, vollständige Adresse. Pflicht für Adress-Seiten. This field is required when destination.airport_iata is not present. Must not be greater than 500 characters. Example: Zeil 1, 60313 Frankfurt am Main, Deutschland

street   string  optional    

Straße (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when destination.airport_iata is not present. Must not be greater than 255 characters. Example: Zeil

house_number   string  optional    

Hausnummer (aus den Place-Details). Pflicht für Adress-Seiten. This field is required when destination.airport_iata is not present. Must not be greater than 50 characters. Example: 1

postal_code   string  optional    

Optionale Postleitzahl. Must not be greater than 20 characters. Example: 60313

city   string  optional    

Optionaler Ort. Must not be greater than 255 characters. Example: Frankfurt am Main

pickup_time   string     

Abholzeitpunkt (ISO 8601), muss in der Zukunft liegen. Must be a valid date. Must be a date after now. Example: 2026-07-10T14:30:00+02:00

roundtrip   boolean  optional    

true für Hin- und Rückfahrt. Example: false

return_time   string  optional    

Rückfahrt-Zeitpunkt (ISO 8601), Pflicht bei roundtrip=true, muss nach pickup_time liegen. This field is required when roundtrip is true. Must be a valid date. Must be a date after pickup_time.

passengers   integer     

Anzahl Passagiere (1–99). Must be at least 1. Must not be greater than 99. Example: 1

child_seats   integer  optional    

Anzahl Kindersitze (0–99). Must be at least 0. Must not be greater than 99. Example: 0

flight_number   string  optional    

Optionale Flugnummer der Hinfahrt. Bei type=airport mit Abholung am Flughafen sorgt sie dafür, dass der Fahrer die Landung überwacht. Must not be greater than 20 characters. Example: LH123

return_flight_number   string  optional    

Optionale Flugnummer der Rückfahrt (nur sinnvoll bei roundtrip=true). Must not be greater than 20 characters. Example: LH124

options   object  optional    

Optionale Zusatzleistungen.

sign_service   boolean  optional    

true bucht den Schilderservice (Abholung mit Namensschild). Kostenpflichtig – der Zuschlag ist im Angebotspreis enthalten und wird im breakdown als sign_service ausgewiesen. Wird nur bei type=airport berechnet. Nur dieses Feld ist preisrelevant, der Name auf dem Schild nicht. Example: false

sign_name   string  optional    

Optional: Name, der auf das Namensschild gedruckt wird. Ohne Angabe wird der Name des Bestellers aus der Buchung verwendet. Da der Name den Preis nicht beeinflusst, gehört er üblicherweise erst in POST /api/v1/bookings – ein dort gesendeter sign_name hat Vorrang vor diesem hier. Must not be greater than 255 characters. Example: Familie Mustermann

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_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).

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]."
}
 

Request   

GET api/v1/quotes/{id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the quote. Example: architecto

quote   string     

Die quote_id des Angebots. Example: 9b1f2c34-1a2b-4c3d-8e9f-000000000000

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": {}
}
 

Request   

POST api/v1/bookings

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Idempotency-Key        

Example: abc-123

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

quote_id   string     

Die quote_id aus der Antwort von POST /api/v1/quotes. Must be a valid UUID. Must match an existing stored value. Example: 9b1f2c34-1a2b-4c3d-8e9f-000000000000

quote_token   string     

Das meta.quote_token aus derselben Antwort. Nur dort einmalig sichtbar, serverseitig aufbewahren und niemals in den Browser geben. Ein falsches Token liefert 403. Example: einmalig-sichtbarer-token

vehicle_id   integer     

Das vom Gast gewählte Fahrzeug – muss eine vehicle_id aus dem Angebot sein, sonst 422. Der Preis wird aus dem Angebot übernommen. Example: 3

customer   object     

Der buchende Gast. Diese Daten landen im CRM und in den Bestätigungsmails.

firstname   string     

Vorname des Gasts. Must not be greater than 255 characters. Example: Max

lastname   string     

Nachname des Gasts. Must not be greater than 255 characters. Example: Mustermann

email   string     

E-Mail-Adresse. Empfängt die Bestätigung; dient zugleich als Schlüssel für den Ansprechpartner im CRM. Must be a valid email address. Must not be greater than 255 characters. Example: [email protected]

phone   string     

Telefonnummer für Rückfragen des Fahrers, am besten im Format +49…. Must not be greater than 50 characters. Example: +4915112345678

payment_method   string  optional    

Zahlungsart: bar, paypal, ec oder invoice. Ohne Angabe gilt die für die Auftragsquelle hinterlegte Standard-Zahlungsart. Example: invoice

Must be one of:
  • bar
  • paypal
  • ec
  • invoice
billing   object  optional    

Abweichende Rechnungsanschrift. Weglassen, wenn die Rechnung an den Gast geht.

different   boolean  optional    

true, wenn die Rechnungsanschrift von den Gastdaten abweicht. Example: false

name   string  optional    

Rechnungsempfänger (Person oder Firma). Must not be greater than 255 characters. Example: Musterfirma GmbH

street   string  optional    

Straße der Rechnungsanschrift. Must not be greater than 255 characters. Example: Musterstraße

house_number   string  optional    

Hausnummer der Rechnungsanschrift. Must not be greater than 50 characters. Example: 1

postal_code   string  optional    

Postleitzahl der Rechnungsanschrift. Must not be greater than 20 characters. Example: 60311

city   string  optional    

Ort der Rechnungsanschrift. Must not be greater than 255 characters. Example: Frankfurt am Main

sign_name   string  optional    

Name auf dem Namensschild – nur relevant, wenn im Angebot options.sign_service gebucht wurde. Hier gehört er üblicherweise hin, weil er den Preis nicht beeinflusst und der Gast ihn erst mit seinen Kontaktdaten eingibt. Reihenfolge: dieser Wert schlägt ein options.sign_name aus dem Angebot; fehlt beides, wird der Name des Bestellers gedruckt. Ohne gebuchten Schilderservice wird das Feld ignoriert. Must not be greater than 255 characters. Example: Familie Mustermann

notes   string  optional    

Freitext des Gasts für die Disposition (z. B. Treffpunkt, Gepäck). Wird an den Fahrer weitergegeben. Must not be greater than 2000 characters. Example: Bitte am Haupteingang warten

reference   string  optional    

Eigene Bestell-/Vorgangsnummer der Landingpage. Kommt unverändert als external_reference zurück und erleichtert die Zuordnung im Support. Must not be greater than 255 characters. Example: LP-ORDER-5567

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]."
}
 

Request   

GET api/v1/bookings/{drive_id}

Headers

Authorization        

Example: Bearer {YOUR_AUTH_KEY}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

drive_id   integer     

The ID of the drive. Example: 3

drive   integer     

Die booking_id (Fahrt-ID). Example: 12345

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"
}
 

Request   

GET api/v1/health

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json