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. The visitor actions include block, unblock, commerce enrichment, conversation history and administrator privacy export/erasure.
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 up to 200 recent conversations and counts calculated from that returned history. These are not all-time workspace totals.
{
"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 merge endpoint. Administrators can export personal data with GET /api/v1/contacts/:id/export or GET /api/v1/visitors/:id/export, and permanently erase it with POST /api/v1/contacts/:id/erase or POST /api/v1/visitors/:id/erase. Erasure requires { confirm: "…" }: use the contact’s email, otherwise its phone, otherwise ERASE; visitor erasure uses ERASE. This is irreversible and is not an ordinary archive action.
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.