Build your own website that plugs into Super Dodo Terminal and HostHelper. You control every pixel — the backend handles the rest.
| Goal | Build a custom HTML site that talks to SDT's API for live island data, accounts, ordering, and more. |
| Prerequisites | SDT installed, HostHelper set up, public access via Cloudflare — follow Site Setup on Set Up Your Dodo Suite (Stable Domain Setup, Dodo Site Setup, or Dodo Site Editing). |
Quick Start
- Build your HTML/CSS/JS site in any editor you like
- Open HostHelper → Website Settings → Step 4
- Check "Use a custom website instead"
- Pick your site folder and click Apply
- HostHelper copies your files to the folder SDT serves from
- Start your tunnel in Step 2 and your site is live
custom-theme-base folder containing a fully functional bare-bones site that wires up every API on this page. Copy it, restyle it, and you're done. Find it at SuperDodoTerminal\custom-theme-base\ next to the SDT executable.
http://localhost:7070 before you set up a tunnel. SDT's dashboard server runs on port 7070 by default.
Folder Structure
Your root folder should look something like this. Not all pages are required — only build the ones your site needs.
your-site/
index.html ← home page (recommended)
legal.html ← terms / privacy (required)
token-signup.html ← account creation
token-login.html ← login
token-forgot.html ← token recovery
token-profile.html ← player profile / settings
token-recovery.html ← account recovery
hosts.html ← team host list (team mode)
rules.html ← rules page
faq.html ← FAQ page
alerts.html ← alerts / announcements
host-about.html ← about the host
subscriptions.html ← subscription tiers
pay.html ← payment page
confirmation.html ← payment confirmation
requests.html ← player requests
report.html ← player reports
order-status.html ← item/villager order tracker
queue-status.html ← visitor queue position tracker
island-paradise.html ← per-island page (one per island)
assets/
theme/ ← your images, fonts, CSS
SDT serves these files from %AppData%\HostHelper\dodo-builder-preview-{mode}\ where {mode} is solo or team. HostHelper copies your folder there when you click Apply.
Special Page Behaviors
/(root) — If there is at least one online island, SDT redirectsGET /to/island-{slug}.htmlfor the first online island. If no islands are connected, it servesindex.html(or the fallback "still setting up" page if no files exist).island-{slug}.html— If a specific island page (e.g.island-paradise.html) doesn't exist, SDT will use anyisland-*.htmlit finds as a template. It injects three JavaScript constants into the template:const TARGET_ISLAND,const FALLBACK_NAME, andconst ISLAND_SLUG. Your template page can use these to know which island to display.queue-status.html— When served via/queue/{slug}, SDT injects a small script that adds Done-button subtext from the queue config. Your page should have a "Done" button for players to end their queue session.order-status.html— Served when a visitor hits/order/{id}in a browser (withAccept: text/html). Your page should poll the order API for progress using the order ID from the URL.
The island-{slug} Convention
The "slug" is created by SDT by lowercasing the island name and removing spaces. So an island named "My Paradise" becomes slug myparadise, and its page would be island-myparadise.html. You can find the correct slug in the islandKey field on each island object from /api/public/status.
API Base URL
All API calls go to the same address your site is served from. In your JavaScript:
const API_BASE = window.location.origin;
This works because SDT serves both your pages and the API from the same server (port 7070). When visitors use your Cloudflare tunnel URL, window.location.origin points to the right place automatically.
Authentication
SDT has two layers of access: website access (form-based, via cookies) and Dodo Token accounts (email + token, via headers).
Website Access (Cookie-Based)
When a visitor first arrives, they may need to fill out a simple access form (player name, island name, contact info). This is handled by:
POST /api/access-request
const res = await fetch(`${API_BASE}/api/access-request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerName: 'Luna',
islandName: 'Moonisle',
contactType: 'Discord',
contactValue: 'luna#1234'
})
});
On success, SDT sets a sdt_site_access cookie (HttpOnly, ~8 hour expiry). This cookie is sent automatically with every future request. No extra JavaScript needed — the browser handles it.
POST /api/logout — Clears the session. Returns { success: true }.
GET /api/access-status — Check the current visitor's access state. Returns:
| Field | Type | What it means |
|---|---|---|
IsApproved |
bool | Whether the visitor has access |
AccessTier |
string | "Free", "Paid", or "None" |
HasPaidAccess |
bool | Whether visitor has a paid subscription |
IsBanned |
bool | Whether visitor is banned |
RequiresForm |
bool | Always true (form-based access) |
Dodo Token (Header-Based)
Players sign up with an email and get a token back. Authenticated calls use two headers:
const headers = {
'Content-Type': 'application/json',
'X-Dodo-Email': localStorage.getItem('dodoEmail'),
'X-Dodo-Token': localStorage.getItem('dodoToken')
};
Store the email and token in localStorage after login or signup so they persist across page loads.
?dodoEmail=...&dodoToken=... as an alternative to headers. Useful for simple links or forms.
How Access Tiers Work
SDT determines a visitor's access tier in this priority order:
sdt_site_accesscookie — if valid, uses the tier stored in the session- IP approval list — if the visitor's IP was approved via the access form
X-Dodo-Emailheader — if the email matches a centrally-subscribed paid account, tier is"Paid"- None of the above — tier is
"None"(limited access)
When Dodo Codes Are Hidden
The dodoCode field on each island will be empty ("") or have a special value when:
- Visitor is banned →
"" - Visitor has no website access (tier is
"None") →"" - Island is password-protected and visitor hasn't unlocked it →
"" - Island is in queue mode →
""(code shown only via queue status endpoint when it's your turn) - Island is monetized and visitor has no paid access →
"PAID"(literal string)
Password-Protected Islands
POST /api/private-unlock
const res = await fetch(`${API_BASE}/api/private-unlock`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
islandKey: 'paradise',
password: 'secret123'
})
});
On success, sets a per-island unlock cookie (sdt_unlock_{islandKey}). The requiresWebsitePassword and websitePasswordUnlocked fields on each island in /api/public/status tell you whether to show a password prompt.
GET /api/website-access-config — Returns the list of access rules per island (with passwords stripped), including IsUnlocked and IsWebsiteApproved for the current visitor.
API Endpoints
islandKey, dodoCode). But queue entry objects, villager arrays, and the Team Host Feed use PascalCase (Position, DodoCode, IslandName). If your site supports team mode, plan for both.
Quick Reference
Every endpoint at a glance. Auth column: None = open, Cookie = website access cookie, Token = X-Dodo-Email + X-Dodo-Token headers, Token/QS = headers or query string.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/public/status | None | All island data, visitor counts, queue state — details |
| POST | /api/access-request | None | Submit access form (sets cookie) — details |
| POST | /api/logout | Cookie | Clear website session |
| GET | /api/access-status | Cookie | Check visitor's access state |
| POST | /api/private-unlock | None | Unlock a password-protected island — details |
| GET | /api/website-access-config | None | Access rules per island |
| GET | /api/public/subscriptions | None | Subscription plans — details |
| POST | /api/public/access/request | None | Create account — details |
| POST | /api/public/access/login | None | Log in |
| POST | /api/public/access/reset-token | None | Recover lost token |
| POST | /api/public/access/profile/update | Token | Update player/island pairs |
| POST | /api/public/access/contact/update | Token | Update contact info |
| POST | /api/public/access/icon | Token | Set player icon |
| POST | /api/public/access/delete-account | Token | Delete account permanently |
| POST | /api/public/access/central-opt-in | Token | Link to central database |
| POST | /api/public/access/agree-rules | Token/QS | Record rules agreement — details |
| GET | /api/public/access/rules-status | Token/QS | Check rules agreement |
| GET | /api/public/access/messages | Token | Fetch player inbox — details |
| POST | /api/public/access/messages/mark-read | Token | Mark all messages as read |
| POST | /api/public/requests/submit | Token | Submit a request — details |
| POST | /api/public/reports/submit | Token | Submit a report |
| GET | /api/queue/island/{slug} | None | Queue entries for an island — details |
| GET | /api/queue-config/{slug} | None | Queue configuration |
| POST | /api/queue/join | Token | Join the visitor queue |
| GET | /api/queue/{queueId}?s={secret} | ID+Secret | Check your queue status |
| POST | /api/queue/{queueId}/done | ID+Secret | Signal you're done visiting |
| GET | /api/queue-status | Cookie | Villager injection queue — details |
| GET | /api/pool-villagers | Cookie | Available villager pool — details |
| GET | /api/villagers | None | Villager list for forms |
| POST | /api/request-villager | Cookie | Submit a villager request |
| GET | /api/requests | Cookie | All villager request records |
| GET | /items | None | Full item catalog — details |
| GET | /api/order-config/{slug} | None | Order form settings |
| POST | /order | Token | Submit an item order |
| GET | /order/{orderId} | None | Check order status |
| POST | /order/{orderId}/complete | None | Mark order as picked up |
| GET | /queue | None | Full order queue |
| GET | /status | None | Full server state (dashboard) |
| GET | /preset/{name}/preview | None | Preview a preset's items |
| GET | /api/catalog/{key}/data | None | Island catalog JSON — details |
| GET | /api/catalog/{key}/overview | None | Catalog overview (PNG) |
| GET | /api/catalog/images/{file} | None | Individual item image |
| GET | /api/map/{key} | None | Island map image — details |
| GET | /api/boarding-pass/{key} | None | Boarding pass image |
| GET | /api/island-info-icon/{key} | None | Island info icon |
| GET | /api/zone-map/{key} | None | Zone map image |
| GET | /api/team-host-feed | Feed token | Full host + island + inbox feed — details |
| POST | /api/team-host-feed/request-villager | Feed token | Relay a villager request |
| POST | /api/public/payments/paypal/create-subscription | Token | Start PayPal subscription — details |
| POST | /api/public/payments/paypal/create-order | Token | Start one-time PayPal payment |
Island Data (the main one)
GET /api/public/status
This is the big one. Returns everything about the host's islands, visitor counts, queue state, and more. No login required.
const res = await fetch(`${API_BASE}/api/public/status`);
const data = await res.json();
Top-Level Fields:
| Field | Type | What it is |
|---|---|---|
generatedAt |
string | ISO timestamp of this response |
version |
string | Always "1.0" |
totalVisits |
number | Total visit count |
totalPlayers |
number | Total registered players |
counterMode |
string | "rolling" or "daily" |
onlineIslands |
number | Count of islands included in this response |
accessTier |
string | This visitor's current access tier ("None", "Free", or "Paid") |
islands |
array | Array of island objects (see below) |
Each island in the array:
| Field | Type | What it is |
|---|---|---|
id |
number | 1-based index in this response (not a stable ID) |
name |
string | Island display name |
hostName |
string | Host's display name |
status |
string | "Online", "Offline", "Not connected", etc. May include "Paid access required" suffix |
dodoCode |
string | The Dodo code, "" if hidden, or "PAID" if paid-gated (see access rules above) |
occupants |
number | Current occupant count (visitors + host, minimum 1) |
visitorCount |
number | Raw visitor count |
flightStatus |
string | "Idle", "Incoming", "Outgoing", etc. |
visitors |
array | Always [] (empty — use visitorCount instead) |
maxOccupants |
number | Always 8 |
sourceApp |
string | Always "SDT" |
islandKey |
string | URL-safe slug for this island (lowercase, no spaces) |
mapImageUrl |
string | Absolute URL to the island map image, or "" |
hideIslandImageMap |
bool | Whether the host wants the map hidden |
boardingPassImageUrl |
string | Absolute URL to the boarding pass image, or "" |
islandInfoIconUrl |
string | Absolute URL to the island info icon, or "" |
islandInfoText |
string | Custom info text for the island |
islandFacebookUrl |
string | Facebook URL for this island |
isMonetized |
bool | Whether this island requires paid access |
monetizationSubscriptionEnabled |
bool | Whether subscriptions are enabled |
monetizationPriceId |
string | PayPal price/plan ID |
monetizationLabel |
string | Label shown for monetization |
monetizationDescription |
string | Description of the paid tier |
monetizationAvailability |
string | Availability text |
monetizationTierName |
string | Tier name |
monetizationTierDescription |
string | Tier description |
monetizationTierAvailability |
string | Tier availability |
monetizationPlansJson |
string | JSON string of plans |
checkoutBaseUrl |
string | Base URL for payment checkout |
ownerHostId |
string | ID of the host who owns this island |
villagerRequestsEnabled |
bool | Whether villager requests are on |
isOrderBotIsland |
bool | Whether this is an order bot island |
isOrderIsland |
bool | Whether this island accepts item orders |
isVillagerMode |
bool | Whether this island is in villager mode |
isQueueMode |
bool | Whether queue mode is active |
isTiPlusOrder |
bool | Whether TI+ ordering is enabled |
instantText |
bool | Instant text setting |
orderFormAllowStayTimeSelection |
bool | Whether players choose stay time on the order form |
orderFormStayTimeOptionsMinutes |
string | Comma-separated stay times (e.g. "3,5,10,15") |
orderFormMultiZoneEnabled |
bool | Whether multi-zone ordering is enabled |
orderApiPort |
number | Port for the local order API |
orderApiBaseUrl |
string | Public base URL for order calls (use this for /items, /order, etc.) |
queuePreSignupMessage |
string | Message shown before joining queue |
queueWaitingMessage |
string | Message shown while waiting in queue |
queueYourTurnMessage |
string | Message shown when it's your turn |
queueStayTimeEnabled |
bool | Whether stay time selection is enabled for queue |
queueStayTimeOptionsMinutes |
string | Comma-separated stay time options (e.g. "15,30,45,60") |
queueDefaultStayMinutes |
number | Default stay time |
requiresWebsitePassword |
bool | Whether a password is needed to see this island |
websitePasswordUnlocked |
bool | Whether the current visitor has unlocked it |
boardingPassDodoXPercent |
number | Dodo code X position on boarding pass (%) |
boardingPassDodoYPercent |
number | Dodo code Y position on boarding pass (%) |
boardingPassDodoWidthPercent |
number | Dodo code box width on boarding pass (%) |
boardingPassDodoHeightPercent |
number | Dodo code box height on boarding pass (%) |
hostFacebookProfileUrl |
string | Always "" in this response |
hostAboutPageUrl |
string | Always "" in this response |
hostAboutMeHtml |
string | Always "" in this response |
currentVillagers |
array | Array of { Name, State } objects (PascalCase) |
upcomingRotation |
array | Array of villager name strings |
hasCatalog |
bool | Whether this island has a catalog available |
GET /api/public/islands returns the same data (it's an alias).
Subscriptions
GET /api/public/subscriptions
Returns available subscription plans and which islands are monetized. No login required.
Top-level fields:
| Field | Type | What it is |
|---|---|---|
hostName |
string | Host display name |
introText |
string | Intro blurb for the subscriptions page |
plans |
array | Array of plan objects |
islands |
array | Array of island objects with tier info |
Each plan:
| Field | Type |
|---|---|
tierName |
string |
length |
string |
costDisplay |
string |
priceId |
string |
description |
string |
availability |
string |
Each island:
| Field | Type |
|---|---|
name |
string |
tier |
string |
description |
string |
availability |
string |
mapUrl |
string |
mapDataUrl |
string |
isOpen |
bool |
Account System
Sign Up
POST /api/public/access/request
Creates a new player account.
const res = await fetch(`${API_BASE}/api/public/access/request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'player@example.com',
dateOfBirth: '2000-01-15',
playerIslandPairs: [
{ playerName: 'Luna', islandName: 'Moonisle' }
],
contactType: 'Discord',
contactValue: 'luna#1234',
centralOptIn: true
})
});
const data = await res.json();
// data.success === true
// data.token → store in localStorage
// data.centralLinked → whether central DB account was created
| Rule | Detail |
|---|---|
email |
Required |
dateOfBirth |
Required — under 18 is rejected with a friendly message |
playerIslandPairs |
Array of { playerName, islandName } objects |
contactType / contactValue |
At least one contact method required |
centralOptIn |
Optional boolean — links to the central Dodo Suite directory |
Error codes: 429 rate limit, 400 validation, 403 under 18, 409 email already exists, 500 server error. All return { success: false, error: "message" }.
On success: { success: true, message: "...", token: "abc123", centralLinked: true/false }. Store email and token in localStorage.
Log In
POST /api/public/access/login
const res = await fetch(`${API_BASE}/api/public/access/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'player@example.com',
token: 'abc123def456'
})
});
Success response:
{
"success": true,
"playerIcon": "icon-name",
"playerIconUrl": "assets/player-icons/icon-name.png",
"playerIslandPairs": [
{ "playerName": "Luna", "islandName": "Moonisle" }
],
"contactRevoked": false,
"centralLinked": true,
"contacts": [
{ "contactType": "Discord", "contactValue": "luna#1234" }
],
"preferredContactType": "Discord"
}
Error codes: 400 missing fields, 401 wrong credentials, 403 banned account ("This account is no longer allowed to sign in.").
Recover Token
POST /api/public/access/reset-token
{
"email": "player@example.com",
"contactType": "Discord",
"contactValue": "luna#1234",
"dateOfBirth": "2000-01-15"
}
On success: { success: true, message: "...", token: "newToken123" }. Most failures return a generic error message to prevent account fishing.
Update Profile
POST /api/public/access/profile/update — update player/island pairs. Requires auth headers.
Body: { playerIslandPairs: [ { playerName, islandName }, ... ] }
Update Contact Info
POST /api/public/access/contact/update — update contact methods. Requires auth headers.
Two body formats are accepted. To update all contacts at once:
{
"contacts": [
{ "contactType": "Discord", "contactValue": "luna#1234" },
{ "contactType": "Twitter", "contactValue": "@luna" }
],
"preferredContactType": "Discord"
}
Or to update just the preferred contact's value:
{
"contactValue": "luna#5678"
}
Success: { success: true, message: "Contact info updated. The host has been notified." }
Errors: 401 not logged in, 400 invalid contact info, 500 server error.
Set Player Icon
POST /api/public/access/icon — set the player's display icon. Body includes email, token, playerIcon. Returns 403 if banned.
Delete Account
POST /api/public/access/delete-account — permanently deletes the account. Requires auth headers. Returns { success: true, message: "Your account has been deleted." }.
Link to Central Database
POST /api/public/access/central-opt-in — links the account to the central Dodo Suite directory. Requires auth headers. Various error codes possible (503 central server down, 409 already linked, 500 failure).
Rules
POST /api/public/access/agree-rules — records that the player agreed to the rules.
GET /api/public/access/rules-status — checks if they've agreed. Returns rulesAgreed (true/false) and rulesAgreedUtc.
Both accept auth via headers or query params (?dodoEmail=...&dodoToken=...). Returns 403 if banned.
Messages (Inbox)
GET /api/public/access/messages — fetches the player's inbox. Requires auth headers.
Returns { success: true, messages: [...] } with an array of message objects.
POST /api/public/access/messages/mark-read — marks all messages as read.
Requests & Reports
POST /api/public/requests/submit — Requires auth headers.
{
"subject": "Can I visit?",
"requestType": "General",
"playerName": "Luna",
"islandName": "Moonisle",
"details": "I'd love to come catalog some items."
}
POST /api/public/reports/submit — Requires auth headers.
{
"reportType": "Website",
"subject": "Bug on island page",
"playerName": "Luna",
"islandName": "Moonisle",
"details": "The queue button doesn't appear.",
"category": "Bug",
"subCategory": "UI"
}
Visitor Queue System
The visitor queue lets players line up to visit an island. It uses a queue ID + secret model — players get both when they join and use them to check status and signal when they're done.
Get Queue Entries for an Island
GET /api/queue/island/{slug} — No login required.
const res = await fetch(`${API_BASE}/api/queue/island/paradise`);
const data = await res.json();
Response:
{
"queueLength": 3,
"entries": [
{
"Position": 1,
"PlayerName": "Luna...",
"StayMinutes": 15,
"EstimatedEtaMinutes": 0,
"Status": "turn"
},
{
"Position": 2,
"PlayerName": "Alex...",
"StayMinutes": 30,
"EstimatedEtaMinutes": 12,
"Status": "next"
}
]
}
queueLength and entries use camelCase, but each entry's fields (Position, PlayerName, etc.) use PascalCase.
| Entry field | Type | What it means |
|---|---|---|
Position |
number | 1 = active turn, 2+ = waiting |
PlayerName |
string | Masked player name |
StayMinutes |
number | How long they plan to stay |
EstimatedEtaMinutes |
number | Minutes until their turn (0 if active) |
Status |
string | "turn", "next", or "waiting" |
If queue mode is off or the island doesn't exist, returns queueLength: 0 and an empty entries array.
Get Queue Configuration
GET /api/queue-config/{slug} — No login required.
const res = await fetch(`${API_BASE}/api/queue-config/paradise`);
const data = await res.json();
Response (200):
{
"preSignupMessage": "Welcome! Join the queue below.",
"waitingMessage": "Hang tight, you're in line!",
"yourTurnMessage": "It's your turn! Here's your Dodo code.",
"stayTimeEnabled": true,
"stayTimeOptionsMinutes": "15,30,45,60",
"defaultStayMinutes": 15,
"doneButtonSubtext": "Use this button when you're completely done..."
}
Returns 404 with { error: "Queue config not found" } if the island doesn't exist or queue mode is off.
Join Queue
POST /api/queue/join — Requires Dodo Token auth headers.
const res = await fetch(`${API_BASE}/api/queue/join`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Dodo-Email': localStorage.getItem('dodoEmail'),
'X-Dodo-Token': localStorage.getItem('dodoToken')
},
body: JSON.stringify({
islandSlug: 'paradise',
playerName: 'Luna',
islandName: 'Moonisle',
stayMinutes: 15
})
});
| Body field | Required | Notes |
|---|---|---|
islandSlug |
Yes | Island slug (from islandKey in status) |
playerName |
Yes | Falls back to pickupPlayerName if empty |
islandName |
Yes | Visitor's island name. Falls back to visitorIslandName or pickupIslandName |
stayMinutes |
No | 0 if omitted |
Success (200):
{
"queueId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
"secret": "xYz123aBcDeFg",
"position": 2
}
Save queueId and secret — you need both for all future queue calls. The queueId is a 32-character hex string (GUID without dashes). The secret is a random string.
Errors: 401 not logged in, 400 missing fields or queue not enabled.
Check Queue Status
GET /api/queue/{queueId}?s={secret} — No login required (possession of ID + secret is the access model).
const res = await fetch(`${API_BASE}/api/queue/${queueId}?s=${encodeURIComponent(secret)}`);
const data = await res.json();
Response (200):
{
"Status": "waiting",
"QueuePosition": 2,
"EstimatedWaitMinutes": 12,
"DodoCode": "",
"StaySecondsRemaining": null,
"IslandName": "paradise",
"WaitingMessage": "Hang tight!",
"YourTurnMessage": "Here's your Dodo code!"
}
| Field | Type | What it means |
|---|---|---|
Status |
string | "waiting", "turn", or "done" |
QueuePosition |
number | 1 when it's your turn, 2+ when waiting, 0 when done |
EstimatedWaitMinutes |
number | 0 when active or done |
DodoCode |
string | The Dodo code when it's your turn and the gate is open; otherwise "" |
StaySecondsRemaining |
number or null | Seconds left in your stay (only when Status is "turn" and timer running) |
IslandName |
string | Island slug |
WaitingMessage |
string or null | Custom waiting message |
YourTurnMessage |
string or null | Custom "your turn" message |
Returns 404 with { error: "not_found", status: "error" } if the queue ID or secret is wrong.
Poll this every 3–5 seconds on your queue-status.html page. When Status changes to "turn", show the Dodo code and a "Done" button.
Signal Done
POST /api/queue/{queueId}/done — Tells the queue you're finished.
Secret can be passed as query ?s={secret} or in the body as { "secret": "..." }.
Success: { "ok": true }
Errors: 400 if it's not your turn or the secret is wrong. Only the active visitor (the one whose Status is "turn") can call done. Players who are still waiting cannot leave the queue via this endpoint.
Villager Injection Queue
This is a separate system from the visitor queue above. This tracks villager injections (moving villagers onto islands).
GET /api/queue-status — Requires website access (logged in or form submitted).
Optional query: ?islandIndex={n} for per-island status.
Response:
{
"QueueLength": 2,
"Pending": ["Raymond", "Marshal"],
"PendingLines": ["Raymond in queue for Alex", "Marshal in queue for Luna"],
"SecondsUntilNextInjection": 45,
"LastInjectionTime": "2026-04-06T12:00:00.0000000Z",
"LastInjectedName": "Raymond",
"IslandName": "Paradise"
}
If the visitor doesn't have website access, returns an empty default object (zeros and empty arrays), still with status 200.
Villager Requests
GET /api/pool-villagers — Returns the available villager pool. Requires website access (returns [] if not approved).
[
{ "InternalName": "cat23", "DisplayName": "Raymond" },
{ "InternalName": "squir09", "DisplayName": "Marshal" }
]
Note: PascalCase field names. This is the raw pool of villagers the host has available for injection.
GET /api/villagers — Returns the list of available villagers for request forms. No login required.
{
"villagers": ["Raymond", "Marshal", "Judy"],
"options": [
{ "internalName": "cat23", "displayName": "Raymond" },
{ "internalName": "squir09", "displayName": "Marshal" }
]
}
POST /api/request-villager — Submit a villager request. Requires website access.
{
"VillagerInternalName": "cat23",
"VillagerName": "Raymond",
"IslandIndex": 0,
"PickupPlayerName": "Luna",
"PickupIslandName": "Moonisle"
}
Success (200):
{
"success": true,
"message": "Raymond has been queued!",
"position": 2,
"etaSeconds": 345
}
If injection is unavailable, position and etaSeconds may be omitted. 403 if access blocked, 429 for rate limit or cooldown, 400 for validation errors.
GET /api/requests — Returns all villager request records as a JSON array. Requires website access; returns [] if not approved.
Item Ordering
SDT has a built-in order system. The dashboard server proxies order requests to the internal order API — your theme just calls the same URLs.
Get Item Catalog
GET /items — Returns the full item catalog. No login required.
{
"items": [
{
"hex": "1A2B",
"name": "Ironwood Dresser",
"category": "Furniture",
"series": "Ironwood",
"colours": ["Brown"],
"interact": "Table",
"diy": true,
"itemTag": "HousewareTable",
"hha": "3",
"hha2": "3",
"imageUrl": "/api/catalog/images/ironwood-dresser.png",
"stackSize": 1,
"variations": [
{
"name": "Birch",
"id": "0",
"hex": "1A2B0000",
"filename": "ironwood-dresser-birch.png",
"imageUrl": "/api/catalog/images/ironwood-dresser-birch.png",
"colours": ["Beige"]
}
]
}
],
"categories": ["Furniture", "Clothing", "Wallpaper"],
"series": ["Ironwood", "Rattan"]
}
If the catalog file doesn't exist on the host's setup, returns {}.
Get Order Form Config
GET /api/order-config/{slug} — Returns order form settings for an island. No login required.
{
"allowStayTimeSelection": true,
"stayTimeOptionsMinutes": "3,5,10,15",
"multiZoneEnabled": false
}
Returns 404 plain text "Order config not found" if the island doesn't exist or isn't an order island.
Submit an Order
POST /order — Submits an item order.
const res = await fetch(`${API_BASE}/order`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Dodo-Email': localStorage.getItem('dodoEmail'),
'X-Dodo-Token': localStorage.getItem('dodoToken')
},
body: JSON.stringify({
villagerName: 'Luna',
items: ['09C9', '1A2B'],
islandName: 'Paradise',
stayMinutes: 5
})
});
Request body fields:
| Field | Type | Required | Notes |
|---|---|---|---|
villagerName |
string | Yes | Player's name (used for queue display and tracking) |
items |
array of strings | Yes (unless preset used) |
Item hex IDs from the catalog (e.g. "09C9" for Gold Nugget). Max 40 items. |
preset |
string | No | Name of a preset instead of individual items. If set, items is ignored. |
islandName |
string | No | Which island the order is for |
stayMinutes |
number | No | How long the player wants to stay. Must be one of the allowed options from order config. |
requestedStayMinutes |
number | No | Alternate field name for stay time (web form compatibility) |
fillerItemHex |
string | No | If provided and items < 40, pads remaining slots with this item |
contactInfo |
string | No | Player contact info for notifications |
contactType |
string | No | Contact method type ("email", "discord", etc.) |
Success (200):
{
"success": true,
"orderId": "ORD-143022-4521",
"queuePosition": 3,
"estimatedWaitMinutes": 15,
"message": "Order queued! You are #3 in queue."
}
Save orderId — you need it for the status page.
Error responses:
| Code | When |
|---|---|
400 |
Invalid JSON, no items, invalid stay time, item not in catalog |
403 |
Too many no-shows (player blocked) |
429 |
Order cooldown active (repeat order too soon) |
503 |
Bot not connected or gate anchors not configured |
SDT also enforces a per-player cooldown at the dashboard level — if OrderCooldownEnabled is on for the island, repeat orders within the cooldown window return 429 with { message: "You can only submit one order every X minutes. Please wait and try again." }.
Check Order Status
GET /order/{orderId} — Returns order status as JSON (when called from JavaScript), or serves your order-status.html page (when opened in a browser).
const res = await fetch(`${API_BASE}/order/${orderId}`);
const data = await res.json();
Response (200):
{
"orderId": "ORD-143022-4521",
"status": "ReadyForPickup",
"queuePosition": null,
"estimatedWaitMinutes": null,
"dodoCode": "ABC12",
"villagerName": "Luna",
"itemCount": 5,
"zoneId": 1,
"zoneX": 10,
"zoneY": 20,
"zoneWidth": 5,
"zoneHeight": 5,
"zoneAssignedAt": "2026-04-06T12:00:00Z",
"zoneCooldownEndsAt": "2026-04-06T12:10:00Z",
"islandName": "Paradise",
"createdAt": "2026-04-06T11:55:00Z",
"visitorArrived": false,
"visitorLeft": false,
"wasNoShow": false,
"completedAt": null,
"failureReason": null,
"queueMessages": {
"waitingSolo": "Hang tight, your order is being prepared!",
"yourTurnSolo": "Your items are ready! Here's your Dodo code.",
"waitingTeam": "Hang tight!",
"yourTurnTeam": "Items ready!"
},
"lineItems": [
{
"hexId": "0x09C9",
"displayName": "Gold Nugget",
"sprite": "GoldNugget.png",
"catalogImage": "09C9.png"
}
],
"queueOverview": {
"slots": [
{ "slot": 0, "isEmpty": false, "playerName": "Alex", "itemCount": 3, "isYou": false },
{ "slot": 1, "isEmpty": false, "playerName": "Luna", "itemCount": 5, "isYou": true },
{ "slot": 2, "isEmpty": true, "playerName": "", "itemCount": 0, "isYou": false }
],
"matchedInQueue": true,
"yourWaitPosition": 1,
"totalWaiting": 2,
"totalInQueue": 2,
"yourPosition": 1,
"behindCount": 1
}
}
Key fields:
| Field | What it means |
|---|---|
status |
"Pending", "Processing", "ReadyForPickup", "WaitingForArrival", "VisitorOnIsland", "Completed", "Cancelled", "Failed", "NoShow" |
dodoCode |
The Dodo code when items are ready — show this prominently |
queuePosition |
Position in queue (null when no longer waiting) |
lineItems |
Array of items in the order with display names and image paths |
queueOverview |
Visual queue display — isYou marks the player's slot |
queueMessages |
Custom waiting/your-turn messages from the host |
zoneId / zoneX / zoneY |
Where on the island to pick up items (for multi-zone islands) |
Returns 404 with { error: "Order not found" } if the ID doesn't exist.
Poll this every 3–5 seconds on your order-status.html page. Show the Dodo code when status is "ReadyForPickup" or "WaitingForArrival".
Mark Order Complete ("I'm Done")
POST /order/{orderId}/complete — Player signals they're done picking up items.
Success: { success: true, message: "Thanks for visiting!" }
Errors: 400 missing ID, 404 order not found.
Get Order Queue
GET /queue — Returns the full order queue with wait times and zone info. Useful for a queue display page.
{
"queueLength": 3,
"estimatedTotalWaitMinutes": 15,
"minutesPerOrder": 5,
"orders": [
{
"position": 1,
"villagerName": "Luna",
"status": "Processing",
"itemCount": 5,
"zoneId": 1,
"zoneState": "Queued",
"estimatedWaitMinutes": 5,
"createdAt": "2026-04-06T11:55:00Z"
}
],
"zones": [
{
"zoneId": 1,
"state": "InUse",
"assignedOrderId": "ORD-143022-4521",
"cooldownEndsAt": "2026-04-06T12:10:00Z",
"x": 10, "y": 20, "width": 5, "height": 5
}
]
}
Get Dashboard / Status
GET /status (or /, /dashboard, /all) — Returns full server state: bot connection, queue, stats, preset list, time selection options, and sprite availability. Good for building a host-facing dashboard or debugging.
Preset Ordering
If the host has presets enabled, players can order a preset by name instead of picking individual items:
{
"villagerName": "Luna",
"preset": "starter-pack"
}
GET /preset/{name}/preview — Preview a preset's items before ordering. Returns item hex IDs, sprite names, and counts.
Other Order Paths
These paths are also handled: /form (HTML order form), /stats (order statistics), /test (test order), /help (endpoint list), /sprites/{name}.png (item sprites), /sprites/mapping (sprite name mapping).
Island Catalog
GET /api/catalog/{islandKey}/data — Returns island-specific catalog JSON. No login required.
GET /api/catalog/{islandKey}/overview — Returns island catalog overview as a PNG image.
GET /catalog/{islandKey} — SDT serves a built-in catalog viewer HTML page (not your theme).
GET /api/catalog/images/{filename} — Serves individual item images. Cached for 7 days.
Island Images
GET /api/map/{islandKey} — Island map image. Add ?view=team for team map variant.
GET /api/boarding-pass/{islandKey} — Boarding pass image.
GET /api/island-info-icon/{islandKey} — Island info icon.
GET /api/zone-map/{islandKey} — Zone map image.
All return PNG image data or 404. No login required.
Team Host Feed
GET /api/team-host-feed — Returns host info, all islands, and inbox items for team mode. This is what HostHelper pulls from each team member's SDT instance.
Optional query params:
| Param | What it does |
|---|---|
token |
Access token if the host configured one (returns 401 if wrong) |
inboxSince |
ISO date — only return inbox items newer than this |
Response (200): All field names are PascalCase (unlike /api/public/status which uses camelCase).
{
"Host": {
"HostDisplayName": "Luna's Islands",
"FacebookProfileUrl": "",
"AboutPageUrl": "",
"HostAboutMeRichTextXaml": "",
"HostAboutMeHtml": "<p>Welcome!</p>"
},
"Islands": [
{
"IslandName": "Paradise",
"HostName": "Luna",
"DodoCode": "ABC12",
"Status": "Online",
"IsOnline": true,
"VisitorCount": 3,
"IsQueueMode": false,
"IsOrderIsland": true,
"VillagerRequestsEnabled": false,
"...": "all DashboardIslandInfo fields in PascalCase"
}
],
"InboxItems": [
{
"Id": "abc-123",
"Type": "Request",
"SubmitterEmail": "player@example.com",
"PreferredContactType": "Discord",
"PreferredContactValue": "luna#1234",
"ContactReachOutSummary": "Discord: luna#1234",
"ReceivedUtc": "2026-04-06T12:00:00Z",
"Subject": "Can I visit?",
"RequestType": "General",
"PlayerName": "Luna",
"IslandName": "Moonisle",
"Details": "I'd love to come catalog!",
"IsRead": false
}
],
"GeneratedUtc": "2026-04-06T12:00:00Z"
}
Errors: 404 if team feed is disabled, 401 if token is wrong.
IslandName, DodoCode, etc.) while /api/public/status uses camelCase (islandKey, dodoCode). Plan for both if your custom site supports team mode.
/api/public/status does. This is because the team feed is consumed by HostHelper (the hub), which applies its own access rules when serving visitors.
Team villager relay: POST /api/team-host-feed/request-villager works the same as POST /api/request-villager but is called by HostHelper on behalf of a visitor. Requires the feed token query param.
PayPal Payments
email and token fields in the request body are the player's Dodo Token credentials — the same ones stored in localStorage after signup/login. SDT uses these to link the PayPal subscription to their account automatically. If the player isn't logged in, SDT returns 403.
Before showing a "Subscribe" or "Buy" button, check if the player is logged in (email + token in localStorage). If not, show a "Log in to subscribe" message instead and link to your login page.
Create Subscription
POST /api/public/payments/paypal/create-subscription
const res = await fetch(`${API_BASE}/api/public/payments/paypal/create-subscription`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Dodo-Email': localStorage.getItem('dodoEmail'),
'X-Dodo-Token': localStorage.getItem('dodoToken')
},
body: JSON.stringify({
planId: 'P-ABC123',
successUrl: `${window.location.origin}/confirmation.html`,
cancelUrl: `${window.location.origin}/subscriptions.html`,
email: localStorage.getItem('dodoEmail'),
token: localStorage.getItem('dodoToken')
})
});
const data = await res.json();
if (data.success) {
window.location.href = data.checkoutUrl;
}
| Field | Required | Notes |
|---|---|---|
planId |
Yes | The priceId from the plan object in /api/public/subscriptions |
successUrl |
Yes | Where PayPal sends the player after successful payment |
cancelUrl |
Yes | Where PayPal sends the player if they cancel |
email |
Yes | Player's Dodo Token email |
token |
Yes | Player's Dodo Token |
Success (200):
{
"success": true,
"checkoutUrl": "https://www.paypal.com/webapps/billing/subscriptions?ba_token=...",
"subscriptionId": "I-ABC123XYZ",
"approvedAccessEntryId": "entry-id-here"
}
Redirect the player to checkoutUrl. PayPal handles the rest.
Error responses:
| Code | When |
|---|---|
400 |
Missing planId, successUrl, or cancelUrl |
403 |
Player not logged in or not approved for website access |
503 |
PayPal runtime disabled or credentials not configured by the host |
500 |
PayPal API error |
Create One-Time Payment
POST /api/public/payments/paypal/create-order — same auth requirements as subscriptions.
{
"amount": "5.00",
"currency": "USD",
"successUrl": "https://your-site.com/confirmation.html",
"cancelUrl": "https://your-site.com/subscriptions.html",
"email": "player@example.com",
"token": "abc123def456"
}
| Field | Required | Notes |
|---|---|---|
amount |
Yes | Price as a string (e.g. "5.00") |
currency |
No | Defaults to "USD" |
successUrl |
Yes | Redirect after payment |
cancelUrl |
Yes | Redirect if cancelled |
email |
Yes | Player's Dodo Token email |
token |
Yes | Player's Dodo Token |
Success (200):
{
"success": true,
"checkoutUrl": "https://www.paypal.com/checkoutnow?token=...",
"orderId": "PAYPAL-ORDER-ID",
"approvedAccessEntryId": "entry-id-here"
}
Same error codes as create-subscription above.
Webhook
The PayPal webhook (POST /api/public/payments/paypal/webhook) is called by PayPal automatically — you don't need to handle it in your frontend. It processes subscription activations and payment completions behind the scenes.
Flows
Step-by-step walkthroughs for the most common things your site will do.
Login / Signup
- Player lands on
token-signup.html - Fills in email, DOB, player name(s), island name(s), contact info
POST /api/public/access/request— get token back- Store email + token in
localStorage— redirect toindex.html - Return visits:
token-login.html→POST /api/public/access/login - Login response includes
playerIslandPairs,contacts,playerIcon, etc. - Forgot token:
token-forgot.html→POST /api/public/access/reset-token
Visitor Queue
- Player visits
island-{slug}.html - Page polls
GET /api/public/statusfor island data - If
isQueueModeis true, show a "Join Queue" button - Load queue config from
GET /api/queue-config/{slug}for messages and stay time options - Player clicks join →
POST /api/queue/join(requires auth headers) - Save the
queueIdandsecretfrom the response - Redirect to
queue-status.html(passqueueIdandsecretvia URL hash orlocalStorage) queue-status.htmlpollsGET /api/queue/{queueId}?s={secret}every 3–5 seconds- Show position, estimated wait, and the waiting message
- When
Statusis"turn"→ show theDodoCodeand the "your turn" message - Show
StaySecondsRemainingas a countdown timer - When player is done →
POST /api/queue/{queueId}/done(with secret) - Status becomes
"done"— show a thank-you / goodbye message
Orders (Items)
- Player visits
island-{slug}.html - Check
isOrderIslandorisOrderBotIslandfrom/api/public/status - Load item catalog from
GET /items(hex IDs, names, categories, images) - Load order config from
GET /api/order-config/{slug}for stay time options - Player picks items and stay time, enters their name
POST /orderwithvillagerName,items(hex ID array),stayMinutes- Response includes
orderId,queuePosition,estimatedWaitMinutes - Redirect to
/order/{orderId}(servesorder-status.htmlin a browser) order-status.htmlextracts the order ID from the URL- Poll
GET /order/{orderId}every 3–5 seconds for status JSON - When
statusis"ReadyForPickup"→ showdodoCodeandlineItems - Player picks up items and clicks "I'm Done" →
POST /order/{orderId}/complete
Villager Requests
- Player visits
island-{slug}.html - Check
villagerRequestsEnabledfrom/api/public/status - Load villager list from
GET /api/villagers - Player picks a villager →
POST /api/request-villager - Response includes
positionandetaSeconds - Show queue position and estimated wait time
Subscriptions / Payments
- Player visits
subscriptions.html GET /api/public/subscriptionsfor available plans (no login needed — show plans to everyone)- Player picks a plan → check if logged in (email + token in
localStorage) - Not logged in → redirect to
token-login.html(or show inline login prompt), then come back - Logged in → go to
pay.htmlwith the selectedplanId POST /api/public/payments/paypal/create-subscriptionwithplanId,successUrl,cancelUrl,email,token
(include email + token in both the request body and asX-Dodo-Email/X-Dodo-Tokenheaders)- SDT auto-links the subscription to their Dodo Token account
- Response includes
checkoutUrl→ redirect player to PayPal - PayPal redirects back to
confirmation.htmlafter payment - Webhook fires automatically (nothing for you to do) — SDT activates their paid access
External Resources
These external sources are used by the built-in theme and available for custom sites too:
| Resource | URL Pattern |
|---|---|
| Villager portraits | https://acnhcdn.com/latest/NpcBromide/NpcNml{id}.png |
| Catalog item images | GET /api/catalog/images/{filename} (served by SDT) |
| Island map images | GET /api/map/{islandKey} (served by SDT) |
| Boarding passes | GET /api/boarding-pass/{islandKey} (served by SDT) |
| Sprite images | GET /sprites/{file}.png (served by SDT) |
Page Checklist
| Page | Required? | Key API Calls |
|---|---|---|
index.html |
Recommended | GET /api/public/status (poll for island tiles) |
legal.html |
Required | Static content |
token-signup.html |
If accounts enabled | POST /api/public/access/request |
token-login.html |
If accounts enabled | POST /api/public/access/login |
token-forgot.html |
If accounts enabled | POST /api/public/access/reset-token |
token-profile.html |
If accounts enabled | POST .../profile/update, .../contact/update, .../icon |
token-recovery.html |
Optional | POST /api/public/access/reset-token |
hosts.html |
Team mode | GET /api/public/status (filter by host) |
rules.html |
Optional | POST .../agree-rules, GET .../rules-status |
faq.html |
Optional | Static content |
alerts.html |
Optional | Static content |
host-about.html |
Optional | Static content |
subscriptions.html |
If monetized | GET /api/public/subscriptions |
pay.html |
If monetized | POST .../paypal/create-subscription or create-order |
confirmation.html |
If monetized | Static confirmation message |
requests.html |
Optional | POST /api/public/requests/submit |
report.html |
Optional | POST /api/public/reports/submit |
order-status.html |
If ordering | Served at /order/{id} — poll order API for status |
queue-status.html |
If visitor queues | Poll GET /api/queue/{queueId}?s={secret} — show position, code, done button |
island-{slug}.html |
Per island | GET /api/public/status, queue/order endpoints for that island |
Tips
- Unknown API paths return a redirect to
/, not a 404. Check for non-JSON responses in your error handling. - The
.custom-themefile in your preview folder tells SDT you're running a custom site. Don't delete it. - CORS is wide open — SDT sets
Access-Control-Allow-Origin: *on all responses, so yourfetch()calls work from anywhere. - You don't need all the pages. Start with
index.htmlandlegal.html, add more as you go. - PascalCase vs camelCase — Most top-level response fields use camelCase. Queue entry DTOs and villager objects inside arrays use PascalCase (
Position,Name,State). Watch for this when parsing. - Rate limits — Signup, access requests, password attempts, and villager requests have per-IP rate limits. Handle
429responses gracefully with a retry message. - The
sdt_site_accesscookie lasts about 8 hours. After it expires, visitors need to re-submit the access form or log in again. - Dynamic island pages — If you only want one island page template, name it
island-template.html(or anyisland-*.htmlname). SDT will injectconst TARGET_ISLAND,const FALLBACK_NAME, andconst ISLAND_SLUGat the top of the page so your JavaScript knows which island to display.
Related pages:
- Your Website — overview of website options
- HostHelper — setting up HostHelper
- Site Setup on Set Up Your Dodo Suite — Stable Domain Setup, Dodo Site Setup, and Dodo Site Editing
- Accounts & Central Database — how the account system works