Developer docs

Specials

Read the specials a venue runs, day by day — dishes and menu-item deals with their price, weekdays and daily time window, in venue-local time.

The specials a venue runs, grouped by calendar day: a dish that exists only as a special, or a deal on an item from the menu, each with its price, the weekdays it runs on and an optional daily time window. Only published specials appear. It follows the same key rules as the rest of /api/venues/** (Keys and headers), is open to every plan, and answers 404 with a problem body when no venue has that slug.

Endpoints

GET /api/venues/{venueSlug}/specials

Query parameters

Parameter Type Meaning
date yyyy-MM-dd The first day to return. Optional: omitted means the venue's today, in the venue's time zone. Must lie between 2000-01-01 and 9998-12-31
days integer How many days to return, counting date itself. Optional, 1 to 14, default 1

Response 200 OK

Field Type Meaning
slug string The venue
timezone string IANA id of the venue's time zone; every date and time below is local to it
currency string ISO 4217 currency of every price
days[] object[] One entry per requested day, in order, with date and specials[]; the list can be empty

Each entry in specials[]:

Field Type Meaning
id string The special's id
kind string Dish (a dish with its own price) or MenuItem (a headline over an existing menu item)
title, description string The headline and an optional description (nullable)
imageUrl string The special's own picture, else the referenced item's (nullable)
price decimal What the guest pays
regularPrice decimal The item's regular price, present only when the special price is lower, for a strike-through (nullable)
menuItemId, itemName, itemDescription string The referenced menu item, resolved (nullable; null for a dish)
tags string[] The item's tags, for example dietary labels; empty for a dish
windowStart, windowEnd string The daily window as HH:mm; both null means all day
endsNextDay boolean true when the window ends after midnight, for example 22:00 to 02:00
startsOn, endsOn string First and last day it runs as yyyy-MM-dd; null means no bound on that side
weekdays string[] The days of the week it runs on, such as "Wednesday"; empty means every day
occurrenceDate string The day this occurrence started: the listed day, or the day before
continuesFromPreviousDay boolean true for yesterday's late window that is still running at midnight

Within a day the list is ordered: windows carried over from the previous day first, then the operator's sort order, then all-day specials before windowed ones, then window start.

Time zone

All dates and times are venue-local, in the timezone the response names. Convert on your side, never the other way round.

  • A window belongs to the day it starts. 22:00 to 02:00 on Friday is Friday's special and has endsNextDay: true.
  • Each day's list begins with the previous day's late windows that are still running at midnight. They are marked continuesFromPreviousDay: true and carry their occurrenceDate, so asking for today alone is enough the morning after.
  • A special is on now when the venue's clocks show a time inside its window, counted from its occurrenceDate. Compare against the venue's local time, not the guest's.
  • On a daylight-saving night a window can be an hour shorter or longer. One that ends inside the repeated autumn hour switches off and on again.

A special of kind MenuItem points at an item of the venue's menus (Menus) by menuItemId. The response resolves it: itemName, itemDescription and tags come from the item, price is the special price (or the item's own price when the special sets none), and regularPrice is sent only when the special price is lower. If the operator deletes the item, the special disappears from this response rather than showing a dangling reference.

A response for demo-sportsbar could look like this. The dates are only for example: the demo venue's specials roll forward, so the date bounds you get back will differ.

{
  "slug": "demo-sportsbar",
  "timezone": "Europe/Berlin",
  "currency": "EUR",
  "days": [
    {
      "date": "2026-10-28",
      "specials": [
        {
          "id": "5be0a4c1e2f84d5f9a6e0c1d2b3a4f51",
          "kind": "Dish",
          "title": "Lunch special",
          "description": "Soup of the day, burger or salad bowl, with a soft drink",
          "imageUrl": null,
          "price": 9.90,
          "regularPrice": null,
          "menuItemId": null,
          "itemName": null,
          "itemDescription": null,
          "tags": [],
          "windowStart": "11:30",
          "windowEnd": "14:00",
          "endsNextDay": false,
          "startsOn": null,
          "endsOn": null,
          "weekdays": [
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday"
          ],
          "occurrenceDate": "2026-10-28",
          "continuesFromPreviousDay": false
        },
        {
          "id": "5be0a4c1e2f84d5f9a6e0c1d2b3a4f52",
          "kind": "MenuItem",
          "title": "Wing Wednesday",
          "description": null,
          "imageUrl": "_content/BookDinePlay.App.Shared/menu/wings.webp",
          "price": 8.00,
          "regularPrice": 11.00,
          "menuItemId": "af1cc7fd0772f60da833b1612f46fa69",
          "itemName": "Buffalo wings",
          "itemDescription": "Blue-cheese dip, celery",
          "tags": [],
          "windowStart": "17:00",
          "windowEnd": "22:00",
          "endsNextDay": false,
          "startsOn": null,
          "endsOn": null,
          "weekdays": [
            "Wednesday"
          ],
          "occurrenceDate": "2026-10-28",
          "continuesFromPreviousDay": false
        },
        {
          "id": "5be0a4c1e2f84d5f9a6e0c1d2b3a4f53",
          "kind": "Dish",
          "title": "Burger of the week: smoky BBQ",
          "description": null,
          "imageUrl": null,
          "price": 14.50,
          "regularPrice": null,
          "menuItemId": null,
          "itemName": null,
          "itemDescription": null,
          "tags": [],
          "windowStart": null,
          "windowEnd": null,
          "endsNextDay": false,
          "startsOn": "2026-10-26",
          "endsOn": "2026-11-01",
          "weekdays": [],
          "occurrenceDate": "2026-10-28",
          "continuesFromPreviousDay": false
        }
      ]
    }
  ]
}

Errors

  • 400 — a validation problem under errors.date when date is not a yyyy-MM-dd date, or lies outside 2000-01-01 to 9998-12-31; under errors.days when days is outside 1 to 14. Both are reported together when both are wrong.
  • 401 — no key, or an unknown one.
  • 403 — the key belongs to another venue, a publishable key was sent from an origin that is not on its allowlist, or a publishable key was sent without an Origin header. See What the API refuses.
  • 404 — no venue with that slug.

curl

curl "https://api.bookdineplay.com/api/venues/your-venue/specials?date=2026-10-28" \
  -H "X-BookDinePlay-Key: bdp_pk_your_publishable_key" \
  -H "Origin: https://www.your-venue.example"

JavaScript

const client = BookDinePlay.createClient({
  apiBaseUrl: 'https://api.bookdineplay.com', venueSlug: 'your-venue', publishableKey: 'bdp_pk_your_publishable_key'
});
const response = await client.specials({ date: '2026-10-28', days: 1 }); // null when the venue offers no specials
for (const day of response?.days ?? []) for (const special of day.specials) {
  console.log(day.date, special.title, special.price, response.timezone);
}

C#

// `client` is the IBookDinePlayClient from AddBookDinePlayClient, registered with the venue's SECRET key
// read from configuration (builder.Configuration["BookDinePlay:ApiKey"]). A server sends no Origin header.
var response = await client.GetSpecialsAsync("your-venue", new DateOnly(2026, 10, 28), days: 1, cancellationToken);
foreach (var day in response?.Days ?? [])
{
    foreach (var special in day.Specials)
    {
        Console.WriteLine($"{day.Date} {special.Title} {special.Price} ({response!.Timezone})");
    }
}

The JavaScript sample uses the publishable key. The C# sample runs on a server, so it uses a secret key (bdp_sk_…) read from configuration and sends no Origin header; never ship a secret key to a browser. client.specials and GetSpecialsAsync both answer null for an unknown venue, where the plain HTTP call is a 404. renderSpecialsWidget draws the same data without any code of your own.

Operator: saving specials

Operators manage specials through the operator API (bearer token of the venue's operator), not through the public /api/venues/** surface. Always send kind and status on a PUT: it is a full replace, so an omitted status binds to Draft and unpublishes the special, and an omitted kind binds to Dish. The same goes for the date bounds: omitting endsOn (or startsOn) removes that bound and makes the special open-ended.

Next steps

  • Venue — the other six reads for this venue.
  • Menus — the items a menu-item special points at.