message.comDevelopers

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

POST/api/v1/visitors/:id/blockAuth: Bearer

Sets blockedAt to now. No body. Returns the full visitor row, wrapped in visitor, not a two-field summary.

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

POST/api/v1/visitors/:id/unblockAuth: Bearer

Clears blockedAt. No body. Same response shape as block: the full visitor row wrapped in visitor.

Commerce enrichment

GET/api/v1/visitors/:id/enrichmentAuth: Bearer

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.

200 OK
{
  "enrichment": {
    "source": "woocommerce",
    "matched": true,
    "customer": { "email": "[email protected]", "lifetimeValueCents": 128400 },
    "orders": [ { "id": "4567", "total": "49.00", "status": "completed" } ]
  }
}

Visitor conversations

GET/api/v1/visitors/:id/conversationsAuth: Bearer

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.

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

GET/api/v1/contactsAuth: Bearer

Query params: search (matches name, email, phone, or company), limit (default 50, max 200), offset.

Code samples

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

GET/api/v1/contacts/:idAuth: Bearer

Full profile plus cross-channel conversation history and per-channel stats.

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

POST/api/v1/contactsAuth: Bearer

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).

Body
{
  "name": "Alice Johnson",
  "email": "[email protected]",
  "phoneE164": "+14155550100",
  "company": "Acme Inc"
}

Update a contact

PATCH/api/v1/contacts/:idAuth: Bearer

Partial update: name, company, note, tags, customFields. Email and phone are not editable through this endpoint.

Body
{
  "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

CodeWhen
400 invalid_bodyContact create/update payload failed validation.
400 empty_patchContact PATCH sent with no recognized fields.
404 not_foundVisitor 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' type field with Conversations API's channel. Same concept, different key name on this endpoint.