Selene is the action layer for agent-initiated bookings. One endpoint returns real availability, resolves the staff member and the service, and comes back with a confirmation the merchant can see. You integrate once, and never with a booking system.
Sign in with your email and create a key. Your first key is always a sandbox key: real merchants, synthetic times, and bookings that come back as a demo_ confirmation without contacting anyone. No approval, no call.
Then follow the quickstart below. Your first demo_ confirmation is about three calls away.
A production key books real appointments that real people will show up for, so production keys are issued after a conversation, a few at a time. Leave your email to get on the production waitlist and we’ll reach out to set up that conversation. You don’t need to wait for it to build: everything above works with a sandbox key today.
We use this address only to follow up about production access. No newsletter, and no sharing with anyone. See our Privacy Policy.
Salons, spas, nail studios, barbershops and restaurants behind a single call. Selene absorbs the fragmentation so your agent never has to know how any given merchant takes a booking.
Live slots from the merchant’s own system, with the staff member and service resolved for you — not a scraped guess at opening hours.
book_appointment returns a confirmation the merchant can see. The appointment exists.
Platforms Selene can read but not commit return merchant_read_only, naming the merchant’s own site, so you hand the user off instead of failing silently.
Streamable-HTTP MCP at https://api.selene.thalosai.com/mcp/ (keep the trailing slash). Authenticate with your key as a bearer token; no OAuth needed. Export it first: export SELENE_API_KEY=selene_pat_…
{
"mcpServers": {
"selene": {
"url": "https://api.selene.thalosai.com/mcp/",
"headers": { "Authorization": "Bearer selene_pat_YOUR_SANDBOX_KEY" }
}
}
}In Claude or ChatGPT, add a custom connector with the same URL instead (Claude: Settings → Connectors → Add custom connector. ChatGPT: Settings → Connectors → Advanced → Developer mode). They sign in with your email; no key needed.
demo_ confirmation# pip install mcp
import asyncio, json, os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
URL = os.environ.get("SELENE_MCP_URL", "https://api.selene.thalosai.com/mcp/")
HEADERS = {"Authorization": f"Bearer {os.environ['SELENE_API_KEY']}"}
async def call(session, tool, args):
result = await session.call_tool(tool, args)
# Rich results carry structuredContent; errors are a JSON text block.
return result.structuredContent or json.loads(result.content[0].text)
async def main():
async with streamablehttp_client(URL, headers=HEADERS) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
found = await call(session, "search_merchants", {
"vertical": "barber", "location": "San Francisco, CA", "limit": 5,
})
for merchant in found["merchants"]:
times = await call(session, "find_available_times", {
"merchant_id": merchant["merchant_id"], "day": "tomorrow",
})
if not times.get("error_code") and times.get("slots"):
break
else:
raise SystemExit("no open times tomorrow at the first 5 results")
slot = times["slots"][0]
print(merchant["name"], "-", slot["local_time"])
booked = await call(session, "book_appointment", {
"booking_token": slot["booking_token"],
"customer_email": "you@example.com", # saved after the first booking
"customer_first_name": "Test",
})
if booked.get("error_code"):
raise SystemExit(f"{booked['error_code']}: {booked['error']['message']}")
print(booked["status"], booked["backend_booking_id"]) # sandbox: demo_...
asyncio.run(main())// npm install @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const url = new URL(process.env.SELENE_MCP_URL ?? "https://api.selene.thalosai.com/mcp/");
const transport = new StreamableHTTPClientTransport(url, {
requestInit: { headers: { Authorization: `Bearer ${process.env.SELENE_API_KEY}` } },
});
const client = new Client({ name: "selene-quickstart", version: "1.0.0" });
await client.connect(transport);
async function call(name: string, args: Record<string, unknown>): Promise<any> {
const res: any = await client.callTool({ name, arguments: args });
// Rich results carry structuredContent; errors are a JSON text block.
return res.structuredContent ?? JSON.parse(res.content[0].text);
}
const found = await call("search_merchants", {
vertical: "barber", location: "San Francisco, CA", limit: 5,
});
let merchant: any, slot: any;
for (const m of found.merchants) {
const times = await call("find_available_times", { merchant_id: m.merchant_id, day: "tomorrow" });
if (!times.error_code && times.slots?.length) { merchant = m; slot = times.slots[0]; break; }
}
if (!slot) throw new Error("no open times tomorrow at the first 5 results");
console.log(merchant.name, "-", slot.local_time);
const booked = await call("book_appointment", {
booking_token: slot.booking_token,
customer_email: "you@example.com", // saved after the first booking
customer_first_name: "Test",
});
if (booked.error_code) throw new Error(`${booked.error_code}: ${booked.error.message}`);
console.log(booked.status, booked.backend_booking_id); // sandbox: demo_...
await client.close();These run against the real merchant catalog: search returns real San Francisco barbers, and on a sandbox key their times are synthetic and the booking never reaches the shop. The loop tries the first few results because some merchants need a staff member or service picked first (list_staff, list_services).
Your first key is a sandbox key. Same tools, same response shapes, same error codes, same timezone handling — with two deliberate differences:
demo_ confirmation, sets test_mode, and never contacts the merchant. Nothing is reserved and nobody is expecting anyone.Merchants, services and staff are real, so what you build against is the real data model. The token decides, not the arguments — a production key books an appointment a real person will be expecting, which is why production isn’t self-serve.