247ch.at Developer API
A public REST API that lets you do anything an operator can do to a 247ch.at site — read and update its config, fetch the install snippet, and run a test chat — wrapped in Python, JavaScript, and C++ SDKs.
https://247ch.at/api/v1
Quickstart
Every request authenticates with a per-site API key sent in the X-API-Key header. Generate one from your dashboard (see Authentication), then call any endpoint:
curl https://247ch.at/api/v1/site \
-H "X-API-Key: YOUR_API_KEY"
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
site = client.get_site()
print(site["displayName"])
import { Chat247 } from "./chat247.js";
const client = new Chat247("YOUR_API_KEY");
const site = await client.getSite();
console.log(site.displayName);
#include "chat247.hpp"
int main() {
Chat247 client("YOUR_API_KEY");
auto site = client.getSite();
std::cout << site["displayName"] << std::endl;
}
What you can do
- Read a site's full public config —
GET /site - Update any operator-editable field —
PATCH /site - Fetch the
<script>install snippet —GET /embed - Run a test chat against the site's starter prompt —
POST /chat
Two key types exist. A per-site key (X-API-Key) is scoped to exactly one site and powers the endpoints above — payments and billing are out of scope and cannot be reached through it. An account-level developer key lets you create a developer account and provision new sites entirely over the API (see POST /developers and POST /sites); it can only create and own sites under your account.
Authentication
The Developer API uses a single per-site API key. One key controls exactly one site.
Generating a key in the dashboard
- Sign in at 247ch.at/dashboard.php.
- Open the site you want to control and find the API key card.
- Click Generate. The key is created once and stored — it is shown persistently, not "see-once". Use Reveal + Copy any time.
- Click Rotate to replace the key if it leaks. The old key stops working immediately.
The key is an MD5-format token (32 hex characters), unique per site.
Sending the key
Pass your key in the X-API-Key header on every request. There is no bearer token or OAuth flow for the public API.
curl https://247ch.at/api/v1/site \
-H "X-API-Key: 9f86d081884c7d659a2feaa0c55ad015"
import requests
headers = {"X-API-Key": "9f86d081884c7d659a2feaa0c55ad015"}
r = requests.get("https://247ch.at/api/v1/site", headers=headers)
print(r.json())
const res = await fetch("https://247ch.at/api/v1/site", {
headers: { "X-API-Key": "9f86d081884c7d659a2feaa0c55ad015" }
});
console.log(await res.json());
// libcurl: add the header to your request
struct curl_slist *headers = NULL;
headers = curl_slist_append(headers,
"X-API-Key: 9f86d081884c7d659a2feaa0c55ad015");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
401 Unauthorized. CORS is open because this is a public API — but treat the key as a secret; it grants full control over the site's config.
POST /api/v1/developers
Create a developer account programmatically. Returns an account-level developer key you use to provision sites with POST /sites.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | Account email. Must be valid and not already in use. |
password | string | no | At least 8 characters. If omitted, a strong password is generated and returned once in the response. |
name | string | no | Display name for the account. |
Response fields
| Field | Type | Description |
|---|---|---|
accountId | number | The new developer account's id |
email | string | The registered email |
developerKey | string | Account-level key (32-hex). Store it securely — use it as a Bearer token for POST /sites. |
password | string | Only present when a password was generated for you. Lets you sign in at the dashboard later. |
Example
curl -X POST https://247ch.at/api/v1/developers \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com", "name": "Ada"}'
import requests
r = requests.post("https://247ch.at/api/v1/developers",
json={"email": "dev@example.com", "name": "Ada"})
acct = r.json()
dev_key = acct["developerKey"]
print(acct["accountId"], dev_key)
const res = await fetch("https://247ch.at/api/v1/developers", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "dev@example.com", name: "Ada" })
});
const acct = await res.json();
const devKey = acct.developerKey;
console.log(acct.accountId, devKey);
Response
{
"ok": true,
"accountId": 42,
"email": "dev@example.com",
"developerKey": "9f86d081884c7d659a2feaa0c55ad015",
"password": "Kp3xq8r2Tn7w",
"message": "Account created. Store developerKey securely; use it as a Bearer token for POST /api/v1/sites."
}
developerKey (and any generated password) are shown once here. Store them securely. A duplicate email returns 409 Conflict; too many signups from one IP return 429 Too Many Requests.
POST /api/v1/sites
Create a new site owned by your developer account. Returns the new siteId and its per-site apiKey (the X-API-Key for the per-site endpoints).
Authentication
Authenticate with your account-level developer key from POST /developers, sent either as a Bearer token or the X-Developer-Key header:
Authorization: Bearer YOUR_DEVELOPER_KEY
# or
X-Developer-Key: YOUR_DEVELOPER_KEY
Body parameters
Same shape as the dashboard's create-site form. All fields are optional except you'll usually want a domain.
| Field | Type | Description |
|---|---|---|
domain | string | The site's domain (e.g. acme.com) |
displayName | string | Public title (defaults to the domain) |
primaryColor | string | Brand/widget color (hex) |
greetingText | string | Widget greeting |
position | string | Widget position (e.g. bottom-right) |
starterPrompt | string | Custom system prompt. Omit to auto-generate a default. |
Free accounts are limited to one site (402 upgrade_required beyond that); Pro accounts are unlimited.
Response fields
| Field | Type | Description |
|---|---|---|
siteId | number | The new site's id |
apiKey | string | The site's per-site key — use it as X-API-Key for GET /site and the other per-site endpoints. |
config | object | The new site's public config (same shape as GET /site) |
Example
curl -X POST https://247ch.at/api/v1/sites \
-H "Authorization: Bearer YOUR_DEVELOPER_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "acme.com", "displayName": "Acme Support"}'
import requests
headers = {"Authorization": "Bearer YOUR_DEVELOPER_KEY"}
r = requests.post("https://247ch.at/api/v1/sites",
headers=headers,
json={"domain": "acme.com", "displayName": "Acme Support"})
site = r.json()
print(site["siteId"], site["apiKey"]) # apiKey is the per-site X-API-Key
const res = await fetch("https://247ch.at/api/v1/sites", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_DEVELOPER_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({ domain: "acme.com", displayName: "Acme Support" })
});
const site = await res.json();
console.log(site.siteId, site.apiKey);
Response
{
"ok": true,
"siteId": 128,
"apiKey": "5d41402abc4b2a76b9719d911017c592",
"domain": "acme.com",
"displayName": "Acme Support",
"config": { "id": 128, "domain": "acme.com", "displayName": "Acme Support", "enabled": true },
"message": "Site created. Use apiKey as the X-API-Key header for the per-site /api/v1 endpoints."
}
With the returned apiKey you can immediately call GET /site, PATCH /site, GET /embed, and POST /chat for the new site.
GET /api/v1/site
Returns the site's full public configuration.
Request
No body or query parameters. The site is resolved from your X-API-Key.
Response fields
| Field | Type | Description |
|---|---|---|
id | string | Site identifier |
domain | string | The site's domain |
displayName | string | Public display name / title |
starterPrompt | string | System prompt that seeds the assistant |
customPrompt | string | Operator's custom prompt additions |
primaryColor | string | Brand/widget color (hex) |
greetingText | string | Greeting shown in the widget |
position | string | Widget screen position |
welcomeMessage | string | First message from the assistant |
businessHours | JSON | Business-hours configuration |
allowedOrigins | string | Origins allowed to load the widget |
contactPhone | string | Contact phone |
contactEmail | string | Contact email |
websiteUrl | string | Site's website URL |
tosUrl | string | Terms of Service URL |
privacyUrl | string | Privacy Policy URL |
contactUrl | string | Contact page URL |
enabled | boolean | Whether the widget is live |
Ownership and billing fields (userId, plan, payment data) are never returned.
Example
curl https://247ch.at/api/v1/site \
-H "X-API-Key: YOUR_API_KEY"
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
site = client.get_site()
print(site["domain"], site["enabled"])
const client = new Chat247("YOUR_API_KEY");
const site = await client.getSite();
console.log(site.domain, site.enabled);
Chat247 client("YOUR_API_KEY");
auto site = client.getSite();
std::cout << site["domain"] << std::endl;
Response
{
"id": "site_8a1f",
"domain": "acme.com",
"displayName": "Acme Support",
"starterPrompt": "You are Acme's friendly support assistant.",
"customPrompt": "",
"primaryColor": "#4338ca",
"greetingText": "Hi! How can we help?",
"position": "bottom-right",
"welcomeMessage": "Welcome to Acme — ask me anything.",
"businessHours": { "mon": "9-17", "tue": "9-17" },
"allowedOrigins": "https://acme.com",
"contactPhone": "555-0100",
"contactEmail": "support@acme.com",
"websiteUrl": "https://acme.com",
"tosUrl": "https://acme.com/terms",
"privacyUrl": "https://acme.com/privacy",
"contactUrl": "https://acme.com/contact",
"enabled": true
}
PATCH /api/v1/site
Updates any of the operator-editable fields. Send only the fields you want to change.
Editable fields
| Field | Type | Notes |
|---|---|---|
displayName / title | string | Public title |
starterPrompt | string | System prompt (sets customPrompt=true) |
resetStarterPrompt | boolean | Reset the starter prompt to default |
primaryColor / brandColor | string | Drives the launcher bubble color. Hex (#rgb or #rrggbb) |
bgColor, fontColor, buttonColor, glowColor | string | The rest of the 5-color palette (hex). glowColor nullable |
greetingText | string | Widget greeting |
position | string | bottom-right, bottom-left, top-right, top-left |
welcomeMessage | string | First assistant message |
businessHours | JSON | Business-hours object (must parse as JSON) |
allowedOrigins | string | Comma-separated allowed widget origins |
contactPhone, contactEmail, websiteUrl, tosUrl, privacyUrl, contactUrl | string | The 6 contact/legal fields |
sttProvider, ttsVoice, ttsSpeed | string | Per-site voice (STT/TTS) settings |
cornerStyle, dropShadow, glowSize, glowStrength | string | Widget style tunables |
aiMode | string | AI auto-reply mode: on, off, or delay |
aiReplyDelaySec | number | Delay in seconds (1-600) used when aiMode=delay |
enabled | boolean | Enable/disable the widget |
Ownership and plan fields are rejected. Validation matches the dashboard's site editor exactly. The updated site is returned (same shape as GET /site).
Example
curl -X PATCH https://247ch.at/api/v1/site \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"greetingText": "Welcome back!", "brandColor": "#4338ca"}'
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
updated = client.update_site({
"greetingText": "Welcome back!",
"brandColor": "#4338ca",
})
print(updated["greetingText"])
const client = new Chat247("YOUR_API_KEY");
const updated = await client.updateSite({
greetingText: "Welcome back!",
brandColor: "#4338ca",
});
console.log(updated.greetingText);
Chat247 client("YOUR_API_KEY");
auto updated = client.updateSite(R"({
"greetingText": "Welcome back!",
"brandColor": "#4338ca"
})");
std::cout << updated["greetingText"] << std::endl;
Response
{
"id": "site_8a1f",
"domain": "acme.com",
"displayName": "Acme Support",
"greetingText": "Welcome back!",
"primaryColor": "#4338ca",
"enabled": true
}
GET /api/site · PATCH /api/sites
Flat, single-site settings endpoints that read/write the full admin-editable field set and accept either auth scheme: the per-site X-API-Key (same key as the /api/v1 endpoints) or a dashboard owner Bearer token. The site is identified by siteId (numeric) — in the query string for GET, in the JSON body for PATCH. They share the exact same whitelist + validation as PATCH /api/v1/site and the dashboard editor, so behavior never drifts.
Auth
X-API-Key: <per-site key>— must match the requestedsiteId(else 403).Authorization: Bearer <dashboard token>— must own the site (or be super-admin).
When authed as the owner, the response also includes the site's apiKey, userId, and createdAt. Editable fields are identical to PATCH /api/v1/site above.
Examples
# Read every editable field for one site (X-API-Key)
curl "https://247ch.at/api/site?siteId=26" \
-H "X-API-Key: YOUR_API_KEY"
# Update the launcher color + a starter prompt (siteId in the body)
curl -X PATCH https://247ch.at/api/sites \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"siteId": 26, "primaryColor": "#abe300", "starterPrompt": "You are ..."}'
# Via the chat247_hook.py wrapper (resolves the key from config.ini [chat247])
from chat247_hook import get_site, update_site
get_site("moneyfornoth.in") # full config
update_site("moneyfornoth.in", primaryColor="#abe300", # set lime launcher
starterPrompt="You are the MoneyForNoth concierge.")
# CLI equivalent:
# python chat247_hook.py get moneyfornoth.in
# python chat247_hook.py set moneyfornoth.in primaryColor=#abe300
GET /api/v1/embed
Returns the <script> install snippet for this site — the same code the dashboard hands you.
Request
No parameters. The snippet is built for the site resolved from your key.
Example
curl https://247ch.at/api/v1/embed \
-H "X-API-Key: YOUR_API_KEY"
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
print(client.get_embed()["snippet"])
const client = new Chat247("YOUR_API_KEY");
const { snippet } = await client.getEmbed();
console.log(snippet);
Chat247 client("YOUR_API_KEY");
auto embed = client.getEmbed();
std::cout << embed["snippet"] << std::endl;
Response
{
"snippet": "<script src=\"https://247ch.at/widget.js\" data-site=\"site_8a1f\" async></script>"
}
Paste the snippet just before the closing </body> tag on every page where you want the chat widget to appear.
POST /api/v1/chat
Runs a test chat against the site's starter prompt and returns the assistant's reply — the same tool-call-free path the live widget uses.
Body parameters
| Field | Type | Required | Description |
|---|---|---|---|
message | string | yes | The user message to send |
conversationId | string | no | Continue an existing conversation; omit to start a new one |
Response fields
| Field | Type | Description |
|---|---|---|
reply | string | The assistant's reply text |
conversationId | string | ID to pass back on the next turn |
Example
curl -X POST https://247ch.at/api/v1/chat \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"message": "What are your hours?"}'
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
res = client.chat("What are your hours?")
print(res["reply"])
# continue the conversation
res2 = client.chat("And on weekends?", res["conversationId"])
print(res2["reply"])
const client = new Chat247("YOUR_API_KEY");
const res = await client.chat("What are your hours?");
console.log(res.reply);
const res2 = await client.chat("And on weekends?", res.conversationId);
console.log(res2.reply);
Chat247 client("YOUR_API_KEY");
auto res = client.chat("What are your hours?");
std::cout << res["reply"] << std::endl;
Response
{
"reply": "We're open Monday to Friday, 9am to 5pm.",
"conversationId": "conv_4d2e9b"
}
POST /chat is rate-limited per key. Exceeding the limit returns 429 Too Many Requests — see Errors & Rate Limits.
SDK Reference
Official thin wrappers for Python, JavaScript, and C++. Each exposes the same four methods over the REST API.
Constructor & methods
| Member | Description |
|---|---|
Chat247(apiKey, baseUrl='https://247ch.at/api/v1') | Create a client bound to one site's key |
getSite() | Fetch the site config (GET /site) |
updateSite(fields) | Update fields (PATCH /site) |
getEmbed() | Fetch the install snippet (GET /embed) |
chat(message, conversationId=None) | Run a test chat (POST /chat) |
Python — sdk/python/chat247.py
Requires requests (pip install requests).
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
site = client.get_site()
client.update_site({"greetingText": "Hi there!"})
snippet = client.get_embed()["snippet"]
reply = client.chat("Hello")["reply"]
print(reply)
JavaScript — sdk/js/chat247.js
ESM module, works in the browser and Node (uses fetch).
import { Chat247 } from "./chat247.js";
const client = new Chat247("YOUR_API_KEY");
const site = await client.getSite();
await client.updateSite({ greetingText: "Hi there!" });
const { snippet } = await client.getEmbed();
const { reply } = await client.chat("Hello");
console.log(reply);
C++ — sdk/cpp/chat247.hpp
Header-only-ish, depends on libcurl. Returns parsed JSON objects.
#include "chat247.hpp"
int main() {
Chat247 client("YOUR_API_KEY");
auto site = client.getSite();
client.updateSite(R"({"greetingText": "Hi there!"})");
auto embed = client.getEmbed();
auto reply = client.chat("Hello");
std::cout << reply["reply"] << std::endl;
}
Test-Chat Guide
Use POST /chat to verify your site's assistant behaves as expected before — or after — going live.
How it works
The test chat runs your site's starterPrompt plus the global voice through the same tool-call-free path the embedded widget uses. It does not place real customer conversations into your inbox — it is meant for testing prompt changes.
- Edit your
starterPromptwith PATCH /site (or in the dashboard). - Send a message with POST /chat and read the
reply. - Pass the returned
conversationIdback to continue a multi-turn test. - Iterate on the prompt until the replies match your brand voice.
End-to-end example
from chat247 import Chat247
client = Chat247("YOUR_API_KEY")
# tune the prompt
client.update_site({"starterPrompt": "You are a concise, friendly concierge."})
# multi-turn test
turn1 = client.chat("Do you offer refunds?")
print("A:", turn1["reply"])
turn2 = client.chat("How long does it take?", turn1["conversationId"])
print("A:", turn2["reply"])
Errors & Rate Limits
The API returns standard HTTP status codes and JSON error bodies.
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Request succeeded |
400 | Bad Request | Invalid body, unknown field, or failed validation |
401 | Unauthorized | Missing or invalid X-API-Key |
403 | Forbidden | Attempt to set an ownership/plan/payment field |
429 | Too Many Requests | Per-key rate limit exceeded (mainly POST /chat) |
500 | Server Error | Something went wrong on our end |
Error body
{
"error": "invalid_api_key",
"message": "The X-API-Key header is missing or invalid."
}
Rate limits
The config endpoints (GET /site, PATCH /site, GET /embed) are lightly limited. The POST /chat endpoint is rate-limited per key because each call hits the LLM. When you hit the limit you receive a 429 — back off and retry after a short delay.
401 by re-checking your key in the dashboard, and 429 with exponential backoff.