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.
Delete a site
Removes the site and invalidates its embedId. Existing conversations are preserved (they still belong to the workspace); only the widget config goes away. The next page load on a deleted embedId renders nothing.
Embed IDs are not reusable. Deleting and recreating a site means updating every install snippet that referenced the old ID. Prefer PATCH for everyday edits.
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-workspacefor the workspace, and the bundle derives the site from thewindow.locationmatch against allowed origins. - 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.