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

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 up to 200 recent conversations and counts calculated from that returned history. These are not all-time workspace totals.

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

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.