Your shop, from your own apps and AI
Everything the Next app shows you is readable from outside it: who is waiting, the wait right now, who is here, the numbers, the reviews. A REST API under /v1/ for your code, and an MCP server for Claude, Cursor, ChatGPT and any other AI tool.
https://tatooine.hellagoodnext.com
Six reads today. Writes (walk-ups, Notify, Done) are coming under the same key.
Comes with the plan. One key per shop, made and revoked by the owner.
curl https://tatooine.hellagoodnext.com/v1/line \
-H "Authorization: Bearer hgn_live_YOUR_KEY"
const r = await fetch("https://tatooine.hellagoodnext.com/v1/line", { headers: { Authorization: "Bearer hgn_live_YOUR_KEY" } }); const { waiting, line } = await r.json(); console.log(waiting, line.map(g => g.name));
import requests r = requests.get("https://tatooine.hellagoodnext.com/v1/line", headers={"Authorization": "Bearer hgn_live_YOUR_KEY"}) line = r.json() print(line["waiting"], [g["name"] for g in line["line"]])
Your key
The owner makes the key in the app: Settings › Account, bottom of the page, Create key. It is shown once. Send it on every request:
Authorization: Bearer hgn_live_q6Yslk…
Keys start with hgn_live_ and are 49 characters long. We keep a hash, never the key: if you lose it, revoke it in the app and make another. Revoking is instant. A key stops working the moment the shop's plan ends.
Limits and errors
60 requests a minute per key, 120 per address. Every answer carries x-ratelimit-limit and x-ratelimit-remaining; a 429 carries retry-after in seconds. The line changes every few seconds at most — polling it every 10 to 15 seconds is plenty.
Every error is JSON with a code you can switch on and a message you can show:
{ "error": { "code": "invalid_key", "message": "This key is not valid. It may have been revoked." } }invalid_keyNo key, a malformed key, or a revoked one.shop_inactiveThe shop's plan is not active.no_scopeThe key cannot do this (every key can read today).not_foundNo such route.method_not_allowed/v1/ takes GET only; /mcp takes POST only.rate_limitedOver the minute's budget. Wait retry-after seconds.server_errorOur side. Try again.REST
All times are ISO 8601 in UTC. Minutes are whole numbers. Ids are the app's own and stay stable for the life of a service, person or visit.
/v1/shopThe shop as it is right now: open or not, today's hours, and what guests pick when they join.
| Field | Meaning |
|---|---|
| codestring | The shop's five-letter code, the one on its sign. |
| openboolean | Taking guests right now. |
| hoursobject | Per weekday (mon…sun), a list of ["09:00","18:00"] stretches. Empty = closed. |
| todayarray | Today's stretches, in the shop's zone. |
| next_openstring|null | When it opens next today, if closed now. |
| tzstring | IANA zone, e.g. America/Los_Angeles. |
| guests_pickstring | wait, services, people or services_and_people. |
| keyobject | The key you sent: its prefix and scopes. |
{
"code": "QF7KD",
"name": "QuickFix Device Repair",
"open": true,
"tz": "America/Los_Angeles",
"hours": { "mon": [["09:00", "18:00"]], … "sun": [] },
"today": [["09:00", "18:00"]],
"next_open": null,
"types": ["repair"],
"guests_pick": "services_and_people",
"key": { "prefix": "hgn_live_q6Yslk", "scopes": ["read"] }
}/v1/lineEveryone waiting, called and being served, oldest first. Names and times only: a guest's pass link never leaves the app.
| Field | Meaning |
|---|---|
| waiting · called · servingint | Counts. |
| line[].statusstring | waiting → notified (called up) → serving. |
| line[].joined_attime | When they joined. called_at and serving_since follow. |
| line[].waited_minint | Minutes since they joined. |
| line[].service · asked_for · served_bystring|null | What they came for, who they asked for, who took them. |
| line[].place · next · wait_minwaiting only | Their place, whether they are next, and the wait as a range {min, max}. |
| line[].walk_upboolean | Added at the counter rather than by scanning. |
| line[].on_wayboolean | A called guest tapped "I'm on my way". |
| line[].saidstring · null | A called guest's latest tap from their pass: here, on_way or late. Null when they have not said anything. |
| line[].holdboolean | The shop held their spot: called once, running late, back in line at the same place. |
{
"open": true, "waiting": 5, "called": 1, "serving": 2,
"line": [
{ "id": "a99b0bd5-…", "name": "Jacob R.", "status": "serving",
"joined_at": "2026-10-16T17:02:22Z", "called_at": "2026-10-16T17:14:22Z",
"serving_since": "2026-10-16T17:16:22Z", "waited_min": 34,
"service": "Broken Screen", "asked_for": "Johnny", "served_by": "Johnny",
"walk_up": false, "on_way": false, "said": null, "hold": false },
{ "id": "bbda4058-…", "name": "Nathan", "status": "notified",
"called_at": "2026-10-16T17:34:22Z", "served_by": "Brin", "on_way": true, … },
{ "id": "1c6e0f2a-…", "name": "Emma K.", "status": "waiting",
"service": "Android Repair", "asked_for": "Andre",
"place": 1, "next": true, "wait_min": { "min": 10, "max": 15 }, … }
]
}/v1/waitsWhat a new guest would be told right now: overall, for each service, and for each person. The same estimate the pass shows, learned from the shop's real service times. Always a range, never a single number.
| Field | Meaning |
|---|---|
| wait_min{min,max} | For "next available", any service. |
| services[]id, name, minutes, wait_min | minutes is the service's typical length. |
| people[]id, name, status, wait_min | wait_min is null for someone who is not here. |
{
"open": true,
"wait_min": { "min": 10, "max": 15 },
"services": [
{ "id": "a61bfc32-…", "name": "iPhone repair", "minutes": 20, "wait_min": { "min": 10, "max": 15 } },
{ "id": "59b9453a-…", "name": "Broken Screen", "minutes": 20, "wait_min": { "min": 15, "max": 20 } }
],
"people": [
{ "id": "fb374011-…", "name": "Johnny", "status": "here", "wait_min": { "min": 20, "max": 25 } },
{ "id": "3b967bcd-…", "name": "Hector", "status": "off", "wait_min": null }
]
}/v1/teamThe people who serve guests, whether they are here, since when, and what each one does.
| Field | Meaning |
|---|---|
| hereint | How many are in right now. |
| people[].statusstring | here, break or off. |
| people[].sincetime|null | When that status began. |
| people[].servicesstring[] | Service names this person does. |
{
"here": 5,
"people": [
{ "id": "fb374011-…", "name": "Johnny", "status": "here", "since": "2026-10-16T15:36:22Z",
"services": ["Android Repair", "Battery Problem", "Broken Screen", "iPad Repair", "iPhone repair"] },
{ "id": "3b967bcd-…", "name": "Hector", "status": "off", "since": null,
"services": ["Android Repair", "iPhone repair"] }
]
}/v1/stats?range=weekThe shop's numbers for today (default), the last 7 days (week) or the last 30 (month), in the shop's own time zone.
| Field | Meaning |
|---|---|
| served · per_dayint | Guests served in the range, and per day. |
| walked{left, no_show, rate_pct} | Left before being called, or called and never came. Rate = walk-aways over everyone finished. |
| wait_min · service_minint|null | Average minutes from joining to being called, and from being called to done. |
| by_hour[]hour, joined, walked | When guests arrive, 0–23. |
| people[]name, served, service_min, stars | Per person. stars is their average rating in the range. |
| services[]name, served, service_min, wait_min | Per service. |
{
"range": "week", "days": 7,
"served": 138, "per_day": 20,
"walked": { "left": 0, "no_show": 5, "rate_pct": 3 },
"wait_min": 2, "service_min": 22,
"by_hour": [ { "hour": 9, "joined": 8, "walked": 0 }, { "hour": 10, "joined": 12, "walked": 2 }, … ],
"people": [ { "name": "Johnny", "served": 32, "service_min": 22, "stars": 4.8 }, … ],
"services": [ { "name": "iPhone repair", "served": 49, "service_min": 21, "wait_min": 2 }, … ]
}/v1/reviews?limit=50The newest ratings first, with the comment, the service and the person rated, plus the count and average over all time. limit is 1 to 200, default 50.
| Field | Meaning |
|---|---|
| count · averageint, float|null | All time. |
| reviews[].stars1–5 | |
| reviews[].commentstring|null | What the guest typed, as typed. |
| reviews[].guest · service · personstring|null | Who, for what, with whom. |
{
"count": 98, "average": 4.6,
"reviews": [
{ "stars": 5, "comment": "Grabbed lunch and came back when they were ready for me.",
"at": "2026-10-16T01:04:18Z", "guest": "Sophia P.", "service": "iPhone repair", "person": "Kevin" },
{ "stars": 5, "comment": "Knew exactly when it was my turn.",
"at": "2026-10-16T01:02:20Z", "guest": "Avery", "service": "Android Repair", "person": "Johnny" }
]
}/mcpMCP: your shop inside an AI tool
The same six reads as tools an AI can call, over the Model Context Protocol (Streamable HTTP, stateless). Point any MCP client at https://tatooine.hellagoodnext.com/mcp with your key in the same Authorization header, then ask it things like "who's waiting right now?", "how was last week?" or "any reviews mention parking?"
| Tool | Returns |
|---|---|
| get_shop | Open or not, hours, what guests pick. |
| get_line | Who is waiting, called and being served, with places and waits. |
| get_waits | The wait a new guest would see, per service and person. |
| get_team | Who is here and what they do. |
| get_stats range: today | week | month | Served, walk-aways, waits, per person and service. |
| get_reviews limit: 1–200 | The newest ratings and comments. |
Each tool answers with text and structuredContent (the same JSON as the REST read). Same limits as REST. One JSON-RPC message per POST; the server keeps no session.
Connect it
claude mcp add --transport http next https://tatooine.hellagoodnext.com/mcp \
--header "Authorization: Bearer hgn_live_YOUR_KEY"
// claude_desktop_config.json — through the mcp-remote bridge { "mcpServers": { "next": { "command": "npx", "args": ["-y", "mcp-remote", "https://tatooine.hellagoodnext.com/mcp", "--header", "Authorization: Bearer hgn_live_YOUR_KEY"] } } }
// .cursor/mcp.json { "mcpServers": { "next": { "url": "https://tatooine.hellagoodnext.com/mcp", "headers": { "Authorization": "Bearer hgn_live_YOUR_KEY" } } } }
curl https://tatooine.hellagoodnext.com/mcp \ -H "Authorization: Bearer hgn_live_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_waits","arguments":{}}}'
- Make a key in the app (Settings › Account › Create key).
- Add the server to your tool with the snippet above. Any client that can send a header works; ChatGPT's connectors and other OAuth-only clients cannot yet.
- Ask. The tool list is small on purpose: an AI reads your shop, it does not run it.
Public text
Changes
| 2026-10-16 | First release: six reads under /v1/, MCP at /mcp, self-serve keys in the app. |
Questions, or something you need that is not here: hello@hellagoodnext.com.