Sites Live
A site is a domain or subdomain where the widget is installed. A workspace can run many sites, each with its own widget appearance, welcome message, and allowed origin list. Most teams have one site and never touch this API; agencies and multi-brand operators rely on it.
List sites
Cursor-paginated list of sites in the workspace. Default limit 50, maximum 200. No filters; the list is small in practice.
{
"sites": [ /* up to 50 site objects */ ],
"nextCursor": null
}Code samples
curl 'https://app.message.com/api/v1/sites' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT'const res = await fetch('https://app.message.com/api/v1/sites', {
headers: { Authorization: 'Bearer ' + token }
});
const { sites } = await res.json();import requests
r = requests.get(
"https://app.message.com/api/v1/sites",
headers={"Authorization": f"Bearer {token}"},
)
sites = r.json()["sites"]require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/sites")
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{token}"
http.request(req)
end
sites = JSON.parse(res.body)["sites"]<?php
$ctx = stream_context_create([
"http" => ["method" => "GET", "header" => "Authorization: Bearer $token"]
]);
$sites = json_decode(file_get_contents("https://app.message.com/api/v1/sites", false, $ctx), true)["sites"];Get one site
Returns the full site record including the public embedId rendered in your install snippet.
{
"id": "uuid",
"version": 3,
"name": "Acme storefront",
"domain": "acme.com",
"embedId": "site_01HRX0...",
"primaryColor": "#FF4D1F",
"launcherPosition": "right",
"welcomeMessage": "Hi! How can we help?",
"businessHoursOverride": null,
"allowedOrigins": ["https://acme.com", "https://www.acme.com"],
"createdAt": "2026-01-04T12:00:00Z"
}Create a site
Creates a new site. Returns the full site record including the generated embedId. Admin-only.
| Field | Type | Description |
|---|---|---|
| namerequired | string | Human-readable name. Shown in the dashboard site picker. |
| domainrequired | string | Primary domain. Used to derive the default allowed origins. |
| primaryColoroptional | string | Hex colour. Overrides workspace primary for this site only. |
| launcherPositionoptional | enum | right or left. Defaults to right. |
| welcomeMessageoptional | string | Override workspace welcome message for this site. |
| allowedOriginsoptional | string[] | Strict-match origin allowlist. Defaults to the domain + www variant. |
{
"name": "Acme storefront",
"domain": "acme.com",
"primaryColor": "#FF4D1F"
}Update a site
Patch one or more site fields. Requires expectedVersion.
There is no delete endpoint
Sites cannot be removed via the API. This follows from the platform's one-website-per-workspace model: a workspace is grounded on one registrable domain, so removing its site would leave the workspace without the identity the AI and routing are built around. Use PATCH to edit an existing site's configuration instead.
Errors
| Code | When |
|---|---|
400 invalid_body | Payload validation failed. |
403 forbidden | Agent role is not admin. |
404 not_found | Site does not exist or belongs to another workspace. |
409 conflict | A site already exists for that domain in this workspace. |
409 stale_version | expectedVersion mismatch. |
Common pitfalls
- Mixing site IDs and workspace IDs in install snippets. The snippet uses
data-site-idwith the site’s publicembedId, not its database ID or the workspace ID. The widget uses that value to select its site configuration. - Overly tight
allowedOrigins. A staging subdomain you forgot to add will load the widget but refuse to send messages. Add wildcards (https://*.staging.acme.com) sparingly. - Expecting per-site billing. Billing is workspace-level. Sites are a routing/appearance concept.