Entwicklerdokumentation

.NET-SDK

BookDinePlay.Sdk installieren, den Client mit einem geheimen Schlüssel registrieren und Verfügbarkeit und Reservierungen aus der eigenen Anwendung nutzen.

BookDinePlay.Sdk ist ein typisierter Client für die öffentliche API – für alles, was auf einem Server läuft: Ihre eigene Buchungsseite, ein internes Werkzeug, ein Dienst, der Reservierungen synchronisiert. Er gibt dieselben DTOs zurück wie die API und fügt nichts hinzu, was Sie nicht verlangt haben – keine Wiederholungen, kein Caching, keine Hintergrundarbeit.

Installation

dotnet add package BookDinePlay.Sdk

Zielplattform .NET 10. Die Contracts und die Schnittstelle IBookDinePlayClient liegen in BookDinePlay.Shared, das als Abhängigkeit mitkommt.

Client registrieren

Das SDK läuft auf einem Server und verwendet daher einen geheimen Schlüssel (bdp_sk_…), angelegt in der Konsole unter Location → API-Schlüssel und einmalig angezeigt. Halten Sie ihn in der Konfiguration oder einem Secret-Store, nie im Quelltext:

builder.Services.AddBookDinePlayClient(
    new Uri("https://api.bookdineplay.com"),
    builder.Configuration["BookDinePlay:ApiKey"]!);

Das registriert einen typisierten HttpClient, der den Schlüssel bei jeder Anfrage als X-BookDinePlay-Key sendet, und IBookDinePlayClient für die Injektion. Ein veröffentlichbarer Schlüssel funktioniert hier nicht: Die API lehnt ihn ohne Browser-Origin ab, und ein Server hat keine.

Location-Daten lesen

Jeder Lesezugriff gibt für eine unbekannte Location null zurück, statt eine Ausnahme zu werfen:

var venue = await client.GetVenueAsync("your-venue", cancellationToken);
var hours = await client.GetOpeningHoursAsync("your-venue", cancellationToken);
var menus = await client.GetMenusAsync("your-venue", cancellationToken);
var resources = await client.GetResourcesAsync("your-venue", cancellationToken);

GetBusinessInfoAsync und GetFloorPlanAsync vervollständigen den Satz, und Specials hat einen eigenen Abschnitt.

Verfügbarkeit prüfen und buchen

Die Verfügbarkeit nimmt das lokale Datum der Location, die Personenzahl, den Ressourcentyp und eine optionale Dauer; eine Reservierung sendet den vom Gast gewählten Slot plus Kontaktdaten:

var availability = await client.GetAvailabilityAsync(
    "your-venue",
    DateOnly.FromDateTime(DateTime.Today),
    partySize: 4,
    BookableResourceType.RestaurantTable,
    cancellationToken: cancellationToken);

var slot = availability?.Slots.FirstOrDefault(s => s.Available);
if (slot is null)
{
    return; // an diesem Tag ist nichts frei
}

var reservation = await client.CreateReservationAsync("your-venue", new CreateReservationRequest
{
    ResourceType = BookableResourceType.RestaurantTable,
    ResourceId = slot.ResourceId,
    Date = availability.Date,
    Start = slot.Start,
    PartySize = 4,
    CustomerName = "Jana Berger",
    Email = "jana@example.com",
    Phone = "+49 30 1234567",
}, cancellationToken);

Console.WriteLine($"Gebucht: {reservation.Reference} um {reservation.Start} ({reservation.Status})");

Date ist yyyy-MM-dd und Start ist HH:mm, beide in der Ortszeit der Location – genau das, was die Verfügbarkeitsantwort geliefert hat. Optionale Felder decken Notizen, eine Spielanzahl bei Preis pro Spiel, buchbare Extras, eine Anzahlungs-Zustimmung und eine Dauer ab.

CreateReservationAsync hat eine zweite Überladung, die ein idempotencyKey vor dem Cancellation-Token nimmt – pro Buchungsversuch eines erzeugen (Guid.NewGuid().ToString()) und bei einer Wiederholung nach Timeout oder einem vorübergehenden Fehler DENSELBEN Wert übergeben, damit die API die Original-Reservierung statt einer zweiten zurückliefert. Der vollständige Vertrag steht unter Sicher wiederholen mit Idempotency-Key.

var request = new CreateReservationRequest
{
    ResourceType = BookableResourceType.RestaurantTable,
    ResourceId = slot.ResourceId,
    Date = availability.Date,
    Start = slot.Start,
    PartySize = 4,
    CustomerName = "Jana Berger",
    Email = "jana@example.com",
    Phone = "+49 30 1234567",
};

var key = Guid.NewGuid().ToString();

// Dieselbe Anfrage unter demselben Key wiederholen: der zweite Aufruf liefert die
// erste Reservierung zurück, statt eine zweite anzulegen.
var reservation = await client.CreateReservationAsync("your-venue", request, key, CancellationToken.None);

Wird der Client mit AddBookDinePlayClient registriert und kein eigener Key übergeben, erzeugt das SDK selbst einen: pro Aufruf ein frischer Wert, angehängt bevor eine Wiederholung auf Transportebene greifen kann. Eine automatische Wiederholung einer Reservierungsanlage – durch den Resilience-Handler eines .NET-Aspire-Hosts oder durch eine eigene Retry-Policy – erreicht die API damit unter einem einzigen Key statt wie mehrere Buchungen auszusehen. Zwei getrennte Aufrufe bekommen immer getrennte Keys, absichtlich doppelt Gebuchtes wird also nie zusammengefasst. Ein selbst übergebener Key gewinnt immer, und betroffen ist ausschließlich die Reservierungsanlage; kein anderer Aufruf erhält einen Header.

Events

Eine Location auf Pro oder höher veröffentlicht Events; Gäste melden sich an und erhalten ein namentliches Ticket pro Teilnehmer (Events-API). Derselbe Client deckt die sieben öffentlichen Aufrufe ab. Die beiden Lesezugriffe geben bei unbekannter Location oder unbekanntem Event null zurück; eine Anmeldung oder ein Checkout wirft bei einer Ablehnung:

var events = await client.GetEventsAsync("your-venue", cancellationToken: cancellationToken);      // nur anstehende
var all = await client.GetEventsAsync("your-venue", upcoming: false, cancellationToken);           // auch vergangene datierte Events
var detail = await client.GetEventAsync("your-venue", "darts-league-night", cancellationToken);    // null bei unbekannt oder unveröffentlicht

if (detail?.Registration is { Open: true, Mode: EventRegistrationMode.Free })
{
    var registration = await client.RegisterForEventAsync("your-venue", "darts-league-night",
        new EventRegistrationRequest
        {
            BuyerName = "Ada Lovelace",
            Email = "ada@example.com",
            Quantity = 2,
            Attendees = ["", "Grace Hopper"]   // der erste fällt auf den Käufer zurück
        }, cancellationToken);

    Console.WriteLine($"{registration.Reference}: {registration.Tickets.Count} Tickets, Storno unter {registration.CancelUrl}");
}
else if (detail?.Registration is { Open: true, Mode: EventRegistrationMode.Paid } paid)
{
    var checkout = await client.CheckoutEventTicketsAsync("your-venue", "gala-night",
        new EventTicketCheckoutRequest
        {
            BuyerName = "Ada Lovelace",
            Email = "ada@example.com",
            Quantity = 2,
            Attendees = ["", "Grace Hopper"],
            SuccessUrl = "https://ihre-website.example/danke",   // optional; sonst die eingebaute Anmeldeseite
            CancelUrl = "https://ihre-website.example/tickets"    // optional
        }, cancellationToken);

    // Gast zu Stripe schicken; Tickets werden ausgestellt, sobald die Zahlung abgeschlossen ist.
    Console.WriteLine($"Weiterleitung zu {checkout.CheckoutUrl} (läuft ab {checkout.ExpiresAt})");
}

EventDetailResponse.Registration ist das Fenster aus Das Anmeldefenster – Mode, Price, Currency, Capacity, Remaining, MaxPerRegistration, ClosesAt, Open, Reason. Eine abgelehnte Anmeldung oder ein abgelehnter Checkout wirft BookDinePlayApiException mit dem Problem auf Eigenschaften gehoben, sodass Sie verzweigen, statt JSON zu parsen:

Member Bedeutung
StatusCode Der HTTP-Status
Type Die type-URL des Problems oder null. Vergleichen Sie mit den Konstanten BookDinePlayApiException.RegistrationClosedType, BookDinePlayApiException.TicketSalesNotEnabledType, BookDinePlayApiException.PayoutsNotConnectedType, BookDinePlayApiException.RefundNotAvailableType und BookDinePlayApiException.TenantSuspendedType
Reason Bei registration-closed: "closed", "sold-out", "past" oder "not-open"; sonst null
Remaining 0 bei Ausverkauf; sonst null
Errors Das Validierungs-Wörterbuch einer 400 – Feldname (oder "registration" bei einer verletzten Regel) zu Meldungen; leer, wenn keine
Messages Jede Meldung aus Errors, flach
Extensions Jedes weitere Mitglied des Problems (etwa requiredPlan bei einer 403) als rohe JsonElements
try
{
    var checkout = await client.CheckoutEventTicketsAsync("your-venue", slug, request, cancellationToken);
}
catch (BookDinePlayApiException ex) when (ex.Type == BookDinePlayApiException.RegistrationClosedType)
{
    // ex.Reason: "closed" | "sold-out" | "past" | "not-open"; ex.Remaining ist 0 bei Ausverkauf
}
catch (BookDinePlayApiException ex) when (ex.Type is BookDinePlayApiException.TicketSalesNotEnabledType or BookDinePlayApiException.PayoutsNotConnectedType)
{
    // ein bezahltes Event: der Online-Ticketverkauf ist gerade nicht verfügbar (drei Ursachen zu
    // einer zusammengefasst – siehe https://bookdineplay.com/docs/api/errors#ticket-sales-not-enabled).
    // Für beide denselben Satz zeigen.
}
catch (BookDinePlayApiException ex) when (ex.StatusCode == HttpStatusCode.BadRequest)
{
    // ex.Errors / ex.Messages: was zu korrigieren ist – auch errors.successUrl/errors.cancelUrl, wenn
    // eine Rückkehr-URL die Origin-Prüfung nicht bestanden hat
}

Drei Aufrufe nehmen keinen API-Schlüssel – Ticketcode, das Storno-Token des Gastes und (für GetRegistrationAsync) dasselbe Storno-Token sind die Berechtigung, die der Gast per E-Mail erhalten hat (Warum drei Routen keinen Schlüssel brauchen):

var ticket = await client.GetTicketAsync("ABCDEFGHJKLM", cancellationToken);                 // null bei unbekannt
var summary = await client.GetRegistrationAsync(cancelToken, cancellationToken);             // null bei unbekannt; Payment spiegelt ein laufendes bezahltes Checkout
var cancelled = await client.CancelEventRegistrationAsync(cancelToken, cancellationToken);   // null bei unbekannt; wirft 409 sobald eingecheckt, oder bei einer bezahlten Anmeldung, deren Rückerstattungsrichtlinie es nicht mehr erlaubt

AddBookDinePlayClient registriert einen KeylessRouteHandler, der den X-BookDinePlay-Key-Header – und einen Bearer bdp_…-Authorization-Header – aus jeder Anfrage entfernt, die der Client als schlüssellos markiert, damit Ihr geheimer Schlüssel nie dorthin reist, wo niemand nach ihm gefragt hat. Das sind diese drei, die eigene Buchung des Gasts (GetReservationAsync, CancelReservationAsync), die QR-Tischrouten (ResolveQrTokenAsync, GetTableSessionAsync, PlaceOrderAsync, CloseTableSessionAsync, StartTabPaymentAsync) – schlüssellos aus demselben Grund: Das am Tisch gescannte Token ist die Berechtigung – und die Gutscheinabfrage GetVoucherAsync (Geschenkgutscheine). Bauen Sie den HttpClient selbst und setzen den Schlüssel als Standard-Header, fügen Sie den Handler in Ihre eigene Pipeline ein (new KeylessRouteHandler { InnerHandler = … }); ohne ihn mischt HttpClient den Standard-Header auch in diese Anfragen. Die Routen ignorieren einen Schlüssel, es bricht also nichts – der Handler ist Hygiene, keine Voraussetzung.

Geschenkgutscheine

Eine Location verkauft Geschenkgutscheine online, und Gäste lösen sie für Anzahlungen, Tickets und Tisch-Rechnungen ein (Gutscheine). Der Client hat drei Gutschein-Aufrufe und eine Überladung; eine Anzahlung oder Tickets mit einem Gutschein zu bezahlen braucht keine neue Methode, nur VoucherCode auf der Anfrage, die Sie ohnehin senden:

// Verkaufen: was die Location online anbietet, dann ein Checkout für einen Gutschein.
var offers = await client.GetVoucherOffersAsync("your-venue", cancellationToken);   // null, wenn die Location unbekannt ist
if (offers is { OnlineSaleAvailable: true })
{
    var checkout = await client.StartVoucherCheckoutAsync("your-venue",
        new StartVoucherCheckoutRequest
        {
            ProductId = offers.Offers[0].ProductId,
            BuyerName = "Ben Buyer",
            BuyerEmail = "ben@example.com",
            RecipientName = "Ada",               // optional, wie E-Mail der beschenkten Person, Nachricht und Versanddatum
            SuccessUrl = "https://your-site.example/thanks"
        },
        idempotencyKey: null, cancellationToken); // null: der Client erzeugt einen je Aufruf

    Console.WriteLine($"{checkout.Reference}: weiterleiten zu {checkout.CheckoutUrl}");
}

// Abfragen: ohne Schlüssel, null für jeden Code, der hier kein Gutschein ist.
var voucher = await client.GetVoucherAsync(code, cancellationToken);

// Einlösen: Anzahlung, Tickets oder Tisch-Rechnung, die Karte zahlt den Rest.
var intent = await client.CreatePaymentIntentAsync("your-venue", new CreatePaymentIntentRequest
{
    ReservationReference = reservation.Reference,
    Amount = reservation.DepositAmount ?? 0m,   // genau die Anzahlung
    Currency = reservation.Currency,
    VoucherCode = code
}, cancellationToken);
var paidInFull = intent.CardAmount == 0m;       // keine Kartenzahlung; CheckoutUrl ist die Bestätigungsseite

var tab = await client.StartTabPaymentAsync(qrToken, code, cancellationToken);   // null bei 404/400; eine 409-Ablehnung wirft
  • GetVoucherOffersAsync antwortet in jedem Tarif; OnlineSaleAvailable und UnavailableReason sagen, ob ein Checkout starten kann.
  • StartVoucherCheckoutAsync wirft bei einer Ablehnung BookDinePlayApiException. Bleibt idempotencyKey null, erzeugt ein Client aus AddBookDinePlayClient je Aufruf einen Idempotency-Key, wie bei Reservierungen, sodass ein Transport-Retry keine zwei Checkouts öffnen kann. Der Code des Gutscheins steht nicht in der Antwort: Er erreicht den Käufer per E-Mail, sobald die Zahlung eintrifft.
  • GetVoucherAsync nimmt den Code in jeder Form, die ein Gast tippt oder scannt, eine eingefügte URL der Gastseite eingeschlossen, und sendet keinen API-Schlüssel. Für jeden Code, der hier kein Gutschein ist, liefert es gleichermaßen null; ein 429 der Ratenbegrenzung wirft BookDinePlayApiException.
  • EventTicketCheckoutRequest.VoucherCode und CreatePaymentIntentRequest.VoucherCode bezahlen ganz oder teilweise mit einem Gutschein; VoucherAmount und AmountDue/CardAmount in der Antwort sagen, wer was bezahlt. Ein abgelehnter Gutschein wirft mit einem der Gutschein-Problemtypen in Type.
  • StartTabPaymentAsync(token, voucherCode, cancellationToken) kommt wie die anderen QR-Aufrufe ohne Schlüssel aus. Ein null- oder leerer Code ist der Aufruf mit zwei Argumenten. PaidInFull heißt, der Gutschein hat die Rechnung gedeckt; die Methode liefert null bei einem 404 (das Token löst sich nicht auf oder der Gutschein wird nicht gefunden) oder einem 400 und wirft BookDinePlayApiException bei einem 409 – ein aufgebrauchter, abgelaufener, nicht anwendbarer oder gerade verwendeter Gutschein, ein Kartenteil unter dem Mindestbetrag (Extensions["reason"] ist below-card-minimum), eine schon bezahlte oder leere Rechnung oder eine Location, die gerade keine Karten annimmt.

Ein Gutscheincode ist ein Inhaber-Credential wie ein Ticketcode: nie loggen und aus Ihren eigenen URLs heraushalten.

Specials

Die Specials einer Location, tageweise: Gerichte und Angebote auf Speisekartenpositionen mit Preis, Wochentagen und Zeitfenster pro Tag, in der Ortszeit der Location (Specials-API). Sie stehen jedem Tarif offen. GetSpecialsAsync nimmt den ersten Tag als DateOnly in der Zeitzone der Location und wie viele Tage zurückkommen sollen (1 bis 14, Standard 1) und antwortet bei einer unbekannten Location wie die anderen Lesezugriffe mit null:

var response = await client.GetSpecialsAsync("your-venue", new DateOnly(2026, 10, 28), days: 7, cancellationToken);

foreach (var day in response?.Days ?? [])
{
    foreach (var special in day.Specials)
    {
        Console.WriteLine($"{day.Date} {special.Title}: {special.Price} {response!.Currency} ({response.Timezone})");
    }
}

Das Datum ist Pflicht und der Kalendertag der Location, nicht Ihrer: Übergeben Sie nicht DateTime.Today, sondern den Tag in der Location (das Timezone der Antwort nennt die Zone), sonst fragt ein Server in einer anderen Zeitzone den falschen Tag ab. Nur ein days ungleich 1 geht über die Leitung. Ein Wert außerhalb von 1 bis 14 oder ein Datum außerhalb des Bereichs der API wirft BookDinePlayApiException mit 400 und dem Feld unter Errors. Jedes Special trägt sein Fenster als WindowStart/WindowEnd und EndsNextDay sowie ContinuesFromPreviousDay für ein spätes Fenster, das um Mitternacht noch läuft; was gerade läuft, ist also ein Vergleich mit der Ortszeit der Location.

Fehler

  • Lesezugriffe (Get…Async) geben bei 404 null zurück und werfen bei jedem anderen Fehler BookDinePlayApiException (mit StatusCode).
  • Schreibzugriffe (CreateReservationAsync, CreatePaymentIntentAsync, RegisterForEventAsync, CheckoutEventTicketsAsync, StartVoucherCheckoutAsync) werfen bei einer Ablehnung BookDinePlayApiException – ein inzwischen vergebener Slot, Validierungsfehler, ein Schlüsselproblem. Die Meldung trägt das Problem-detail der API; Type, Errors und die weiteren Member unter Events sind bei jedem Problem gefüllt, nicht nur bei Event-Problemen.
  • Schlüssel-Ablehnungen sind 401/403 mit den Gründen unter Authentifizierung.
try
{
    var reservation = await client.CreateReservationAsync("your-venue", request, cancellationToken);
}
catch (BookDinePlayApiException ex) when (ex.StatusCode == HttpStatusCode.Conflict)
{
    // der Slot wurde zwischen Verfügbarkeit und Buchung vergeben – neu prüfen und einen anderen anbieten
}

Weitere Endpunkte

Der Client kapselt außerdem Zahlungsabsichten (CreatePaymentIntentAsync), den QR-/Tischsitzungs-Ablauf für Bestellungen vor Ort (ResolveQrTokenAsync, GetTableSessionAsync, PlaceOrderAsync, CloseTableSessionAsync, StartTabPaymentAsync), Gäste-Nachrichten (StartConversationAsync) und die Event- und Gutschein-Aufrufe oben. Die Methodennamen folgen den API-Routen eins zu eins.

Resilienz und Tests

Der Client ist ein dünner HttpClient-Wrapper: Wiederholungen oder Timeouts ergänzen Sie mit Ihrer eigenen HttpClient-Konfiguration (etwa Microsoft.Extensions.Http.Resilience) auf der Registrierung, die AddBookDinePlayClient zurückgibt. In Tests injizieren Sie ein Fake-IBookDinePlayClient – die Schnittstelle liegt in BookDinePlay.Shared, Testprojekte brauchen das SDK-Paket also gar nicht.

Binärkompatibilität über Versionen hinweg

BookDinePlay.Shared wird auf NuGet veröffentlicht, und seine öffentlichen Records wachsen, indem am Ende ein optionaler Parameter angehängt wird – Active, SlotDurationOverrideMinutes und ResourceGroupId auf BookableResourceDto sind alle auf diesem Weg dazugekommen, zuletzt seatSold (siehe das Changelog). Das erhält die Quellcode-Kompatibilität: Code, der den Konstruktor bereits aufruft oder den Record mit with baut, kompiliert unverändert gegen eine neuere Paketversion weiter.

Es erhält nicht die Binärkompatibilität – und der Unterschied ist relevant, sobald Sie ein Versions-Update einer Abhängigkeit übernehmen, ohne Ihr eigenes Projekt dagegen neu zu kompilieren: ein transitives NuGet-Restore, ein aus einer Lockfile neu gebautes Container-Image, ein Plugin, das in einen Host geladen wird, der bereits ein älteres BookDinePlay.Shared in seinem Suchpfad trägt.

// Ein veröffentlichter Record wächst jedes Mal auf demselben Weg: ein neuer Parameter am Ende,
// mit einem Standardwert – z. B. BookableResourceDtos eigenes Active, dann
// SlotDurationOverrideMinutes, dann ResourceGroupId, dann SeatSold, jeweils in einem späteren
// Release auf diese Weise hinzugefügt.
public sealed record ExampleDto(string Id, string Name, bool Active = true);

// Code, der gegen das ÄLTERE Paket kompiliert wurde, erzeugt einen Aufruf des Konstruktors, den
// er zur Build-Zeit gesehen hat:
var dto = new ExampleDto("lane-1", "Lane 1");   // 2 Argumente – Active bekommt seinen Standardwert zur Compile-Zeit

// Ein neueres Paket fügt demselben Record einen weiteren, am Ende angehängten Parameter mit
// Standardwert hinzu (SeatSold). EIN STANDARDWERT FÜR EINEN PARAMETER IST EIN
// COMPILE-ZEIT-KONSTRUKT – der obige Aufruf zielt weiterhin auf den 2-Argumente-Konstruktor.
// Wird nur die Assembly darunter ausgetauscht (kein Neukompilieren, kein neuer Quellcode),
// existiert dieser Overload nicht mehr: MissingMethodException zur Laufzeit, kein Build-Fehler.

Der generierte Deconstruct ändert die Stelligkeit auf dieselbe Weise, sodass var (id, name, …) = dto oder ein positionelles Pattern (dto is (var id, var name, …)), das gegen die alte Form geschrieben wurde, beim nächsten Build nicht mehr kompiliert – ein quellcode-seitiger Bruch im selben Release, das zur Laufzeit für alle, die nicht neu bauen, binärbrechend war.

Was das für Sie bedeutet: Behandeln Sie ein Versions-Update von BookDinePlay.Shared/BookDinePlay.Sdk wie jedes andere Abhängigkeits-Upgrade, das ein echtes Neukompilieren braucht, nicht wie einen austauschbaren Binär-Drop-in – kompilieren Sie Ihr Projekt gegen das neue Paket neu, statt nur die DLL auf der Festplatte auszutauschen oder sich auf eine Container-Schicht zu verlassen, die nur den Paket-Cache aktualisiert. Das ist bei uns eine bewusste, feststehende Konvention (neue Felder kommen weiterhin als am Ende angehängte optionale Konstruktorparameter hinzu, nicht als neue Typen oder nicht-positionelle Records) – keine geplante Änderung, und keine Bitte an uns, das zu ändern. Das Changelog listet release-weise genau, welche Records gewachsen sind, damit Sie wissen, wann ein Neukompilieren fällig ist.

Nächste Schritte