Build your integration / Watch contacts and prepare replies
GUIDE

Watch contacts and prepare replies

Use the Inbox API to show LeadTruffle conversations inside your CRM: find a contact, read the latest messages, keep selected conversations current, and discover which channels can accept a reply.

Available in this read API: inbox and message history, call summaries, bookings, booking-change requests, usage and reply-method discovery. Public message sending is not implemented yet. You can build the conversation viewer and reply-channel selector now; send replies from the LeadTruffle app until the public send operation is available. There is no supported POST /clients/{clientId}/messages operation in this contract.

Choose the right endpoint

All paths below follow https://pub-api.leadtruffle.com/api. Authenticate with X-API-Key from your server. A clientId is a contact ID; a lead inquiry ID identifies one interaction and cannot be substituted for it.

Task Endpoint Reference group
Find recently active contacts or contacts needing a human GET /v2/pub/inbox Inbox
Read one contact's profile, AI state and previews GET /v2/pub/clients/{clientId}/conversation Inbox
Read messages, newest first or oldest first GET /v2/pub/clients/{clientId}/messages Messages
Retrieve all saved text for a long message GET /v2/pub/clients/{clientId}/messages/{messageId}/body Messages
Combine messages, calls, bookings and activity GET /v2/pub/clients/{clientId}/timeline Inbox
Discover channels and targets for a reply GET /v2/pub/clients/{clientId}/send-methods Messages
Read call summaries and find transcript/recording IDs GET /v2/pub/clients/{clientId}/calls Calls
Find a contact's bookings GET /v2/pub/clients/{clientId}/bookings Bookings
Watch requested cancellations or reschedules GET /v2/pub/appointment-change-requests?clientId={clientId} Bookings
Check SMS/email allowances and billing eligibility GET /v2/pub/account/usage Usage

For a message viewer, a restricted key needs inbox:read. Add usage:read for allowances. The combined timeline requires inbox:read, calls:read and bookings:read together, even with a kind filter. See the permission table for calls and recordings.

1. Find a contact

Start with the most recently active contacts, including archived conversations:

curl --get 'https://pub-api.leadtruffle.com/api/v2/pub/inbox' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'view=all' \
  --data-urlencode 'limit=25'

Results are in data.items. Store each clientId with the company it belongs to. Follow data.nextCursor to find additional contacts. To search for a specific person, add search=Jamie (name, phone or email); verify the matching contact rather than assuming the first result is the right person. Once you know the ID, use clientId=CONTACT_UUID&view=all to refresh that contact's inbox state.

Use humanEscalation=open to build a human-takeover queue. aiState=paused is broader: it also includes ordinary pauses. Other useful filters include assignedTo=TEAMMATE_UUID, unassigned=true, channel=sms, and hasOpenBookingChangeRequest=true. Repeat a query key to match any value in that filter family. Different families are combined with AND.

2. Check the latest messages

Replace CONTACT_UUID with the selected clientId:

curl --get 'https://pub-api.leadtruffle.com/api/v2/pub/clients/CONTACT_UUID/messages' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'order=desc' \
  --data-urlencode 'limit=25'

data.items[0] is the newest returned message; the list can be empty. Both inbound and outbound messages are returned. Inspect direction=inbound in your application to find customer messages; there is no direction query parameter. Follow more pages if the first page contains only outbound messages. actor distinguishes contact, human, AI, API, automation or unknown origins. Do not treat every new row as an instruction to reply.

Use channel=email to restrict the list to a medium, or inquiryId=INQUIRY_UUID to inspect messages associated with one inquiry. Leave these filters off to watch the contact across all channels. order=asc is useful for an initial history import or chronological display.

Use /conversation for profile, AI/escalation state and bounded previews. Its ten recent messages are a preview: follow the nested message cursor at /messages for more. Use /timeline when you also need call summaries, bookings and booking-change activity. Reads never mark a conversation read.

If a message has textCompleteness=truncated, page its /messages/{messageId}/body endpoint and concatenate data.text. A 409 MESSAGE_CHANGED response means restart that body read. preview and unavailable mean the original text is not fully stored; paging cannot restore it. See history coverage.

3. Watch selected contacts

Keep a watch list of contact IDs in your CRM and poll each contact's /messages endpoint. No persistent watch registration or streaming endpoint is required or currently provided.

  1. Import initial history with order=asc, following every page. Upsert by company + contact + message ID; preserve IDs as opaque strings. Never deduplicate by text.
  2. For each later poll, set createdFrom to slightly before the last successfully completed window and createdTo to the start of this poll. A two-minute overlap is a reasonable starting point. These bounds concern occurredAt, not when the message was ingested.
  3. Follow nextCursor with those same bounds and filters until it is null. Save the new window checkpoint only after every page has been stored successfully. Start the next poll without the previous cursor.
  4. Refresh /conversation separately to keep AI/escalation state and contact details current. Periodically reconcile full history: late imports, edits, older timestamps and reordered legacy arrays can fall outside the overlap. This API is not a lossless change feed.

The following server-side JavaScript demonstrates one contact sync pass. upsertMessage and saveCheckpoint are your application's durable storage functions. Schedule the function through a shared request budget; do not run one unrestricted loop per CRM user. Omit checkpoint for the initial import.

async function syncContactMessages({
  apiKey, companyId, clientId, checkpoint,
  upsertMessage, saveCheckpoint,
}) {
  const through = new Date().toISOString();
  const from = checkpoint
    ? new Date(Date.parse(checkpoint) - 120_000).toISOString()
    : undefined;
  let cursor;

  do {
    const url = new URL(
      `https://pub-api.leadtruffle.com/api/v2/pub/clients/${encodeURIComponent(clientId)}/messages`,
    );
    url.searchParams.set('order', 'asc');
    url.searchParams.set('limit', '100');
    url.searchParams.set('createdTo', through);
    if (from) url.searchParams.set('createdFrom', from);
    if (cursor) url.searchParams.set('cursor', cursor);

    const response = await fetch(url, {
      headers: { 'X-API-Key': apiKey },
      signal: AbortSignal.timeout(20_000),
    });
    if (!response.ok) {
      // The scheduler should honor Retry-After for 429, back off on 503,
      // and fix permissions or parameters for 400/401/403.
      throw Object.assign(new Error(`Inbox read failed: ${response.status}`), {
        status: response.status,
        retryAfter: response.headers.get('Retry-After'),
      });
    }
    const { data } = await response.json();
    for (const message of data.items) {
      await upsertMessage({ companyId, clientId, message });
    }
    cursor = data.nextCursor;
  } while (cursor);

  await saveCheckpoint({ companyId, clientId, through });
}

A failed pass leaves the previous checkpoint intact; replaying the window is safe when upserts use message identity. Persist the checkpoint only after message writes have committed. A cursor expires after 24 hours; on 400 INVALID_CURSOR, restart the window from its beginning. Bound retries and surface persistent errors to the integration operator.

For company-wide discovery, poll /inbox?view=all and optionally use an overlapping activityFrom / activityTo window. Inbox activity uses indexed events and inquiry/contact creation, so it is a discovery signal rather than a guarantee that every unindexed message or metadata edit advances the contact. Poll watched contacts directly and periodically reconcile the complete contact list.

Start around one poll every 30 seconds for a small active watch list; slow down idle contacts. Ten contacts with one page each every 30 seconds consume 20 requests/minute, before inbox, profile, usage or additional-page reads. Share results across users, stagger requests, and leave headroom. A key allows 60 reads/minute with a burst of 10; a company allows 120/minute with a burst of 20 and four concurrent reads. Honor Retry-After on every 429.

Existing webhooks can trigger an earlier refresh where their event coverage applies. MESSAGE_REPLY is used for incoming SMS and Messenger replies; it is not an all-channel sent-message feed. Sent-message webhooks are planned for a later phase. Keep polling and reconciliation as the recovery path.

4. Prepare a reply to the right conversation

Fetch the contact's current state and eligible reply targets:

curl 'https://pub-api.leadtruffle.com/api/v2/pub/clients/CONTACT_UUID/conversation' \
  --header 'X-API-Key: YOUR_API_KEY'

curl --get 'https://pub-api.leadtruffle.com/api/v2/pub/clients/CONTACT_UUID/send-methods' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'limit=100'

curl 'https://pub-api.leadtruffle.com/api/v2/pub/account/usage' \
  --header 'X-API-Key: YOUR_API_KEY'

The send-method response has a methods summary and paginated target items. Follow nextCursor to see all targets. A contact can have several conversations on the same medium: select both the sendMethod and the exact sendTargetId, using its leadInquiryId to connect it to the conversation. Do not choose SMS automatically just because the contact has a phone number.

Inspect enabled, blockedReasonCodes, maxTextLength, allowedAiReplies and the current automation/escalation state. An open escalation takes precedence over ordinary AI settings. Keep the explicit aiReplies choice (off or on) with the prepared reply; only present choices returned in allowedAiReplies. These are preparations for the future sending contract, not an accepted send request.

SMS requires a currently approved, active 10DLC sender. Billing eligibility requires an active subscription, lead usage not above its limit, and sufficient SMS/email allowance for the selected method. Opt-outs, disconnected channels and expired reply windows can still block a target. Google LSA reports PROVIDER_REVIEW_REQUIRED until a send-time review occurs.

Stop at preparation while sendingAvailable=false. An enabled target means saved eligibility, not permission to send now. The future send operation will recheck eligibility and require an explicit method, target and AI on/off choice. The Inbox API does not yet provide operations to turn AI off, take over a contact, assign it or submit the message. Use the LeadTruffle app for those actions. The email lead-capture gateway and Yelp outbound webhook are different workflows, not substitutes for a general contact reply endpoint.

5. Watch booking requests alongside messages

For the selected contact, poll /v2/pub/appointment-change-requests?clientId=CONTACT_UUID&status=REQUESTED. Use status=ALL during reconciliation to find completed, declined or superseded requests. The combined timeline also includes booking-change activity with a bookingChangeRequestId; fetch /v2/pub/appointment-change-requests/{requestId} for the details.

To find contacts with future AI bookings, use /v2/pub/inbox?bookingOrigin=ai&bookingView=upcoming. Use bookingView=recently-booked for bookings created in the last seven days by default. A requested external slot with UNKNOWN status is not a confirmed future appointment. Reading or marking a request in your CRM does not cancel or reschedule the actual appointment; public booking-control operations are not available yet.

Guides & API endpoints Esc to close