Build your integration / Watch contacts and prepare replies
GUIDE

Watch contacts and send 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: inbox and message history, calls, bookings, booking-change requests, usage, reply-method discovery and immediate message sending. Read the conversation first, choose a valid target and an explicit AI setting, then send through POST /clients/{clientId}/messages.

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
Send one reviewed reply POST /v2/pub/clients/{clientId}/messages Messages
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. Discovery does not send anything or reserve permission; submit the reviewed reply explicitly as described below.

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 refreshes provider history before sending and refuses a reply if that history changed.

Send the reply now

Follow the complete message-sending walkthrough for setup, a target example, AI choices, saved JSON/curl requests, response examples and error recovery.

When sendingAvailable=true and the selected target is enabled, submit one immediate send. Use a key with messages:send and copy the chosen target's version into expectedConversationVersion:

export BASE_URL='https://pub-api.leadtruffle.com/api'
export API_KEY='YOUR_API_KEY'
export CLIENT_ID='CONTACT_UUID'

curl --max-time 35 -X POST "$BASE_URL/v2/pub/clients/$CLIENT_ID/messages" \
  -H "X-API-Key: $API_KEY" \
  -H "Idempotency-Key: crm-message-12345678" \
  -H "Content-Type: application/json" \
  -d '{
    "sendMethod": "sms",
    "sendTargetId": "COPY_TARGET_ID",
    "expectedConversationVersion": "COPY_TARGET_VERSION",
    "aiReplies": "off",
    "text": "Thanks! What time works for a quick call?"
  }'

The request sends immediately; there is no queue or automatic send retry. A 200 result contains status: "accepted" or "unknown". Accepted is not delivered. Yelp acceptance means a configured outbound webhook accepted the request; webchat acceptance means the reply was stored for the widget.

Choose aiReplies explicitly. off pauses the selected conversation and linked inquiry before dispatch. on enables ordinary paused controls. Neither option resolves a human escalation or overrides a contact-wide takeover timer. Other contact threads stay unchanged. A choice applied before an attempted send persists even if its provider result is uncertain.

If the HTTP request times out or returns unknown, repeat with the same API key, contact, identical body and Idempotency-Key to retrieve its result without sending again. Do not switch to a fresh key to retry an uncertain send. Keys/results remain in Redis for 24 hours; expired keys and loss of the store are outside this protection. There is no exactly-once guarantee. A changed body with the same key returns 409 IDEMPOTENCY_CONFLICT.

All methods require an active subscription/license and lead usage at or below the lead limit. SMS/email also require enough remaining capacity in that channel. SMS segments are charged using the final text after compliance processing. API SMS is rejected, rather than truncated, if that text exceeds 600 characters. Email costs one unit. These hard gates apply to the API; UI sending permissions are unchanged. An uncertain metered send retains its usage charge to avoid overspending.

Starting send limits: one concurrent send per company, 30 attempts/minute/company, 20/minute/key, 6/minute/contact, 20/hour/contact, 20/rolling-day/contact and 500/rolling-day/company. At most three attempted API replies per contact are allowed without a new customer message, across keys and channels. Confirmed pre-dispatch failures release that three-reply slot. 429 includes Retry-After; 409 AUTOMATION_REPLY_BUDGET_EXHAUSTED requires a new inbound message, not a timed retry. Every denial is logged, with bounded engineering alerts.

SMS/email limits also follow the actual destination across duplicate contact records: 6 attempts/minute, 20/hour, 20/rolling day and three without newer inbound activity. Changing contact IDs or API keys does not reset these limits.

If send methods report API_SENDING_PAUSED, API replies have been temporarily disabled for the company or selected channel. Sending returns 403 until the stop is removed; another key does not bypass it. Reading and dashboard replies remain available.

Include an optional externalReference for your CRM message identifier. It is saved with the request and AI-control audit for support investigation. Keep the returned requestId when investigating an uncertain send; there is no public reference-search or send-status endpoint.

Assignment and standalone AI/escalation changes remain future operations. Sent-message webhooks are Phase 2; use message polling for now.

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