API Documentation

JSON APIs for connecting DuesterTap menus to your POS, website, or mobile app. All data is live — 86’d (out-of-stock) items are already filtered out. A POS can read the menu and, with write access, 86 items and manage staff PINs.

Two ways in

1. Credentialed POS API (/v1) — key + secret per venue, for point-of-sale and back-office integrations. Requires the plan tier with mobile-app integration; access is requested from the DuesterTap admin (Integration → API) and approved by our team.

2. Public endpoints — keyless, identified by the venue’s public slug. Meant for the venue’s own website embeds and mobile apps.

Base URL for everything: https://api.duestertap.com. Everything returns JSON. Reads are GET; a credentialed key can also write (86 and staff PINs) once write access is granted to that venue — see below.

Credentialed POS API (/v1)

Authentication

Send your venue’s credentials as headers on every request. Get them in the DuesterTap admin under Integration → API (request access → approval → key issued → generate your secret; the secret is shown once and can be regenerated anytime).

curl https://api.duestertap.com/v1/menu \ -H "X-API-Key: dtk_your_api_key" \ -H "X-API-Secret: dts_your_api_secret"

Invalid, revoked, or plan-lapsed credentials return 401. Unknown ids return 404. Poll as often as you need — responses are uncached and always current.

GET/v1/me

Who your credentials belong to and what they may do. Call it once at setup to validate the key, show which venue you’re connected to, and learn whether write access is on — without having to attempt a write and watch for the 403.

{ "venue": { "id": "…", "name": "Willow Brook", "slug": "willow-brook" }, "write_access": true, "locations": [ { "id": "…", "name": "Main Bar", "slug": "main-bar" } ], "beer_classes": [ { "key": "flagship", "label": "Flagship" }, { "key": "seasonal", "label": "Seasonal" } ], "option_groups": [ { "id": "…", "name": "Milk", "required": true, "multi": false, "options": [ { "id": "…", "name": "Whole", "price_delta": null, "default": true }, { "id": "…", "name": "Oat", "price_delta": 0.75, "default": false } ] } ] }

beer_classes maps the beer_class key on item rows to its display label (empty when the venue doesn’t use classes). Items carry the key, so a venue renaming a class never breaks your integration.

option_groups is the venue’s dictionary of reusable choice sets (“Milk”, “Size”). required: the customer must choose. multi: pick-many vs pick-one. price_delta is the signed dollar change when picked (null = free); a pick-one group has at most one default: true, and a required pick-one group with no default means the customer must actively choose. Option ids are stable across renames. Items reference these via option_group_ids and also inline the full definitions.

GET/v1/menu

The venue’s published menu — boards and items exactly as their screens display them.

{ "venue": { "id": "…", "name": "…", "slug": "…" }, "menu": { "id": "…", "name": "Main Menu", "boards": [ { "id": "…", "name": "On Tap", "board_type": "menu", "items": [ { "id": "…", "name": "Item Name", "description": "…", "style": "IPA", "abv": 6.5, "ibu": 55, "price": 7.00, "price_tiers": [ { "size": "16 oz", "price": 6.95 }, { "size": "32 oz", "price": 13.90 } ], "size_oz": 16, "tap_number": 1, "beer_class": "seasonal", "option_group_ids": [ "…" ], "option_groups": [ { "id": "…", "name": "Milk", "required": true, "multi": false, "options": [ { "id": "…", "name": "Oat", "price_delta": 0.75, "default": false } ] } ], "image_url": "…", "modifications": [ { "action": "add", "name": "Cheese", "price_delta": 0.75 }, { "action": "remove", "name": "Lettuce", "price_delta": null } ] } ] } ] } }

modifications appears on food items only — the add-ons and removals a guest can order (“+ Cheese”, “No Lettuce”). action is "add" or "remove". price_delta is the signed change to the item price: positive is an upcharge, negative a discount, null no charge. Items without modifications omit the field or return null.

Slideshow boards: a board can be "board_type": "slideshow" instead of a menu of items. It carries the slide deck directly:

{ "id": "…", "name": "band logos", "board_type": "slideshow", "slideshow_interval_sec": 10, "slideshow_images": [ { "url": "https://…/uploads/….jpg", "type": "image", "enabled": true, "sort_order": 0, "schedule": { "start": "17:00", "end": "22:00", "startDate": "2026-08-22", "endDate": "2026-08-22" } } ] }

The urls are plain public image/video URLs — fetch them directly. If you re-display slides elsewhere, honor enabled and schedule the way the venue’s own screens do: schedule may carry a date+time window (band backdrops do — showing one outside its window puts the wrong band on your surface), a daily time range, or {"days":[…]} weekday recurrence (0 = Sunday). type can be "video" (mp4/webm). Slideshow menus are usually reached via /v1/locations/{locationId}/menu — plain /v1/menu returns only the venue’s most recently published menu, which may not be the slideshow one.

GET/v1/locations

The venue’s locations with their ids — for a location picker, or for location-scoped 86 calls. Venues with a single bar may have none; use "global" scope in that case.

{ "locations": [ { "id": "…", "name": "Main Bar", "slug": "main-bar" } ] }

GET/v1/locations/{locationId}/menu

The published menu for one physical location (multi-location venues). Same response shape as /v1/menu, with that location’s 86 list applied.

GET/v1/categories

Every item list (category) in the venue’s Item Library.

{ "venue": { "id": "…", "name": "…", "slug": "…" }, "categories": [ { "id": "…", "name": "All Beers", "category_type": "beer" }, { "id": "…", "name": "Food", "category_type": "food" } ] }

GET/v1/categories/{categoryId}/items

Live items in one list, with out-of-stock items already removed.

{ "venue": { "id": "…", "name": "…", "slug": "…" }, "category": { "id": "…", "name": "All Beers" }, "items": [ { "id": "…", "name": "Item Name", "description": "…", "style": "IPA", "abv": 6.5, "ibu": 55, "price": 7.00, "price_tiers": [ { "size": "16 oz", "price": 6.95 }, { "size": "32 oz", "price": 13.90 } ], "size_oz": 16, "tap_number": 1, "beer_class": "seasonal", "option_group_ids": [ "…" ], "option_groups": [ { "id": "…", "name": "Milk", "required": true, "multi": false, "options": [ { "id": "…", "name": "Oat", "price_delta": 0.75, "default": false } ] } ], "image_url": "…", "modifications": [ { "action": "add", "name": "Cheese", "price_delta": 0.75 }, { "action": "remove", "name": "Lettuce", "price_delta": null } ] } ] }

Food items may include modifications — same shape and meaning as on /v1/menu above.

Pricing: an item is priced by price_tiers (per pour size) or by the single price — venues that price by pour leave price null. Check tiers first, fall back to price; treat an item as unpriced only when both are empty.

Write endpoints (86 & staff PINs)

Approval issues a read-only key. Writing is a separate grant, because a staff PIN opens the staff console — a venue asks for it under Integration → API and it is enabled per venue. Until then every non-GET call returns 403.

GET/v1/86

Everything currently 86’d. Read this when your integration starts: staff also 86 from the staff console and the admin, so your POS is not the only writer.

{ "items": [ { "item_id": "…", "name": "Cheese Curds", "scope": "global", "location_id": null, "location_name": null, "removed_at": "…" }, { "item_id": "…", "name": "Willow Burger", "scope": "location", "location_id": "…", "location_name": "Bar", "removed_at": "…" } ] }

POST/v1/86

Mark an item 86’d. It disappears from the screens and from every read endpoint immediately — the affected menus re-publish themselves.

{ "item_id": "…", // the id from any read endpoint "location_id": "global", // "global" (default) or one of your location ids "staff_pin_id": "…" // optional: attribute it to a person instead of the integration }

Items are addressed by DuesterTap id, never by name — names get edited mid-service and two items can share one. 86ing an item from the Item Library applies to every copy of it across your menus. Location ids come from GET /v1/locations.

DELETE/v1/86/{itemId}

Put it back. Add ?location_id=… to clear a 86 that was set at one location; omit it for a venue-wide one.

GET/v1/staff/roles

The roles the venue has defined, for use as role_id below.

{ "roles": [ { "id": "…", "name": "Bartender" } ] }

GET/v1/staff/pins

Staff PINs at this venue. The PIN itself is not returned — a list endpoint that handed back every PIN would turn one leaked key into every staff login at once.

{ "pins": [ { "id": "…", "display_name": "Sam", "role_id": "…", "role_name": "Bartender", "created_at": "…" } ] }

POST/v1/staff/pins

Create a PIN. 4–8 digits, unique within the venue. role_id is optional and must be a role the venue already created, so the API can never grant more access than the venue has set up.

{ "pin": "4821", "display_name": "Sam", "role_id": "…" }

The response echoes the created PIN once, so you can show it to whoever it was made for. It is not retrievable from the API afterwards.

DELETE/v1/staff/pins/{pinId}

Remove a PIN. That person can no longer sign in to the staff console.

Public endpoints (keyless)

For a venue’s own website and mobile app. Identified by the venue’s public slug — no credentials required.

GET/public/venues/{slug}/menu

The published menu (same shape as /v1/menu). Add /public/venues/{slug}/locations/{locationId}/menu for a specific location.

GET/public/venues/{slug}/catalog

All item lists (same shape as /v1/categories).

GET/public/venues/{slug}/catalog/{categoryId}

Live items in one list (same shape as /v1/categories/{id}/items).

GET/public/venues/{slug}/catalog/{categoryId}/widget.js

A drop-in JavaScript widget that renders the list on any website. Configuration options are generated in the admin under Integration → Website Embed.

Freshness & stability

Getting access

POS API access is included with the DuesterTap plan tier that has mobile-app integration. In the admin console, go to Integration → API, tell us what you’re connecting, and you’ll get your credentials after a quick review — usually within a business day. Questions: support@duestertap.com.