message.comDevelopers

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

GET/api/v1/sitesAuth: Bearer

Cursor-paginated list of sites in the workspace. Default limit 50, maximum 200. No filters; the list is small in practice.

200 OK
{
  "sites": [ /* up to 50 site objects */ ],
  "nextCursor": null
}

Code samples

cURL
curl 'https://app.message.com/api/v1/sites' \
  -H 'Authorization: Bearer YOUR_WORKSPACE_JWT'
JavaScript
const res = await fetch('https://app.message.com/api/v1/sites', {
  headers: { Authorization: 'Bearer ' + token }
});
const { sites } = await res.json();
Python
import requests
r = requests.get(
    "https://app.message.com/api/v1/sites",
    headers={"Authorization": f"Bearer {token}"},
)
sites = r.json()["sites"]
Ruby
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
<?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

GET/api/v1/sites/:idAuth: Bearer

Returns the full site record including the public embedId rendered in your install snippet.

200 OK
{
  "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

POST/api/v1/sitesAuth: Bearer

Creates a new site. Returns the full site record including the generated embedId. Admin-only.

FieldTypeDescription
namerequiredstringHuman-readable name. Shown in the dashboard site picker.
domainrequiredstringPrimary domain. Used to derive the default allowed origins.
primaryColoroptionalstringHex colour. Overrides workspace primary for this site only.
launcherPositionoptionalenumright or left. Defaults to right.
welcomeMessageoptionalstringOverride workspace welcome message for this site.
allowedOriginsoptionalstring[]Strict-match origin allowlist. Defaults to the domain + www variant.
Body
{
  "name": "Acme storefront",
  "domain": "acme.com",
  "primaryColor": "#FF4D1F"
}

Update a site

PATCH/api/v1/sites/:idAuth: Bearer

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

CodeWhen
400 invalid_bodyPayload validation failed.
403 forbiddenAgent role is not admin.
404 not_foundSite does not exist or belongs to another workspace.
409 conflictA site already exists for that domain in this workspace.
409 stale_versionexpectedVersion mismatch.

Common pitfalls

  • Mixing site IDs and workspace IDs in install snippets. The snippet uses data-site-id with the site’s public embedId, 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.