247 Developer API

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.

Base URL 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

  1. Sign in at 247ch.at/dashboard.php.
  2. Open the site you want to control and find the API key card.
  3. Click Generate. The key is created once and stored — it is shown persistently, not "see-once". Use Reveal + Copy any time.
  4. 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);
A missing or invalid key returns 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.

Public & rate-limited. No auth is required to call this endpoint, but it is capped per IP. The email is validated and de-duplicated — one account per address.

Body parameters

FieldTypeRequiredDescription
emailstringyesAccount email. Must be valid and not already in use.
passwordstringnoAt least 8 characters. If omitted, a strong password is generated and returned once in the response.
namestringnoDisplay name for the account.

Response fields

FieldTypeDescription
accountIdnumberThe new developer account's id
emailstringThe registered email
developerKeystringAccount-level key (32-hex). Store it securely — use it as a Bearer token for POST /sites.
passwordstringOnly 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."
}
The 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.

FieldTypeDescription
domainstringThe site's domain (e.g. acme.com)
displayNamestringPublic title (defaults to the domain)
primaryColorstringBrand/widget color (hex)
greetingTextstringWidget greeting
positionstringWidget position (e.g. bottom-right)
starterPromptstringCustom 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

FieldTypeDescription
siteIdnumberThe new site's id
apiKeystringThe site's per-site key — use it as X-API-Key for GET /site and the other per-site endpoints.
configobjectThe 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

FieldTypeDescription
idstringSite identifier
domainstringThe site's domain
displayNamestringPublic display name / title
starterPromptstringSystem prompt that seeds the assistant
customPromptstringOperator's custom prompt additions
primaryColorstringBrand/widget color (hex)
greetingTextstringGreeting shown in the widget
positionstringWidget screen position
welcomeMessagestringFirst message from the assistant
businessHoursJSONBusiness-hours configuration
allowedOriginsstringOrigins allowed to load the widget
contactPhonestringContact phone
contactEmailstringContact email
websiteUrlstringSite's website URL
tosUrlstringTerms of Service URL
privacyUrlstringPrivacy Policy URL
contactUrlstringContact page URL
enabledbooleanWhether 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

FieldTypeNotes
displayName / titlestringPublic title
starterPromptstringSystem prompt (sets customPrompt=true)
resetStarterPromptbooleanReset the starter prompt to default
primaryColor / brandColorstringDrives the launcher bubble color. Hex (#rgb or #rrggbb)
bgColor, fontColor, buttonColor, glowColorstringThe rest of the 5-color palette (hex). glowColor nullable
greetingTextstringWidget greeting
positionstringbottom-right, bottom-left, top-right, top-left
welcomeMessagestringFirst assistant message
businessHoursJSONBusiness-hours object (must parse as JSON)
allowedOriginsstringComma-separated allowed widget origins
contactPhone, contactEmail, websiteUrl, tosUrl, privacyUrl, contactUrlstringThe 6 contact/legal fields
sttProvider, ttsVoice, ttsSpeedstringPer-site voice (STT/TTS) settings
cornerStyle, dropShadow, glowSize, glowStrengthstringWidget style tunables
aiModestringAI auto-reply mode: on, off, or delay
aiReplyDelaySecnumberDelay in seconds (1-600) used when aiMode=delay
enabledbooleanEnable/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 requested siteId (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

FieldTypeRequiredDescription
messagestringyesThe user message to send
conversationIdstringnoContinue an existing conversation; omit to start a new one

Response fields

FieldTypeDescription
replystringThe assistant's reply text
conversationIdstringID 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

MemberDescription
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.

  1. Edit your starterPrompt with PATCH /site (or in the dashboard).
  2. Send a message with POST /chat and read the reply.
  3. Pass the returned conversationId back to continue a multi-turn test.
  4. 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

CodeMeaningWhen
200OKRequest succeeded
400Bad RequestInvalid body, unknown field, or failed validation
401UnauthorizedMissing or invalid X-API-Key
403ForbiddenAttempt to set an ownership/plan/payment field
429Too Many RequestsPer-key rate limit exceeded (mainly POST /chat)
500Server ErrorSomething 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.

Tip: handle 401 by re-checking your key in the dashboard, and 429 with exponential backoff.