Visitors Live
A visitor is a widget session record: one row per anonymous-or-identified browser session, scoped to a site. There is no list, no plain GET by ID, no create, no update, and no merge for visitors. Four sub-actions exist: block, unblock, commerce enrichment, and conversation history.
Looking for CRM-style create, search, and update (name, email, tags, custom fields, lifetime value)? That's a different, separate resource: Contacts, below. Contacts and visitors are related (a visitor can point at a contact) but have different APIs entirely.
Block a visitor
Sets blockedAt to now. No body. Returns the full visitor row, wrapped in visitor, not a two-field summary.
{
"visitor": {
"id": "uuid",
"siteId": "uuid",
"visitorToken": "v_...",
"email": "[email protected]",
"name": "Alice Johnson",
"phoneE164": null,
"ip": "203.0.113.42",
"country": "US",
"userAgent": "Mozilla/5.0...",
"geo": { "city": "San Francisco", "region": "CA" },
"wcEnrichment": null,
"contactId": "uuid",
"currentPageUrl": "https://acme.com/checkout",
"blockedAt": "2026-05-13T15:30:00Z",
"firstSeenAt": "2024-09-04T12:00:00Z",
"lastSeenAt": "2026-05-13T15:30:00Z",
"visitCount": 12
}
}Unblock a visitor
Clears blockedAt. No body. Same response shape as block: the full visitor row wrapped in visitor.
Commerce enrichment
Cached WooCommerce or Shopify customer + order data for this visitor, source-agnostic to callers. Computed lazily on first request if nothing is cached yet, then persisted for subsequent reads.
{
"enrichment": {
"source": "woocommerce",
"matched": true,
"customer": { "email": "[email protected]", "lifetimeValueCents": 128400 },
"orders": [ { "id": "4567", "total": "49.00", "status": "completed" } ]
}
}Visitor conversations
This visitor's conversations across every channel, newest first, capped at 50. Not the same shape as the Conversations API: field names here are type (not channel) and include call-specific callDirection / callPeerE164.
{
"conversations": [
{
"id": "uuid",
"type": "chat",
"status": "solved",
"preview": "Thanks, that resolved it!",
"callDirection": null,
"callPeerE164": null,
"agentName": "Alex Lee",
"startedAt": "2026-05-10T12:00:00Z",
"endedAt": "2026-05-10T12:14:00Z",
"durationSec": 840,
"rating": null
}
]
}Contacts: the actual CRM resource
Contacts are the customer directory: search by name, email, phone, or company, with a 360-degree profile (cross-channel conversation history, tags, custom fields, lifetime value). This is what a CRM sync integration should target, not visitors.
List / search contacts
Query params: search (matches name, email, phone, or company), limit (default 50, max 200), offset.
Code samples
curl 'https://app.message.com/api/v1/[email protected]' \
-H 'Authorization: Bearer YOUR_WORKSPACE_JWT'const res = await fetch('https://app.message.com/api/v1/[email protected]', {
headers: { Authorization: 'Bearer ' + token }
});
const { contacts } = await res.json();import requests
r = requests.get(
"https://app.message.com/api/v1/contacts",
headers={"Authorization": f"Bearer {token}"},
params={"search": "[email protected]"},
)
contacts = r.json()["contacts"]require "net/http"
require "json"
uri = URI("https://app.message.com/api/v1/[email protected]")
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
contacts = JSON.parse(res.body)["contacts"]<?php
$ctx = stream_context_create([
"http" => ["method" => "GET", "header" => "Authorization: Bearer $token"]
]);
$contacts = json_decode(file_get_contents("https://app.message.com/api/v1/[email protected]", false, $ctx), true)["contacts"];Get one contact
Full profile plus cross-channel conversation history and per-channel stats.
{
"contact": {
"id": "uuid",
"name": "Alice Johnson",
"email": "[email protected]",
"phone": "+14155550100",
"company": "Acme Inc",
"tags": ["vip"],
"customFields": { "plan": "pro", "mrr": 99 },
"lifetimeValueCents": 128400,
"note": null,
"firstSeenAt": "2024-09-04T12:00:00Z",
"lastSeenAt": "2026-05-13T15:30:00Z",
"createdAt": "2024-09-04T12:00:00Z"
},
"conversations": [],
"stats": { "total": 12, "chats": 9, "tickets": 2, "calls": 1 }
}Create a contact
Plain create, not an upsert: there is no idempotent create-or-update-by-email endpoint. To sync from a CRM without duplicating records, search by email first (above), then POST only if nothing matches, otherwise PATCH the existing contact (below).
{
"name": "Alice Johnson",
"email": "[email protected]",
"phoneE164": "+14155550100",
"company": "Acme Inc"
}Update a contact
Partial update: name, company, note, tags, customFields. Email and phone are not editable through this endpoint.
{
"company": "Acme Inc",
"tags": ["vip"],
"customFields": { "plan": "pro", "mrr": 99 }
}There is no delete endpoint for contacts or visitors, and no merge endpoint for either resource.
Errors
| Code | When |
|---|---|
400 invalid_body | Contact create/update payload failed validation. |
400 empty_patch | Contact PATCH sent with no recognized fields. |
404 not_found | Visitor or contact does not exist in this workspace. |
Common pitfalls
- Expecting a plain create-or-update endpoint on visitors. No such endpoint exists. Use Contacts for CRM-style create/update.
- Expecting contact create to dedupe by email. It doesn't. Search first, then decide POST vs PATCH yourself.
- Confusing visitor conversations'
typefield with Conversations API'schannel. Same concept, different key name on this endpoint.