# LeadTruffle developer documentation Generated from the same guides and OpenAPI schema as https://api-docs.leadtruffle.com/. All local links below are relative to https://api-docs.leadtruffle.com. ## Guides # Make your first request Connect to LeadTruffle and read your first lead inquiry. This example only reads data. ## Get your API key Open [Settings → API Keys](https://app.leadtruffle.com/settings/api-keys) in your LeadTruffle dashboard. Use a key for the company you want to access. If you cannot see this setting, ask your company administrator for access. Keep this key on your server. Do not put it in website JavaScript, a public repository, or a widget embed. ## Send a request Replace `YOUR_API_KEY` with your key and run this in your terminal: ```bash curl --request GET \ --url 'https://pub-api.leadtruffle.com/api/v2/pub/leads-created?limit=1' \ --header 'X-API-Key: YOUR_API_KEY' ``` The base URL is `https://pub-api.leadtruffle.com/api`. Every REST API request needs the `X-API-Key` header. ## Check the result A successful request returns HTTP `200`. The response contains `success`, a `data.leads` array, and `data.hasMore`. An empty leads array can be a valid result when the company has no matching lead inquiries. See [List leads created](/reference/#tag/lead-inquiries/GET/v2/pub/leads-created) for the complete response schema. If you receive `401`, check the key and header. If you receive `429`, slow down your requests before trying again. See [Errors and pagination](/guides/troubleshooting/). ## Choose your next step - [Receive lead events](/guides/webhooks/) when a lead arrives or a conversation finishes. - [Explore the API reference](/reference/) to work with contacts, conversations, and calendar data. - [Add a website widget](/guides/widgets/) using the browser widget API. - [Give your AI agent the docs](/guides/ai-agents/) as a single text document. --- # How the API fits together A contact is the person or business you work with. A lead inquiry is a specific interaction with that contact. Understanding the difference helps you update the right record. ## Contacts and lead inquiries **Contacts** hold details such as names, phone numbers, email addresses, notes, and pipeline status. Existing API paths and payloads use `clients` and `clientId` for these records. **Lead inquiries** represent interactions such as a website conversation, phone call, or marketplace lead. A contact can have multiple lead inquiries. Use the IDs returned by the API rather than assuming a contact ID and a lead ID are interchangeable. ## Created and completed events A **lead created** payload contains the information available when the lead first arrives. A **conversation completed** payload contains the information collected through the conversation. Choose the event that matches your workflow. An initial lead event does not mean qualification or booking has finished. ## API versions The version is part of each endpoint path. V1 and V2 endpoints coexist; a V1 endpoint is not automatically obsolete. Check the specific operation's request and response schema. For new webhook integrations, use **V2 webhooks**. V1 webhook operations remain documented for existing integrations under Legacy webhooks. ## REST API and browser widgets Use the REST API from your server with an API key. Website widgets use their own browser JavaScript APIs and company or agency identifiers. They do not require you to expose your REST API key. See [Website widgets](/guides/widgets/) for the available methods and their differences. ## Operations that take action Read the description and prerequisites before calling an endpoint. Some operations update contacts, register webhook destinations, initiate messages, or trigger review requests. The lead qualifier trigger requires account approval and the prerequisites listed in its reference. A successful acceptance response does not guarantee that a message has already been delivered. See [Trigger lead qualifier agent](/reference/#tag/widgets/POST/v1/pub/chat-widget/trigger-lead-qualifier). --- # 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](/reference/#tag/inbox) | | Read one contact's profile, AI state and previews | `GET /v2/pub/clients/{clientId}/conversation` | [Inbox](/reference/#tag/inbox) | | Read messages, newest first or oldest first | `GET /v2/pub/clients/{clientId}/messages` | [Messages](/reference/#tag/messages) | | Retrieve all saved text for a long message | `GET /v2/pub/clients/{clientId}/messages/{messageId}/body` | [Messages](/reference/#tag/messages) | | Combine messages, calls, bookings and activity | `GET /v2/pub/clients/{clientId}/timeline` | [Inbox](/reference/#tag/inbox) | | Send one reviewed reply | `POST /v2/pub/clients/{clientId}/messages` | [Messages](/reference/#tag/messages) | | Discover channels and targets for a reply | `GET /v2/pub/clients/{clientId}/send-methods` | [Messages](/reference/#tag/messages) | | Read call summaries and find transcript/recording IDs | `GET /v2/pub/clients/{clientId}/calls` | [Calls](/reference/#tag/calls) | | Find a contact's bookings | `GET /v2/pub/clients/{clientId}/bookings` | [Bookings](/reference/#tag/bookings) | | Watch requested cancellations or reschedules | `GET /v2/pub/appointment-change-requests?clientId={clientId}` | [Bookings](/reference/#tag/bookings) | | Check SMS/email allowances and billing eligibility | `GET /v2/pub/account/usage` | [Usage](/reference/#tag/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](/guides/inbox/#access-and-permissions) for calls and recordings. ## 1. Find a contact Start with the most recently active contacts, including archived conversations: ```bash 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`: ```bash 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](/guides/inbox/#read-a-conversation-and-discover-reply-targets). ## 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. ```javascript 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](/guides/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: ```bash 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](/guides/inbox/#send-a-message) 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`: ```bash 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. --- # Inbox API and data coverage Bring contact conversations, calls, bookings and usage into your CRM. Start with [Watch contacts and send replies](/guides/inbox-workflows/) for an end-to-end integration recipe; use this guide for [detailed message-sending instructions](#send-a-message), field semantics, permissions and coverage. The read API includes **Inbox**, **Messages**, **Calls**, **Bookings** and **Usage** resource groups. Messages support immediate replies with explicit channel and AI choices. Assignment changes and standalone AI/escalation controls are not available yet. Reads never send a message or change conversation state. ## Access and permissions Use an active company API key in the `X-API-Key` header. Keep the key on your server. Read permission is independent of subscription sending eligibility, so an integration can still inspect an account whose sending is blocked. | Resource | Required scope for a restricted key | | --- | --- | | Inbox, conversation, messages, full message bodies, send methods | `inbox:read` | | Combined timeline | `inbox:read` **and** `calls:read` **and** `bookings:read` | | Calls and transcripts | `calls:read` | | Recording links | `calls:media:read` | | Bookings and booking-change requests | `bookings:read` | | Account usage | `usage:read` | | Send a reply | `messages:send` | Null scopes on existing keys and `["ALL_SCOPES"]` grant all current and future scopes. New keys default to `["ALL_SCOPES"]`; `[]` grants none. Omitting scopes when editing a key preserves its permissions. Scope restrictions apply to these Inbox operations; legacy API routes retain their existing permissions. No creation-date cutoff applies. ## Find contacts and automation state Use `GET /v2/pub/inbox`. Results default to unarchived contacts, newest activity first; `view=all` includes archived contacts. Activity uses indexed conversation/call/escalation events and inquiry/contact creation. Internal notes and read/bookkeeping events do not advance it. Search covers names, phone and email. Filter by assignment, lead status, acquisition source, original source, conversation channel, AI controls or human escalation. Repeat query keys for OR within a filter, for example `source=WEBCHAT&source=YELP_LEAD`; different filter families are combined with AND. Acquisition source is independent of conversation medium. Original source uses the latest resolved inquiry provenance. `automation.controlState` reports `enabled`, `paused`, `mixed` or `unavailable` across existing reply threads, independently of sender availability. An open human escalation takes precedence: `automation.aiState` becomes `paused` while the underlying control state stays visible. These are reply-control settings, not a guarantee that provider connections or account-wide automation are ready. `humanEscalation=open` finds contacts needing takeover; an ordinary AI pause is not an escalation. Booking filters apply across the contact's recorded bookings **before pagination**: ```text GET /v2/pub/inbox?bookingOrigin=ai&bookingView=upcoming GET /v2/pub/inbox?bookingOrigin=ai&bookingView=recently-booked GET /v2/pub/inbox?hasOpenBookingChangeRequest=true&bookingChangeRequestType=RESCHEDULE ``` `upcoming` requires a confirmed future appointment with `BOOKED` status. `recently-booked` uses booking creation time, defaults to the preceding seven days, and accepts `bookedFrom` / `bookedTo` overrides. `appointmentStartFrom` / `appointmentStartTo` concern confirmed appointment times. All booking predicates must match the same booking. `matchedBookingIds` contains at most 25 matches; `matchedBookingsHaveMore` signals more results. Use the contact booking list to retrieve them. `hasUpcomingAiBooking=null` means no confirmed upcoming appointment was found but external AI booking status is unknown; inspect `unknownAiBookingCount` and `bookingCoverage`. Do not interpret null as false. ## Read a conversation and discover reply targets `GET /v2/pub/clients/{clientId}/conversation` returns the public contact profile, opt-outs, assignment/status, AI/escalation and booking state, up to 25 inquiry summaries, 10 recent messages and 25 reply targets. Follow the nested message/target cursors through their respective endpoints. `inquiriesHaveMore` marks a partial inquiry preview; use the existing lead-inquiry endpoints for full inquiry records. Call summaries/details and bookings have separate endpoints and scopes. Use `GET /v2/pub/clients/{clientId}/messages?order=asc` to load stored message history oldest first. This combines existing SMS, email, Facebook Messenger, Google LSA, Thumbtack, Yelp and webchat records with messages retained only in the history index. `coverage=stored_sources_and_index` includes legacy records even if they were never indexed; reads do not backfill or contact providers. IDs are opaque strings (`message:hash` or `history:bigint`). Preserve them unchanged. Matching source/index identities appear once; equal text remains distinct. Legacy entries without message IDs use their stored array position, so reconcile if a thread is reordered. `createdFrom` / `createdTo` filter occurrence time. Message lists include up to 20,000 Unicode code points per message. `textCompleteness` is `complete`, `preview`, `truncated` or `unavailable`. To retrieve all saved text, use: ```text GET /v2/pub/clients/{clientId}/messages/{messageId}/body?limit=10000 ``` Concatenate `text` from each page in cursor order until `nextCursor=null`. `limit` counts Unicode code points (default 10,000; maximum 20,000). HTML-only email is converted to plain text; scripts and embedded assets are excluded. A `409 MESSAGE_CHANGED` response means restart without the cursor. `availability=preview` means only the indexed preview survives; `unavailable` means no saved text. Deleted or never-stored content cannot be recovered by this API. Unknown outbound authors remain `unknown`; attachments and raw provider payloads are excluded. Use `GET /v2/pub/clients/{clientId}/timeline?order=asc` for a combined chronological view of messages, call summaries, bookings, escalation decisions, inquiry creation, booking-change requests and supported indexed activity. Filter by `kind=message|call|booking|escalation|activity` and occurrence date bounds. Follow `nextCursor` to retrieve the full stored timeline. Booking-change activity includes `bookingChangeRequestId` for the request detail endpoint. Call, booking, escalation and request items show saved state at record creation time; this is not an exhaustive audit log of every transition. Calls do not include transcripts or recording links in this view. **Timeline access requires all three scopes: `inbox:read`, `calls:read` and `bookings:read`, even with a kind filter.** Legacy null scopes and `ALL_SCOPES` continue to work. Message bodies require `inbox:read`. For narrower permissions, use the separate message, call or booking endpoints. Use `GET /v2/pub/clients/{clientId}/send-methods` to discover existing targets for SMS, email, Facebook Messenger, Google LSA, Thumbtack, Yelp and webchat. The method list reports `NO_REPLY_TARGET` when no target exists. Each target includes its method, inquiry, AI choices, version, text-length limit and blockers. SMS requires an active approved 10DLC sender; opt-outs, billing limits, connection state and reply windows are checked. Messenger uses the configured enabled page and its saved reply window. Google LSA performs its refresh/review handshake during sending. **Check `sendingAvailable` and target eligibility before replying.** Send with `POST /v2/pub/clients/{clientId}/messages`, an `Idempotency-Key`, and explicit `sendMethod`, `sendTargetId`, `aiReplies` and `expectedConversationVersion` copied from the chosen target. There is no channel fallback, shared SMS sender, queue or automatic retry. Open escalations block sending. Attachments are unsupported. See the [send workflow](/guides/inbox-workflows/) for outcomes, limits and retry handling. ## Send a message Sending uses **`POST /v2/pub/clients/{clientId}/messages`**. It sends one immediate reply through an existing contact conversation. There is no send queue, channel fallback or automatic provider retry. The [Messages reference](/reference/#tag/messages) describes the request schema; the steps below show how to use it safely from your CRM or agent. ### 1. Set up access and read the contact Use a server-side API key with `inbox:read` and `messages:send`; add `usage:read` to inspect allowances. Existing null-scope keys and `ALL_SCOPES` include these permissions. Keep the key out of browsers and agent prompts exposed to customers. Set these placeholders before running the examples. `CLIENT_ID` is the **contact** ID returned by `/inbox`, not a lead inquiry ID: ```bash export BASE_URL='https://pub-api.leadtruffle.com/api' export API_KEY='YOUR_API_KEY' export CLIENT_ID='CONTACT_UUID' curl --silent --show-error "$BASE_URL/v2/pub/clients/$CLIENT_ID/conversation" \ --header "X-API-Key: $API_KEY" curl --silent --show-error "$BASE_URL/v2/pub/clients/$CLIENT_ID/messages?order=desc&limit=25" \ --header "X-API-Key: $API_KEY" curl --silent --show-error "$BASE_URL/v2/pub/account/usage" \ --header "X-API-Key: $API_KEY" ``` Read the recent customer message and check AI/escalation state before drafting a reply. Every method requires an active subscription or license and lead usage **at or below** its allowance. SMS and email also require enough remaining sending credits. An ordinary AI pause can be compatible with an explicit API reply; an open human escalation blocks sending. ### 2. Discover and choose the exact reply target ```bash curl --silent --show-error --get \ "$BASE_URL/v2/pub/clients/$CLIENT_ID/send-methods" \ --header "X-API-Key: $API_KEY" \ --data-urlencode 'limit=100' ``` Inspect `data.items` and follow `data.nextCursor` with the same filters until you find the intended conversation. You can filter with `sendMethod=email`, for example. `data.methods` summarizes channel availability; the actual selectable targets are in `data.items`. A contact can have several targets with the same method. Use `leadInquiryId` to match the target to the inquiry you are replying to; never assume the first item is correct. Example **target item** (illustrative IDs and version; use the values returned for your contact): ```json { "sendTargetId": "email:22222222-2222-4222-8222-222222222222", "sendMethod": "email", "leadInquiryId": "33333333-3333-4333-8333-333333333333", "label": "email conversation", "enabled": true, "blockedReasonCodes": [], "aiState": "paused", "allowedAiReplies": ["off", "on"], "version": "COPY_THE_CURRENT_TARGET_VERSION", "maxTextLength": 5000, "supportsAttachments": false, "requiresProviderRefresh": false } ``` Proceed only when sending is available, the selected target is `enabled`, and your AI choice appears in `allowedAiReplies`. Treat `sendTargetId` and `version` as opaque values: copy them; do not construct them or substitute a contact ID. Eligibility is checked again immediately before dispatch, so discovery is not a reservation. | `sendMethod` | What the selected target means | | --- | --- | | `sms` | An existing eligible inquiry and the company's active, approved 10DLC sender. A phone number alone is insufficient. | | `email` | An existing email thread. The thread determines the actual recipient and reply routing, which can differ from the contact's primary email. | | `facebook-messenger` | The existing Messenger conversation on the connected page, within its eligible reply window. | | `google-lsa` | An eligible Google LSA message conversation. Sending refreshes provider history and rejects a changed conversation for review. | | `thumbtack` | The existing Thumbtack conversation. | | `yelp` | The existing Yelp conversation and its configured outbound webhook relay. | | `webchat` | The existing webchat conversation; the reply is stored for the widget. | `NO_REPLY_TARGET` means this endpoint cannot initiate that channel for the contact. It does not create a new email thread, a new SMS association or a marketplace conversation. No arbitrary destination override is accepted. ### 3. Choose what happens to AI replies You must submit one of these values—there is no default: | `aiReplies` | Effect | | --- | --- | | `off` | Pause AI for the selected conversation and its linked inquiry before sending. Use this when your CRM team or external agent is taking over. | | `on` | Enable ordinary paused AI controls for the selected conversation and linked inquiry. This does not ask the AI to generate another reply immediately. | Neither choice resolves a human escalation or overrides a protected contact-wide takeover timer. Other conversations on the contact are unchanged. If dispatch is attempted but its result is uncertain, the applied AI setting remains; refresh `/conversation` to inspect current state. ### 4. Save one request, then send it Save the following **request body** as `reply.json`, replacing the example target/version with your selected values. The message is plain text. A custom subject, HTML body, attachments, destination address and extra fields are not accepted by this endpoint. ```json { "sendMethod": "email", "sendTargetId": "email:22222222-2222-4222-8222-222222222222", "expectedConversationVersion": "COPY_THE_CURRENT_TARGET_VERSION", "aiReplies": "off", "text": "Thanks for your message. What time works for a quick call?", "externalReference": "crm-reply-12345678" } ``` `text` is required and must fit the target's `maxTextLength` (at most 5,000). For SMS, final text including compliance language must fit 600 characters; it is rejected rather than silently truncated. SMS consumes segments; email consumes one credit. `externalReference` is optional (maximum 200 characters) and helps support correlate a CRM message; it is **not** the deduplication key or a public search filter. Choose a unique `Idempotency-Key` for this one intended reply: 16–128 letters, digits, hyphens or underscores. A UUID or a stable CRM message identifier works. Persist the key, contact ID, API-key identity and exact JSON body **before** the HTTP call. The example below requires the `reply.json` you just saved; replace the example key with a unique value for each new intended message. ```bash export IDEMPOTENCY_KEY='crm-reply-12345678' curl --silent --show-error --max-time 35 --request POST \ "$BASE_URL/v2/pub/clients/$CLIENT_ID/messages" \ --header "X-API-Key: $API_KEY" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-binary @reply.json \ --dump-header reply-response.headers \ --output reply-response.json \ --write-out 'HTTP %{http_code}\n' ``` Do not add an automatic retry with a newly generated key. Let your HTTP client wait longer than the approximately 29-second gateway window so it can receive the API outcome. The server bounds provider requests to the available time; a timeout still cannot prove that no message was sent. ### 5. Interpret the result and verify history Example **accepted response**: ```json { "success": true, "data": { "requestId": "44444444-4444-4444-8444-444444444444", "status": "accepted", "sendMethod": "email", "sendTargetId": "email:22222222-2222-4222-8222-222222222222", "aiReplies": "off", "acceptedAt": "2026-09-29T12:00:00.000Z", "providerMessageId": "" } } ``` An HTTP `200` can contain either `accepted` or `unknown`; always inspect `data.status`. **Accepted is not delivered.** It means provider acceptance, a successful Yelp relay webhook response, or local storage for webchat. `providerMessageId` may be null. Save `requestId` for support. Refresh `/messages?order=desc` to display the new message. Stored API replies have `direction: "outbound"` and `actor: "api"`, and the application inbox labels them **API**. A missing history row after a timeout is not proof of non-delivery. Public `deliveryStatus` may be null even when internal provider receipt evidence exists; it is not a universal delivery-status feed. Sent-message webhooks and a standalone send-status endpoint are not available yet. ### Retry and error handling For **`unknown`, HTTP timeout, network disconnect or an ambiguous 5xx**, repeat the original POST with the **same API key, same contact, same Idempotency-Key and identical body**. Do not refresh the version inside that saved body, switch API keys or generate another key to investigate an uncertain send. Within retention, the recorded result is replayed without sending or charging again. A replay can remain `unknown`; stop automated retries and investigate with the saved request ID and inbox/provider evidence. Results are retained for 24 hours. After expiry or loss of the replay store, the same key no longer guarantees replay. Do not automatically resend an unresolved message after that window. There is no exactly-once guarantee across storage loss or expiry. | Response/code | What to do | | --- | --- | | `400` invalid input | Correct the missing/invalid fields before sending. All routing and AI choices are required. | | `401` / `403 FORBIDDEN` | Fix key validity/scope. Do not change keys to replay an uncertain previous send. | | `403 SUBSCRIPTION_INACTIVE`, `LEAD_LIMIT_EXCEEDED`, `SMS_LIMIT_EXCEEDED`, `EMAIL_LIMIT_EXCEEDED` | Inspect `/account/usage` and resolve eligibility/capacity. Do not fall back to another method to evade limits. | | `403 API_SENDING_PAUSED` | API sends are paused for this company/channel. Reads and dashboard replies remain available. | | `409 CONVERSATION_CHANGED` | The rejected request did not dispatch. Read current messages and targets, review a new reply, then use the new version and a new idempotency key. Replaying the old rejected request does not re-evaluate it. | | `409 HUMAN_ESCALATION_OPEN` / `AI_CONTROL_CONFLICT` | Have the team review takeover state in the inbox. An API reply cannot override it. | | `409 IDEMPOTENCY_CONFLICT` | The key was used with different content/contact. Recover the original saved request; do not create a fresh key to bypass an uncertain result. | | `409 AUTOMATION_REPLY_BUDGET_EXHAUSTED` | Wait for a new customer message. A timed retry, different API key or duplicate contact does not solve the loop limit. | | `422` invalid content or unavailable preparation | Review the returned code and refresh targets. Correct a definitively rejected request before creating a new intended attempt. | | `429 RATE_LIMITED` | Honor `Retry-After` and add backoff. Replaying the same request/key is safe, but some definitive rejections are cached. Once the limit clears, refresh and review the target before creating a new attempt/key only if the prior attempt was definitively rejected. Never replace the key for an uncertain send. | | `503` or any uncertain transport result | Preserve and replay the original request/key. Escalate persistent uncertainty; never assume a generic error means no provider call occurred. | New send attempts are limited to one concurrent send/company, 30/minute/company, 20/minute/key, 6/minute/contact, 20/hour/contact, 20/rolling 24 hours/contact and 500/rolling 24 hours/company. Only three attempted API replies/contact are allowed without a new inbound message, across methods and keys. SMS/email also share recipient limits across duplicate contact records (6/minute, 20/hour, 20/rolling 24 hours and three without newer inbound activity). Confirmed pre-dispatch failures release the three-reply slot but still count toward rolling attempt limits. For an external AI agent: poll with deduplication, respond only to a new customer message that needs a reply, select an eligible target explicitly, and normally choose `aiReplies: "off"` when the external agent owns the conversation. Never respond to your own outbound API message or treat an unknown outcome as a reason to send again. See [watching selected contacts](/guides/inbox-workflows/#3-watch-selected-contacts) for polling and checkpoint details. ## Poll from a CRM 1. Poll `/v2/pub/inbox?view=all` about once every 30 seconds per company, sharing the result across CRM users. Back off while idle. 2. Follow `nextCursor` with unchanged filters, deduplicate contact IDs, then fetch conversations or messages for contacts of interest. Messages, targets, calls and bookings have their own pagination. 3. Rescan an overlapping activity window and periodically reconcile all contacts, including archives. Mutable activity ordering is not a snapshot or a lossless change feed; metadata-only changes may not advance activity. `asOf` keeps relative booking dates fixed during an inbox traversal. 4. Keep every cursor with its tenant, endpoint and filters. Cursors expire after 24 hours. Restart a traversal after `400 INVALID_CURSOR`; use `Retry-After` and backoff on `429`, and bounded retries on temporary `503` errors. Reads default to 25 records and cap at 100 unless a specific endpoint documents a different limit. Date bounds include `From` and exclude `To`. Company and key buckets refill at 120 and 60 requests/minute with bursts of 20 and 10; a company may have four concurrent reads. No GET sends messages, marks conversations read, changes AI controls or refreshes a provider. ## Read usage and sending limits Use `GET /v2/pub/account/usage` to inspect the billing period, lead usage, and separate SMS/email limits. SMS is measured in segments. Each limit uses its subscription or license override when present, otherwise the plan allowance. A null SMS/email override uses the plan default; zero means no sending capacity. These are not API request-rate allowances. The API sending policy requires an active subscription, lead usage not above the lead limit, and sufficient capacity for the selected message method. Trial-only and past-due accounts do not meet that sending policy. Read permission remains separate so an integration can inspect usage when sending would be blocked. `apiBillingEligible` and the per-method `billingEligibility` describe billing only. `sendingAvailable` reports endpoint availability, not account permission. These values do not grant permission to send through other endpoints, override opt-outs, or promise that a channel is connected. Reservations remain zero: synchronous sends atomically debit the existing meter before provider dispatch. Unknown attempts retain their debit; confirmed pre-dispatch failures refund it. ## Find requested booking changes Use `GET /v2/pub/appointment-change-requests`. The default status is `REQUESTED`. Supported filters include `clientId`, `requestType`, `bookingProvider`, `createdFrom` and `createdTo`. Use `status=ALL` for resolved requests too. ```bash curl --request GET \ --url 'https://pub-api.leadtruffle.com/api/v2/pub/appointment-change-requests?status=REQUESTED&limit=50' \ --header 'X-API-Key: YOUR_API_KEY' ``` Follow `data.nextCursor` until it is null. Keep the same filters on subsequent requests. Cursors expire after 24 hours; begin a new traversal if one expires. Dates use an inclusive lower bound and exclusive upper bound. Status can change while paging, so refresh recent pages and reconcile by request ID. Retrieve one request with `GET /v2/pub/appointment-change-requests/{requestId}`. Request types are `CANCEL` and `RESCHEDULE`; statuses are `REQUESTED`, `COMPLETED`, `DECLINED` and `SUPERSEDED`. Some records have no linked contact or native appointment, and remain visible in the company queue. A completed request means the request workflow was marked handled. These read endpoints do not cancel or reschedule an appointment. Internal notes, notification recipients and raw provider diagnostics are excluded. ## Read bookings Use `GET /v2/pub/clients/{clientId}/bookings` and follow `nextCursor`. Use `origin=ai&view=upcoming` to find confirmed future AI appointments for a contact. `bookedFrom` / `bookedTo` filter when a booking was created; `appointmentStartFrom` / `appointmentStartTo` filter its scheduled time. Retrieve one result with `GET /v2/pub/bookings/{bookingId}`. Native appointments and recorded booking successes across the contact's inquiries are combined. Repeated successes for the same external booking are deduplicated. AI origin requires evidence; being associated with an AI-qualified contact is insufficient. `openChangeRequestCount` links this view to the requested-change queue. For external bookings, `status=UNKNOWN` and `statusFreshness=last_observed` mean the API has not queried the CRM for current status. `observedAt` gives the evidence time. `timeSource=requested_slot` distinguishes a recorded requested slot from a native appointment time. External records are excluded from `view=upcoming` because a requested slot is not confirmation that an appointment remains booked. Use the unfiltered list to inspect them; they might since have been moved or canceled. Missing or malformed times stay null. Coverage is limited to local appointments and recorded booking actions, rather than every booking in your CRM. ## Read calls and transcripts Use `GET /v2/pub/clients/{clientId}/calls` for a bounded list, filtered by `kind=ai|outbound|missed` and optional creation-date bounds. Results are newest first by record creation time and ID. IDs such as `ai:UUID`, `outbound:UUID`, and `missed:UUID` identify LeadTruffle records, not provider IDs. `GET /v2/pub/calls/{callId}` returns metadata, summary and saved-content flags. `GET /v2/pub/calls/{callId}/transcript` returns plain text in bounded pages; `limit` counts Unicode code points (default 10,000; maximum 20,000). Concatenate pages in cursor order. The API does not invent speakers or timestamps. `not_available` means no saved nonblank transcript. If the transcript changes during pagination, `409 TRANSCRIPT_CHANGED` means restart without a cursor. Recordings require the separate `calls:media:read` permission. `GET /v2/pub/calls/{callId}/recordings` returns five-minute proxy download links when configured. Each `GET` or `HEAD` of a link rechecks the API key, company, media scope and current call ownership. Treat the URL as a secret; request a fresh one when it expires. Call summaries and transcripts never embed recording URLs. The proxy supports single byte ranges for audio players. Downloads use the same read rate budget and allow two concurrent downloads per company, eight per server, files up to 100 MiB and a 60-second request timeout. Resume a large transfer with byte ranges. A `429` response includes `Retry-After`; an expired or revoked link returns `403`. Missing files return `404`, invalid ranges `416`, oversized files `422`, and temporary failures `503`. `proxy_pending` means link issuance is not configured; `not_available` means there is no saved recording link; `not_archived` means only unsupported/provider links exist. These responses have an empty `recordings` array. Issuing a link does not verify the saved file still exists. The API never falls back to bucket or provider URLs. **Proxy expiry does not make a legacy public origin private.** Storage permissions have not changed; private-origin rollout and deployed playback verification remain separate requirements. --- # Receive lead events Have LeadTruffle send events to your application when a lead arrives, a conversation finishes, or another supported event occurs. ## Prepare your receiver Create an HTTPS endpoint in your application that accepts JSON POST requests. Keep the raw request body available so you can verify signed requests before parsing JSON. Use V2 webhooks for new integrations. You will need your LeadTruffle API key, your receiver's URL, and a shared secret for signature verification. ## Choose an event | Event | Use it to | | --- | --- | | `LEAD_CREATED` | Capture a lead as soon as it arrives | | `CONVERSATION_COMPLETED` | Receive the completed conversation and collected details | | `MESSAGE_REPLY` | Refresh a contact after an incoming SMS or Messenger reply | | `CLIENT_STATUS_CHANGED` | Track changes to a contact's pipeline status | | `NEW_APPOINTMENT` | Receive a new calendar booking | | `YELP_MESSAGE_OUTBOUND` | Connect outbound Yelp messages to your integration | The [Create V2 webhook reference](/reference/#tag/webhooks/POST/v2/pub/webhooks) includes payload examples and the full set of supported event values. ## Register your webhook This request creates a webhook subscription. Replace the example URL with your receiver and use your own shared secret. ```bash curl --request POST \ --url 'https://pub-api.leadtruffle.com/api/v2/pub/webhooks' \ --header 'X-API-Key: YOUR_API_KEY' \ --header 'Content-Type: application/json' \ --data '{ "eventType": "LEAD_CREATED", "url": "https://your-app.example/webhooks/leadtruffle", "secret": "YOUR_WEBHOOK_SECRET" }' ``` A successful registration returns HTTP `201`. Keep the webhook ID from the response so you can update or remove the subscription later. ## Verify incoming deliveries Follow [Verify webhook signatures](/guides/webhook-signatures/) to check the timestamp and signature against the raw request body. The shared secret is separate from your LeadTruffle API key. Design your receiver so a duplicate event cannot create duplicate downstream actions. Save the incoming event before handing off slow work. ## Confirm the integration Use [List V2 webhooks](/reference/#tag/webhooks/GET/v2/pub/webhooks) to verify the registration. Then confirm that a matching event reaches your receiver and that your application processes it successfully. Registration alone does not verify delivery. Use [Update V2 webhook](/reference/#tag/webhooks/PUT/v2/pub/webhooks/{id}) to change a destination or disable a subscription. For ongoing conversations, follow [Watch contacts and prepare replies](/guides/inbox-workflows/). Existing reply events are not a complete all-channel or sent-message feed; use message polling and periodic reconciliation to recover missed or delayed updates. --- # Verify webhook signatures If you set a `secret` on a V2 webhook, LeadTruffle signs each delivery with HMAC SHA-256 headers: - `x-leadtruffle-timestamp`: Unix timestamp in seconds - `x-leadtruffle-signature`: `v1=` followed by the hex HMAC SHA-256 signature - `x-leadtruffle-signature-algorithm`: `hmac-sha256` The signature is computed over `${timestamp}.${rawRequestBody}` using the webhook secret as the HMAC key. Verify the signature against the raw request body before parsing JSON. Python verification example: ```python import hmac, hashlib, time raw_body = request.get_data() timestamp = request.headers["x-leadtruffle-timestamp"] signature = request.headers["x-leadtruffle-signature"].removeprefix("v1=") try: timestamp_seconds = int(timestamp) except ValueError: raise Exception("invalid timestamp") if abs(time.time() - timestamp_seconds) > 5 * 60: raise Exception("stale webhook") expected = hmac.new( b"YOUR_WEBHOOK_SECRET", timestamp.encode("utf-8") + b"." + raw_body, hashlib.sha256, ).hexdigest() if not hmac.compare_digest(expected, signature): raise Exception("invalid signature") ``` PHP verification example: ```php $rawBody = file_get_contents('php://input'); $timestamp = $_SERVER['HTTP_X_LEADTRUFFLE_TIMESTAMP']; $signature = preg_replace('/^v1=/', '', $_SERVER['HTTP_X_LEADTRUFFLE_SIGNATURE']); if (!ctype_digit($timestamp) || abs(time() - intval($timestamp)) > 5 * 60) { http_response_code(401); exit; } $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, 'YOUR_WEBHOOK_SECRET'); if (!hash_equals($expected, $signature)) { http_response_code(401); exit; } ``` --- # Website widgets LeadTruffle also provides browser-embedded web clients (chat widgets). These JavaScript APIs are separate from the REST API and are available after each widget script loads. ## Install your widget 1. Copy the installation snippet for your company from the widget setup in your [LeadTruffle dashboard](https://app.leadtruffle.com). 2. Paste the snippet before the closing `` tag on each page where the widget should appear, or use your website builder's equivalent footer-code setting. 3. Publish the website change, open the page, and confirm the widget appears. Keep your REST API key out of the embed code. The snippet loads and initializes the widget. The methods below are for additional customization after its script has loaded. ## Choose the right browser API ### Standard Chat Widget (`window.LTWidget`) Methods: - `initialize({ companyId, initialMessage? })` - `open({ initialMessage? })` - `setAttribution({ gclid?, utm_source?, utm_medium?, utm_campaign?, utm_term?, utm_content?, ... })` - `destroy()` Example: ```js if (window.LTWidget && typeof window.LTWidget.initialize === 'function') { window.LTWidget.initialize({ companyId: 'YOUR_COMPANY_UUID', initialMessage: 'Hi! How can we help today?', }) } if (window.LTWidget && typeof window.LTWidget.open === 'function') { window.LTWidget.open({ initialMessage: 'Need help with pricing or scheduling?', }) } if (window.LTWidget && typeof window.LTWidget.setAttribution === 'function') { window.LTWidget.setAttribution({ gclid: 'GOOGLE_CLICK_ID', utm_source: 'google', utm_medium: 'cpc', utm_campaign: 'spring-service', }) } ``` ### Franchise Widget (`window.FranchiseLeadtruffle`) Methods: - `initialize({ agencyId, initialMessage? })` - `open({ initialMessage? })` - `setAttribution({ gclid?, utm_source?, utm_medium?, utm_campaign?, utm_term?, utm_content?, ... })` - `destroy()` ### Popup Widget (`window.TPOPWidget`) Methods: - `initialize({ companyId })` - `show()` - `reset()` - `setAttribution({ gclid?, utm_source?, utm_medium?, utm_campaign?, utm_term?, utm_content?, ... })` - `destroy()` ### Other Web Clients - `window.LTWebchat`: `initialize({ companyId, initialMessage? })`, `prefillLead({ name?, email?, phone?, metadata? })`, `setAttribution({ gclid?, utm_source?, utm_medium?, utm_campaign?, utm_term?, utm_content?, ... })`, `open({ initialMessage? })`, `destroy()` - `window.TJSFormWidget`: `initialize({ companyId, targetElement? })`, `setAttribution({ gclid?, utm_source?, utm_medium?, utm_campaign?, utm_term?, utm_content?, ... })`, `show()`, `hide()`, `destroy()` `prefillLead(...)` is only available on the webchat widget. It lets you pre-populate the initial lead form and attach custom metadata that will be submitted with that webchat lead. This does not apply to the standard website texting widget, popup widget, franchise widget, or JS form widget. `setAttribution(...)` is available on all current JavaScript widget clients. LeadTruffle automatically captures supported URL parameters and falls back to the Google Ads `_gcl_aw` first-party cookie for `gclid` when available. Use `setAttribution(...)` only when your site or tag manager already has attribution values that you want to push into the LeadTruffle widget context. Supported fields include `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `gclid`, `fbclid`, `msclkid`, `ttclid`, `snapcid`, `gbraid`, `wbraid`, `gad_source`, `igshid`, `gclsrc`, and `srsltid`. Example: ```js if (window.LTWebchat && typeof window.LTWebchat.initialize === 'function') { window.LTWebchat.initialize({ companyId: 'YOUR_COMPANY_UUID', }) } if (window.LTWebchat && typeof window.LTWebchat.prefillLead === 'function') { window.LTWebchat.prefillLead({ name: 'Jane Smith', email: 'jane@example.com', phone: '+15555550123', metadata: { userId: 'internal-user-123', sourceApp: 'tooldesk-next', }, }) } ``` ### Browser Compatibility Guidance Because embeds can run on unknown/older browsers and third-party pages: - Always check object and method existence before calling (`if (window.X && typeof window.X.method === 'function')`). - Wrap manual widget API calls in `try/catch` to avoid breaking host page JavaScript. - Call `initialize(...)` only after the widget script has loaded (for example in the script `onload` handler). - Use `destroy()` before re-initializing with a different `companyId`/`agencyId`. --- # Errors and pagination Start with the endpoint's response schema and the HTTP status code. Different endpoints can use different response shapes and pagination cursors. ## Authentication errors For a `401` response, check that you sent the `X-API-Key` header and that its value is a valid key for the intended company. Keep the key private when sharing logs or asking for support. ## Request errors For a `400` response, compare your request with the endpoint's required fields, enum values, and examples. For a `404`, check the record ID and the company associated with your key. ## Rate limits The API rate limit is company-based. Plan for roughly one request per second and slow down when you receive `429`. Contact [LeadTruffle support](mailto:support@leadtruffle.com) if your integration needs a higher rate. Before retrying a request that creates data or sends a message, check whether the first attempt took effect. A network timeout alone does not prove the operation failed. ## Pagination Follow the cursor documented on the specific endpoint. Do not assume every list operation uses the same parameters. For [List leads created](/reference/#tag/lead-inquiries/GET/v2/pub/leads-created): 1. Send the first request with your desired `limit` (up to 100). 2. Process the returned `data.leads`. 3. If `data.hasMore` is true, use the oldest returned lead's `timestamp` as the next request's `before` value. 4. Continue until `data.hasMore` is false. If a page is empty or its cursor does not advance, stop and investigate rather than repeating the same request indefinitely. ```bash curl --get \ --url 'https://pub-api.leadtruffle.com/api/v2/pub/leads-created' \ --header 'X-API-Key: YOUR_API_KEY' \ --data-urlencode 'limit=100' \ --data-urlencode 'before=2026-01-01T00:00:00.000Z' ``` The date above is an example. Use the timestamp from your previous response. ## Webhook issues Confirm the subscription's URL and event type. If signature verification fails, check that you use the exact raw request body and the webhook's shared secret. Parsing and re-serializing JSON can change the bytes used for signing. See [Receive lead events](/guides/webhooks/) and [Verify webhook signatures](/guides/webhook-signatures/). ## Get help Email [support@leadtruffle.com](mailto:support@leadtruffle.com) with the endpoint, time of the request, HTTP status, and a redacted response. Do not include API keys or webhook secrets. --- # Give your AI agent the docs Use one URL to give an AI agent the guides and complete API contract. These files are generated in the same build as this documentation. ## One document, everything included [Open llms-full.txt](/llms-full.txt) for the full text export: guides, endpoint descriptions, parameters, request and response definitions, examples, and shared schemas. Copy this prompt into your agent: ```text Read https://api-docs.leadtruffle.com/llms-full.txt to learn the LeadTruffle API. Use the documented endpoint schemas and prerequisites. Ask me for the intended company and workflow before making changes. Keep API keys and webhook secrets private. ``` ## Choose the right format | File | Best for | | --- | --- | | [llms.txt](/llms.txt) | A short index to locate relevant documentation | | [llms-full.txt](/llms-full.txt) | A single document with guides and the complete API contract | | [openapi.json](/openapi.json) | Tools that consume OpenAPI, including client generators | For agents with smaller context windows, start with `llms.txt` and load only the guides and operations needed for the task. ## Read before taking action The reference includes endpoints that change data, trigger messages, and register webhook destinations. Preserve the documented approval requirements and account restrictions. Do not treat an API acceptance response as proof that a message was delivered or a downstream workflow finished. Use placeholder credentials in generated code and load real secrets from the application's secure configuration. # Endpoint index - GET /v1/pub/leads — List leads (Lead inquiries) - GET /v1/pub/leads/{uuid} — Get lead by ID (Lead inquiries) - PUT /v1/pub/leads/{uuid} — Update lead status and notes (Lead inquiries) - GET /v1/pub/webhooks — List webhooks (Legacy webhooks) - POST /v1/pub/webhooks/missed-call-complete — Add missed call webhook (Legacy webhooks) - DELETE /v1/pub/webhooks/missed-call-complete — Remove missed call webhook (Legacy webhooks) - POST /v1/pub/webhooks/chat-widget-lead-complete — Add chat widget webhook (Legacy webhooks) - DELETE /v1/pub/webhooks/chat-widget-lead-complete — Remove chat widget webhook (Legacy webhooks) - POST /v1/pub/clients/upsert — Create or update a contact (Contacts) - GET /v1/pub/clients/by-phone/{phone} — Get contact by phone number (Contacts) - GET /v1/pub/leads-lite — List leads without conversation history (Lead inquiries) - GET /v2/pub/webhooks — List V2 webhooks (Webhooks) - POST /v2/pub/webhooks — Create V2 webhook (Webhooks) - PUT /v2/pub/webhooks/{id} — Update V2 webhook (Webhooks) - DELETE /v2/pub/webhooks/{id} — Delete V2 webhook (Webhooks) - GET /v2/pub/leads-created/{uuid} — Get single lead created by ID (V2) (Lead inquiries) - GET /v2/pub/leads-created — List leads created (V2) (Lead inquiries) - GET /v2/pub/leads-completed/{uuid} — Get single completed conversation by ID (V2) (Conversations) - GET /v2/pub/leads-completed — List completed conversations (V2) (Conversations) - GET /v2/pub/message-replies — List recent message replies (V2) (Conversations) - GET /v2/pub/employees — List employees (Team) - PUT /v2/pub/clients/update — Update contact (Contacts) - GET /v2/pub/clients/statuses — List contact pipeline statuses (Contacts) - GET /v2/pub/clients/{clientId}/notes — List contact notes (Contacts) - POST /v2/pub/clients/{clientId}/notes — Add contact note (Contacts) - GET /v1/pub/chat-widget/config — Get chat widget configuration (Widgets) - PUT /v1/pub/chat-widget/config — Update chat widget configuration (Widgets) - POST /v1/pub/chat-widget/trigger-lead-qualifier — Trigger lead qualifier agent (Widgets) - GET /v1/pub/default-calendar/appointments — Get Default Calendar Appointments (Default Calendar) - GET /v1/pub/default-calendar/info — Get Default Calendar Information (Default Calendar) - POST /v1/pub/email-gateway/submit — Submit email gateway message (Email gateway) - POST /v1/pub/yelp-lead-agent/process-incoming-message — Process incoming Yelp message with AI agent (New Consumer Message) (Yelp Lead Agent) - POST /v1/pub/yelp-lead-agent/process-new-lead — Capture Yelp New Lead payload (Yelp Lead Agent) - POST /v1/pub/yelp-lead-agent/process-phone-availability — Capture Yelp Phone Number Available payload (Yelp Lead Agent) - POST /v1/pub/yelp-lead-agent/process-business-message — Process incoming business message from Yelp (Yelp Lead Agent) - POST /v1/pub/yelp-lead-agent/send-message — Send a follow-up message to a Yelp lead (Yelp Lead Agent) - GET /v1/pub/yelp-lead-agent/conversations — List Yelp AI conversations (Yelp Lead Agent) - GET /v1/pub/yelp-lead-agent/conversations/lead/{externalLeadId} — Get Yelp conversation by Yelp lead ID (Yelp Lead Agent) - GET /v1/pub/yelp-lead-agent/outbound-messages — List outbound messages in webhook payload format (Yelp Lead Agent) - PUT /v2/pub/leads/conversion — Upsert (create or update) lead conversion data (Lead inquiries) - PUT /v2/pub/leads/update — Update lead (Lead inquiries) - POST /_EXPERIMENTAL/v2/pub/leads/review-gathering — Trigger review gathering for a lead (Experimental) - GET /v2/pub/inbox — List the contact inbox (Inbox) - GET /v2/pub/clients/{clientId}/conversation — Read a contact conversation (Inbox) - GET /v2/pub/clients/{clientId}/timeline — Read a combined contact timeline (Inbox) - POST /v2/pub/clients/{clientId}/messages — Send an immediate reply to a contact (Messages) - GET /v2/pub/clients/{clientId}/messages — List contact messages (Messages) - GET /v2/pub/clients/{clientId}/messages/{messageId}/body — Read complete stored message text (Messages) - GET /v2/pub/clients/{clientId}/send-methods — Discover contact reply methods (Messages) - GET /v2/pub/clients/{clientId}/calls — List contact calls (Calls) - GET /v2/pub/calls/{callId} — Get call details (Calls) - GET /v2/pub/calls/{callId}/transcript — Read a call transcript (Calls) - GET /v2/pub/calls/{callId}/recordings — Get temporary recording download links (Calls) - GET /v2/pub/clients/{clientId}/bookings — List contact bookings (Bookings) - GET /v2/pub/bookings/{bookingId} — Get booking details (Bookings) - GET /v2/pub/appointment-change-requests — List booking change requests (Bookings) - GET /v2/pub/appointment-change-requests/{requestId} — Get booking change request (Bookings) - GET /v2/pub/account/usage — Get Inbox API usage and limits (Usage) # Complete OpenAPI contract The JSON below includes every operation, authentication scheme, request and response definition, example, and shared schema. Resolve local $ref values against this document. Endpoint descriptions include prerequisites and restrictions; preserve them when using the API. ```json { "openapi": "3.0.3", "info": { "title": "LeadTruffle Public API", "description": "Connect your contacts, lead inquiries, conversations, and workflows to LeadTruffle.\n\n[Make your first request](/guides/quickstart/) · [Set up webhooks](/guides/webhooks/) · [Website widgets](/guides/widgets/)\n\nUse the resource groups to find an endpoint. Each endpoint documents its own API version, parameters, and response format.\n", "version": "1.0.0", "contact": { "name": "LeadTruffle Support", "url": "https://www.leadtruffle.co", "email": "support@leadtruffle.com" } }, "servers": [ { "url": "https://pub-api.leadtruffle.com/api", "description": "Production API" } ], "security": [ { "ApiKeyAuth": [] } ], "tags": [ { "name": "Contacts", "description": "Create and update contacts, manage notes, and look up pipeline statuses." }, { "name": "Lead inquiries", "description": "Retrieve lead inquiries and update their status and conversion data." }, { "name": "Inbox", "description": "Find active contacts, inspect AI and escalation state, and read a combined timeline." }, { "name": "Messages", "description": "Read ongoing contact messages, retrieve full text, and discover eligible reply channels." }, { "name": "Calls", "description": "Read contact call summaries, details, transcripts, and temporary recording links." }, { "name": "Bookings", "description": "Find upcoming or recent AI bookings and inspect customer cancellation or rescheduling requests." }, { "name": "Usage", "description": "Check lead usage, separate SMS and email allowances, and API sending eligibility." }, { "name": "Conversations", "description": "Read completed lead qualification conversations and legacy reply records. Use Messages for ongoing contact history." }, { "name": "Webhooks", "description": "Subscribe to events. Use V2 webhooks for new integrations." }, { "name": "Default Calendar", "description": "Read your default calendar and appointments." }, { "name": "Widgets", "description": "Configure website widgets and access the approved lead qualifier workflow." }, { "name": "Email gateway", "description": "Submit email messages to the lead capture gateway." }, { "name": "Yelp Lead Agent", "description": "Connect Yelp lead messages and conversations." }, { "name": "Team", "description": "Look up employees in your company." }, { "name": "Experimental", "description": "Experimental operations. Review endpoint requirements before using them." }, { "name": "Legacy webhooks", "description": "V1 webhook operations for existing integrations. Use V2 for new integrations." } ], "paths": { "/v1/pub/leads": { "get": { "tags": [ "Lead inquiries" ], "summary": "List leads", "description": "Retrieve a paginated list of leads for your company", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "before", "schema": { "type": "string", "format": "date-time" }, "description": "Pagination cursor - get leads created before this timestamp (ISO8601). Defaults to current time if not provided.", "example": "2024-01-01T00:00:00Z" }, { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 10 }, "description": "Maximum number of leads to return (max 10)" } ], "responses": { "200": { "description": "Successfully retrieved leads", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadList" } } } }, "401": { "description": "Authentication error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/pub/leads/{uuid}": { "get": { "tags": [ "Lead inquiries" ], "summary": "Get lead by ID", "description": "Retrieve a specific lead by its UUID", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "uuid", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The UUID of the lead to retrieve" } ], "responses": { "200": { "description": "Successfully retrieved lead", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/WebhookPayload" } } } } } }, "401": { "description": "Authentication error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "put": { "tags": [ "Lead inquiries" ], "summary": "Update lead status and notes", "description": "Update the conversion status and notes for a specific lead.\n\nThis endpoint allows you to track the progress of leads through your sales funnel\nby updating their conversion status and adding notes for internal tracking.\n\n**Conversion Status Values:**\n- **NEW**: Just qualified (default status)\n- **CONTACTED**: Sales team has reached out to the lead\n- **QUOTED**: Quote or estimate has been provided\n- **WON**: Lead converted to customer/sale closed\n- **LOST**: Lead did not convert/sale was lost\n- **NURTURING**: Lead is in long-term follow-up process\n- **CLOSED**: Lead is closed and no longer being pursued\n\nThe status update timestamp is automatically recorded for tracking purposes.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "uuid", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The UUID of the lead to update", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "conversionStatus" ], "properties": { "conversionStatus": { "type": "string", "enum": [ "NEW", "CONTACTED", "QUOTED", "WON", "LOST", "NURTURING", "CLOSED" ], "description": "The new conversion status for the lead", "example": "CONTACTED" }, "notes": { "type": "string", "maxLength": 1000, "nullable": true, "description": "Optional notes about the lead status update", "example": "Called customer, interested in scheduling estimate for next week" } } }, "examples": { "status_update": { "summary": "Update status only", "value": { "conversionStatus": "QUOTED" } }, "status_with_notes": { "summary": "Update status and add notes", "value": { "conversionStatus": "CONTACTED", "notes": "Left voicemail, customer will call back tomorrow" } }, "won_conversion": { "summary": "Mark as won conversion", "value": { "conversionStatus": "WON", "notes": "Customer signed contract for $5,000 HVAC installation. Project starts next month." } }, "lost_conversion": { "summary": "Mark as lost", "value": { "conversionStatus": "LOST", "notes": "Customer decided to go with competitor due to pricing" } } } } } }, "responses": { "200": { "description": "Lead status successfully updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" }, "conversionStatus": { "type": "string", "enum": [ "NEW", "CONTACTED", "QUOTED", "WON", "LOST", "NURTURING", "CLOSED" ], "example": "CONTACTED" }, "conversionStatusUpdatedAt": { "type": "string", "format": "date-time", "example": "2024-01-15T14:30:00Z", "description": "Timestamp when the status was last updated" }, "leadNotes": { "type": "string", "nullable": true, "example": "Called customer, interested in scheduling estimate for next week", "description": "Current notes for the lead" } } } } }, "examples": { "success_response": { "summary": "Successful status update", "value": { "success": true, "data": { "id": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "QUOTED", "conversionStatusUpdatedAt": "2024-01-15T14:30:00Z", "leadNotes": "Provided estimate for $3,500. Customer reviewing with spouse." } } } } } } }, "400": { "description": "Invalid request data", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "validStatuses": { "type": "array", "items": { "type": "string" }, "description": "List of valid conversion status values" } } } ] }, "examples": { "invalid_status": { "summary": "Invalid conversion status", "value": { "success": false, "error": "Valid conversion status is required", "validStatuses": [ "NEW", "CONTACTED", "QUOTED", "WON", "LOST", "NURTURING", "CLOSED" ] } }, "notes_too_long": { "summary": "Notes too long", "value": { "success": false, "error": "Notes must be at most 1000 characters" } }, "invalid_notes_type": { "summary": "Invalid notes type", "value": { "success": false, "error": "Notes must be a string" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Lead not found" } } } }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/webhooks": { "get": { "tags": [ "Legacy webhooks" ], "summary": "List webhooks", "description": "Retrieve all configured webhooks for both chat widget and missed call lead completion", "security": [ { "ApiKeyAuth": [] } ], "responses": { "200": { "description": "Successfully retrieved webhooks", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookList" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/webhooks/missed-call-complete": { "post": { "tags": [ "Legacy webhooks" ], "summary": "Add missed call webhook", "description": "Add a new webhook URL for missed call lead completion notifications.\n\nWhen a lead conversation is completed, we will POST the payload described below to your webhook URL.\nThe webhook will timeout after 10 seconds and we will retry failed deliveries up to 3 times with exponential backoff.\n\n### Webhook Payload Example\n```json\n{\n \"type\": \"conversation_completed\",\n \"clientId\": \"456\",\n \"leadId\": \"123\",\n \"companyId\": \"789\",\n \"leadQualificationStatus\": \"COMPLETED\",\n \"leadInformation\": {\n \"name\": \"John Doe\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"+18001234567\",\n \"additionalData\": {\n \"source\": \"popup\",\n \"message\": \"I need help with...\"\n }\n },\n \"trackingData\": {\n \"source\": \"popup\",\n \"utm_source\": \"google\",\n \"utm_medium\": \"cpc\",\n \"utm_campaign\": \"home_renovation\",\n \"pageInfo\": {\n \"title\": \"Home - Best HVAC Services\",\n \"referrer\": \"https://www.google.com/\",\n \"currentUrl\": \"http://localhost:3002/index.html\"\n }\n },\n \"qualifyingData\": {\n \"example_budget\": \"$5000\",\n \"example_timeline\": \"Within 3 months\",\n \"example_projectType\": \"Home Renovation\"\n },\n \"commonFields\": {\n \"fullAddress\": \"111 main st, Austin TX 73301\",\n \"address\": \"111 main st\",\n \"zipcode\": \"73301\",\n \"state\": \"TX\",\n \"city\": \"Austin\",\n \"country\": \"US\",\n \"isHomeowner\": true,\n \"customerName\": \"John\"\n },\n \"qualifyingDataSummary\": \"The client lives in a 3 bedroom house.\\nHas a budget of $2000.\\nzipcode is 45150.\",\n \"contactReason\": \"Client is interested in a home renovation project...\",\n \"timestamp\": \"2024-01-01T00:00:00Z\",\n \"messageHistory\": [\n {\n \"direction\": \"outbound\",\n \"name\": \"AI Agent\",\n \"message\": \"How can we help you...\",\n \"date\": \"2024-01-01T00:00:00Z\"\n },\n {\n \"direction\": \"inbound\",\n \"name\": \"John Doe\",\n \"message\": \"I need help with...\",\n \"date\": \"2024-01-01T00:01:00Z\"\n }\n ],\n \"userMedia\": [\n {\n \"type\": \"image/jpeg\",\n \"url\": \"https://tooldesk-public-user-uploads.s3.us-west-2.amazonaws.com/email-assets/leadtruffle-Wordmark-white.png\"\n }\n ]\n}\n```\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "example": "https://api.example.com/webhooks/leads" } } } } } }, "responses": { "200": { "description": "Webhook successfully added", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookResponse" } } } }, "400": { "description": "Invalid request or webhook limit reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "$ref": "#/components/responses/NotFoundError" }, "500": { "$ref": "#/components/responses/InternalError" } } }, "delete": { "tags": [ "Legacy webhooks" ], "summary": "Remove missed call webhook", "description": "Remove an existing webhook URL for missed call lead completion notifications", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "example": "https://api.example.com/webhooks/leads" } } } } } }, "responses": { "200": { "description": "Webhook successfully removed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookResponse" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "$ref": "#/components/responses/NotFoundError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/webhooks/chat-widget-lead-complete": { "post": { "tags": [ "Legacy webhooks" ], "summary": "Add chat widget webhook", "description": "Add a new webhook URL for chat widget lead completion notifications.\n\nWhen a lead conversation is completed, we will POST the payload described below to your webhook URL.\nThe webhook will timeout after 10 seconds and we will retry failed deliveries up to 3 times with exponential backoff.\n\n### Webhook Payload Example\n```json\n{\n \"type\": \"conversation_completed\",\n \"clientId\": \"456\",\n \"leadId\": \"123\",\n \"companyId\": \"789\",\n \"leadQualificationStatus\": \"COMPLETED\",\n \"leadInformation\": {\n \"name\": \"John Doe\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"+18001234567\",\n \"additionalData\": {\n \"source\": \"popup\",\n \"message\": \"I need help with...\"\n }\n },\n \"trackingData\": {\n \"source\": \"popup\",\n \"utm_source\": \"google\",\n \"utm_medium\": \"cpc\",\n \"utm_campaign\": \"home_renovation\",\n \"pageInfo\": {\n \"title\": \"Home - Best HVAC Services\",\n \"referrer\": \"https://www.google.com/\",\n \"currentUrl\": \"http://localhost:3002/index.html\"\n }\n },\n \"qualifyingData\": {\n \"example_budget\": \"$5000\",\n \"example_timeline\": \"Within 3 months\",\n \"example_projectType\": \"Home Renovation\"\n },\n \"commonFields\": {\n \"fullAddress\": \"111 main st, Austin TX 73301\",\n \"address\": \"111 main st\",\n \"zipcode\": \"73301\",\n \"state\": \"TX\",\n \"city\": \"Austin\",\n \"country\": \"US\",\n \"isHomeowner\": true,\n \"customerName\": \"John\"\n },\n \"qualifyingDataSummary\": \"The client lives in a 3 bedroom house.\\nHas a budget of $2000.\\nzipcode is 45150.\",\n \"contactReason\": \"Client is interested in a home renovation project...\",\n \"timestamp\": \"2024-01-01T00:00:00Z\",\n \"messageHistory\": [\n {\n \"direction\": \"outbound\",\n \"name\": \"AI Agent\",\n \"message\": \"How can we help you...\",\n \"date\": \"2024-01-01T00:00:00Z\"\n },\n {\n \"direction\": \"inbound\",\n \"name\": \"John Doe\",\n \"message\": \"I need help with...\",\n \"date\": \"2024-01-01T00:01:00Z\"\n }\n ],\n \"userMedia\": [\n {\n \"type\": \"image/jpeg\",\n \"url\": \"https://tooldesk-public-user-uploads.s3.us-west-2.amazonaws.com/email-assets/leadtruffle-Wordmark-white.png\"\n }\n ]\n}\n```\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "example": "https://api.example.com/webhooks/leads" } } } } } }, "responses": { "200": { "description": "Webhook successfully added", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookResponse" } } } }, "400": { "description": "Invalid request or webhook limit reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "$ref": "#/components/responses/NotFoundError" }, "500": { "$ref": "#/components/responses/InternalError" } } }, "delete": { "tags": [ "Legacy webhooks" ], "summary": "Remove chat widget webhook", "description": "Remove an existing webhook URL for chat widget lead completion notifications", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "url" ], "properties": { "url": { "type": "string", "format": "uri", "example": "https://api.example.com/webhooks/leads" } } } } } }, "responses": { "200": { "description": "Webhook successfully removed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookResponse" } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "$ref": "#/components/responses/NotFoundError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/clients/upsert": { "post": { "tags": [ "Contacts" ], "summary": "Create or update a contact", "description": "Creates a new client record or updates an existing one based on the phone number.\n\nThe phone number is used as the unique identifier for finding existing clients. Phone numbers\nare normalized to E.164 format internally (+1XXXXXXXXXX for US numbers).\n\nWhen updating existing clients:\n- Only non-null fields in the request are updated\n- Existing data is preserved for fields not included in the request\n- The phone number cannot be changed once a client is created\n- Operations are restricted to the authenticated company's data\n\n### Notes\n- Phone numbers must be valid US numbers\n- All fields except phone are optional\n- Email addresses must be valid format\n- New clients are automatically marked as leads with status 'NEW'\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClientUpsertRequest" } } } }, "responses": { "200": { "description": "Client successfully created or updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ClientUpsertResponse" }, "examples": { "created": { "summary": "New client created", "value": { "success": true, "data": { "id": "415f2b29-39e6-4182-9d6f-ec2d817f01c2", "companyId": "f096f9e3-001d-49ac-864c-3d73453bbe08", "primaryPhone": "+18001234567", "firstName": "John", "lastName": "Doe", "primaryEmail": "john.doe@example.com", "address": "123 Main St", "address2": "Unit 456", "city": "Austin", "state": "TX", "zipCode": "78701", "country": "US", "isLead": true, "leadStatus": "NEW", "createdAt": "2023-06-01T00:00:00Z", "updatedAt": "2023-06-01T00:00:00Z" }, "action": "CREATED" } }, "updated": { "summary": "Existing client updated", "value": { "success": true, "data": { "id": "415f2b29-39e6-4182-9d6f-ec2d817f01c2", "companyId": "f096f9e3-001d-49ac-864c-3d73453bbe08", "primaryPhone": "+18001234567", "firstName": "John", "lastName": "Smith", "primaryEmail": "john.smith@example.com", "address": "456 Oak St", "address2": null, "city": "Austin", "state": "TX", "zipCode": "78701", "country": "US", "isLead": true, "leadStatus": "NEW", "createdAt": "2023-06-01T00:00:00Z", "updatedAt": "2023-06-02T00:00:00Z" }, "action": "UPDATED" } }, "unchanged": { "summary": "No changes needed", "value": { "success": true, "data": { "id": "415f2b29-39e6-4182-9d6f-ec2d817f01c2", "companyId": "f096f9e3-001d-49ac-864c-3d73453bbe08", "primaryPhone": "+18001234567", "firstName": "John", "lastName": "Smith", "primaryEmail": "john.smith@example.com", "address": "456 Oak St", "address2": null, "city": "Austin", "state": "TX", "zipCode": "78701", "country": "US", "isLead": true, "leadStatus": "NEW", "createdAt": "2023-06-01T00:00:00Z", "updatedAt": "2023-06-01T00:00:00Z" }, "action": "UNCHANGED" } } } } } }, "400": { "description": "Invalid request data", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "details": { "type": "object", "description": "Validation error details" } } } ] }, "example": { "success": false, "error": "Invalid input", "details": { "phone": { "_errors": [ "Phone number must be a valid US phone number in E.164 or national format" ] } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/clients/by-phone/{phone}": { "get": { "tags": [ "Contacts" ], "summary": "Get contact by phone number", "description": "Retrieves a client record by their phone number.\n\nThe phone number should be in E.164 format (+1XXXXXXXXXX) or a standard US format.\nThe system will normalize the phone number to E.164 format before searching.\n\nReturns a 404 error if no client with the specified phone number exists for the authenticated company.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "phone", "required": true, "schema": { "type": "string" }, "description": "Phone number in E.164 format or US national format (e.g., +18001234567 or 8001234567)", "example": "8001234567" } ], "responses": { "200": { "description": "Client successfully retrieved", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/Client" } } } } } }, "400": { "description": "Invalid phone number format", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "details": { "type": "object", "description": "Validation error details" } } } ] }, "example": { "success": false, "error": "Invalid phone number format", "details": { "phone": { "_errors": [ "Phone number must be a valid US phone number in E.164 or national format" ] } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Client not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Client not found" } } } }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/leads-lite": { "get": { "tags": [ "Lead inquiries" ], "summary": "List leads without conversation history", "description": "Retrieve a paginated list of leads for your company without full conversation history.\nThis endpoint is optimized for performance and can return up to 100 records at once.\n\nUse this endpoint when you need to fetch larger batches of leads and don't require\nthe full message history for each lead.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "before", "schema": { "type": "string", "format": "date-time" }, "description": "Pagination cursor - get leads created before this timestamp (ISO8601). Defaults to current time if not provided.", "example": "2024-01-01T00:00:00Z" }, { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }, "description": "Maximum number of leads to return (max 100)" } ], "responses": { "200": { "description": "Successfully retrieved leads", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "leads": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookPayload" }, "description": "List of leads, limited to a maximum of 100 per request. These leads will have empty messageHistory arrays." }, "hasMore": { "type": "boolean", "description": "Indicates if there are more results available. To fetch the next page, use the oldest lead's timestamp as the 'before' parameter." } } } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/webhooks": { "get": { "tags": [ "Webhooks" ], "summary": "List V2 webhooks", "description": "Retrieve all configured V2 webhooks for your company.\n\nV2 webhooks provide improved payload structure and support for multiple event types:\n- **LEAD_CREATED**: Fires instantly when a new lead is created\n- **CONVERSATION_COMPLETED**: Fires when AI qualification finishes with full data\n- **MESSAGE_REPLY**: Fires when leads reply via SMS\n- **LEAD_STATUS_CHANGED**: Legacy lead pipeline event. Use only if your account still uses the legacy Leads system.\n- **CLIENT_STATUS_CHANGED**: Fires when a contact Status changes in the new Contacts/client pipeline\n- **NEW_APPOINTMENT**: Fires when a calendar booking is created\n- **YELP_MESSAGE_OUTBOUND**: Fires to send messages to Yelp via Zapier\n\nEach webhook includes tracking information such as delivery success/failure rates and timestamps.\n\nSee the **POST** endpoint documentation below for full payload examples for each event type.\n", "security": [ { "ApiKeyAuth": [] } ], "responses": { "200": { "description": "Successfully retrieved webhooks", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookV2ListResponse" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } }, "post": { "tags": [ "Webhooks" ], "summary": "Create V2 webhook", "description": "Create a new V2 webhook for your company.\n\n**Headers sent with every webhook:**\n- `Content-Type: application/json`\n- `x-origin: leadtruffle`\n- `x-event-type: [EVENT_TYPE]`\n\n**Signature headers when `secret` is set:**\n- `x-leadtruffle-timestamp`: Unix timestamp in seconds\n- `x-leadtruffle-signature`: `v1=` followed by the hex HMAC SHA-256 signature\n- `x-leadtruffle-signature-algorithm`: `hmac-sha256`\n\nTo verify a signed webhook, compute `HMAC_SHA256(secret, timestamp + \".\" + rawRequestBody)` and compare it to the value after `v1=` in `x-leadtruffle-signature`. Use the raw body exactly as received, before JSON parsing.\n\nPython example:\n```python\nimport hmac, hashlib, time\n\nraw_body = request.get_data()\ntimestamp = request.headers[\"x-leadtruffle-timestamp\"]\nsignature = request.headers[\"x-leadtruffle-signature\"].removeprefix(\"v1=\")\n\ntry:\n timestamp_seconds = int(timestamp)\nexcept ValueError:\n raise Exception(\"invalid timestamp\")\n\nif abs(time.time() - timestamp_seconds) > 5 * 60:\n raise Exception(\"stale webhook\")\n\nexpected = hmac.new(\n b\"YOUR_WEBHOOK_SECRET\",\n timestamp.encode(\"utf-8\") + b\".\" + raw_body,\n hashlib.sha256,\n).hexdigest()\n\nif not hmac.compare_digest(expected, signature):\n raise Exception(\"invalid signature\")\n```\n\nPHP example:\n```php\n$rawBody = file_get_contents('php://input');\n$timestamp = $_SERVER['HTTP_X_LEADTRUFFLE_TIMESTAMP'];\n$signature = preg_replace('/^v1=/', '', $_SERVER['HTTP_X_LEADTRUFFLE_SIGNATURE']);\n\nif (!ctype_digit($timestamp) || abs(time() - intval($timestamp)) > 5 * 60) {\n http_response_code(401);\n exit;\n}\n\n$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, 'YOUR_WEBHOOK_SECRET');\n\nif (!hash_equals($expected, $signature)) {\n http_response_code(401);\n exit;\n}\n```\n\n**Delivery & Retry:**\n- Webhooks timeout after 10 seconds\n- Failed deliveries are retried with exponential backoff\n- Maximum of 6 webhooks per company\n\n---\n\n## Event Types & Payload Examples\n\n### LEAD_CREATED\nFires instantly when a new lead is created. The fastest way to get notified.\n\n```json\n{\n \"eventType\": \"LEAD_CREATED\",\n \"eventTypeDetails\": \"NEW_CHAT_WIDGET_SUBMISSION\",\n \"leadId\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"clientId\": \"client_11111111-2222-3333-4444-555555555555\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"name\": \"John Doe\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"+15551234567\",\n \"message\": \"I need help with HVAC repair\",\n \"timestamp\": \"2024-01-15T14:30:00.000Z\",\n \"isRepeatLead\": false,\n \"qualificationSource\": \"WEBCHAT\",\n \"isManualTakeoverEnabled\": false,\n \"trackingData\": {\n \"source\": \"google_ads\",\n \"utm_source\": \"google\",\n \"utm_medium\": \"cpc\",\n \"gclid\": \"Cj0KCQjw...\"\n }\n}\n```\n\n### CONVERSATION_COMPLETED\nFires when AI qualification finishes. Contains full conversation history and extracted data.\n\n```json\n{\n \"eventType\": \"CONVERSATION_COMPLETED\",\n \"eventTypeDetails\": \"CONVERSATION_COMPLETE_CHAT_WIDGET\",\n \"qualificationSource\": \"WEBCHAT\",\n \"leadId\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"clientId\": \"client_11111111-2222-3333-4444-555555555555\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"leadInformation\": {\n \"name\": \"John Doe\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"+15551234567\"\n },\n \"qualifyingData\": {\n \"contactReason\": \"HVAC repair needed\",\n \"serviceType\": \"Air Conditioning Repair\",\n \"timeframe\": \"ASAP\"\n },\n \"qualifyingDataSummary\": \"Customer needs AC repair ASAP. Unit not cooling.\",\n \"contactReason\": \"HVAC repair needed\",\n \"timestamp\": \"2024-01-15T14:45:00.000Z\",\n \"messageHistory\": [\n {\"name\": \"John\", \"message\": \"Hi, my AC stopped working\", \"direction\": \"inbound\"},\n {\"name\": \"AI\", \"message\": \"I can help! What seems to be the issue?\", \"direction\": \"outbound\"}\n ],\n \"commonFields\": {\n \"address\": \"123 Main St\",\n \"city\": \"Springfield\",\n \"state\": \"IL\",\n \"zipcode\": \"62701\"\n }\n}\n```\n\n### MESSAGE_REPLY\nFires when a lead replies via SMS. Useful for Slack notifications.\n\n```json\n{\n \"eventType\": \"MESSAGE_REPLY\",\n \"eventTypeDetails\": \"NEW_MESSAGE_REPLY\",\n \"messageId\": \"msg_12345678-abcd-1234-5678-123456789abc\",\n \"leadId\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"clientId\": \"client_11111111-2222-3333-4444-555555555555\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"from\": \"+15551234567\",\n \"to\": \"+15559876543\",\n \"body\": \"Yes, I'm still interested in getting a quote!\",\n \"mediaUrl\": null,\n \"timestamp\": \"2024-01-15T15:00:00.000Z\",\n \"leadInformation\": {\n \"name\": \"John Doe\",\n \"phone\": \"+15551234567\",\n \"email\": \"john@example.com\"\n }\n}\n```\n\n### LEAD_STATUS_CHANGED\n**Legacy lead pipeline event.** Use this only if your account is still using\nthe legacy Leads system and you need status updates from individual\n`lead_form_submissions` records.\n\nIf your account is using the new Contacts/client pipeline, use\n`CLIENT_STATUS_CHANGED` instead. `LEAD_STATUS_CHANGED` does not represent\nthe contact-level pipeline status in the new system.\n\n```json\n{\n \"eventType\": \"LEAD_STATUS_CHANGED\",\n \"eventTypeDetails\": \"LEAD_STATUS_CHANGED\",\n \"leadId\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"clientId\": \"client_11111111-2222-3333-4444-555555555555\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"previousStatus\": \"NEW\",\n \"newStatus\": \"WON\",\n \"timestamp\": \"2024-01-16T10:00:00.000Z\",\n \"changedByUserId\": \"user_11111111-2222-3333-4444-555555555555\",\n \"changedByUserEmail\": \"team@example-hvac.com\",\n \"lead\": {\n \"id\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"clientId\": \"client_11111111-2222-3333-4444-555555555555\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"email\": \"john@example.com\",\n \"phone\": \"+15551234567\",\n \"leadFormData\": {\n \"source\": \"google_ads\",\n \"serviceNeeded\": \"HVAC repair\"\n },\n \"conversionStatus\": \"WON\",\n \"conversionStatusUpdatedAt\": \"2024-01-16T10:00:00.000Z\"\n }\n}\n```\n\n### CLIENT_STATUS_CHANGED\nFires when a contact Status is changed in the new Contacts/client pipeline.\nUse this event for accounts using the new contact-centric pipeline.\n\n```json\n{\n \"eventType\": \"CLIENT_STATUS_CHANGED\",\n \"eventTypeDetails\": \"CLIENT_STATUS_CHANGED\",\n \"clientId\": \"client_11111111-2222-3333-4444-555555555555\",\n \"latestLeadSubmissionId\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"previousStatus\": \"NEW\",\n \"newStatus\": \"WON\",\n \"timestamp\": \"2024-01-16T10:00:00.000Z\",\n \"changedByUserId\": \"user_11111111-2222-3333-4444-555555555555\",\n \"changedByUserEmail\": \"team@example-hvac.com\",\n \"client\": {\n \"id\": \"client_11111111-2222-3333-4444-555555555555\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"primaryEmail\": \"john@example.com\",\n \"primaryPhone\": \"+15551234567\",\n \"leadStatus\": \"WON\",\n \"latestLeadSubmissionId\": \"lead_87654321-dcba-4321-8765-987654321def\",\n \"leadStatusUpdatedAt\": \"2024-01-16T10:00:00.000Z\"\n }\n}\n```\n\n### NEW_APPOINTMENT\nFires when a calendar booking is created.\n\n```json\n{\n \"eventType\": \"NEW_APPOINTMENT\",\n \"eventTypeDetails\": \"NEW_APPOINTMENT\",\n \"appointmentId\": \"apt_123e4567-e89b-12d3-a456-426614174000\",\n \"companyId\": \"comp_123e4567-e89b-12d3-a456-426614174000\",\n \"clientId\": \"client_123e4567-e89b-12d3-a456-426614174000\",\n \"appointmentDetails\": {\n \"title\": \"Service Appointment\",\n \"startAt\": \"2024-01-20T14:00:00.000Z\",\n \"endAt\": \"2024-01-20T15:00:00.000Z\",\n \"status\": \"BOOKED\"\n },\n \"clientInformation\": {\n \"firstName\": \"John\",\n \"lastName\": \"Doe\",\n \"primaryEmail\": \"john@example.com\",\n \"primaryPhone\": \"+15551234567\",\n \"address\": \"123 Main St\"\n }\n}\n```\n\n### YELP_MESSAGE_OUTBOUND\nFires when sending a message to a Yelp lead via Zapier.\n\n**Setup:** Register a Zapier \"Webhooks by Zapier\" trigger, then connect to Yelp's \"Create Message\" action.\n\n**Trigger via:** `POST /v1/pub/yelp-lead-agent/send-message` or the LeadTruffle UI.\n\n```json\n{\n \"eventType\": \"YELP_MESSAGE_OUTBOUND\",\n \"eventTypeDetails\": \"YELP_FOLLOWUP_MESSAGE\",\n \"yelpLeadId\": \"yelp_lead_123456789\",\n \"yelpBusinessId\": \"yelp_biz_987654321\",\n \"conversationId\": \"conv_123e4567-e89b-12d3-a456-426614174000\",\n \"companyId\": \"comp_99999999-8888-7777-6666-444444444444\",\n \"message\": \"Hi! Thanks for reaching out. Are you still interested in a quote?\",\n \"attachmentUrls\": [],\n \"timestamp\": \"2024-01-16T10:00:00.000Z\",\n \"triggeredBy\": \"API\",\n \"leadInformation\": {\n \"name\": \"John Doe\",\n \"phone\": \"+15551234567\",\n \"email\": \"john@example.com\"\n }\n}\n```\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookV2CreateRequest" }, "examples": { "lead_created": { "summary": "Lead Created Webhook", "value": { "eventType": "LEAD_CREATED", "url": "https://api.example.com/webhooks/leads", "secret": "whsec_your_shared_secret" } }, "lead_status_changed": { "summary": "Legacy Lead Status Changed Webhook", "value": { "eventType": "LEAD_STATUS_CHANGED", "url": "https://api.example.com/webhooks/lead-status" } }, "client_status_changed": { "summary": "Contact Status Changed Webhook", "value": { "eventType": "CLIENT_STATUS_CHANGED", "url": "https://api.example.com/webhooks/contact-status" } }, "conversation_completed": { "summary": "Conversation Completed Webhook", "value": { "eventType": "CONVERSATION_COMPLETED", "url": "https://api.example.com/webhooks/completed" } }, "message_reply": { "summary": "Message Reply Webhook", "value": { "eventType": "MESSAGE_REPLY", "url": "https://api.example.com/webhooks/messages" } }, "new_appointment": { "summary": "New Appointment Webhook", "value": { "eventType": "NEW_APPOINTMENT", "url": "https://api.example.com/webhooks/appointments" } }, "yelp_message_outbound": { "summary": "Yelp Message Outbound (for Zapier)", "value": { "eventType": "YELP_MESSAGE_OUTBOUND", "url": "https://hooks.zapier.com/hooks/catch/123456/abcdef/" } } } } } }, "responses": { "201": { "description": "Webhook successfully created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookV2Response" } } } }, "400": { "description": "Invalid request or webhook limit reached", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "validation_error": { "summary": "Validation Error", "value": { "success": false, "error": "Invalid event type. Must be one of: CONVERSATION_COMPLETED, MESSAGE_REPLY, LEAD_CREATED, LEAD_STATUS_CHANGED, CLIENT_STATUS_CHANGED, NEW_APPOINTMENT, YELP_MESSAGE_OUTBOUND" } }, "limit_reached": { "summary": "Webhook Limit", "value": { "success": false, "error": "Maximum of 6 webhooks allowed" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/webhooks/{id}": { "put": { "tags": [ "Webhooks" ], "summary": "Update V2 webhook", "description": "Update an existing V2 webhook by ID.\n\nYou can partially update webhooks by including only the fields you want to change.\nAll fields are optional in the update request.\n\n**Special fields:**\n- `enabled`: Set to false to disable the webhook without deleting it\n- `clearErrors`: Set to true to reset the failed delivery counter\n- `secret`: Set a shared signing secret, or send `null`/an empty value to remove signing\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The UUID of the webhook to update", "example": "550e8400-e29b-41d4-a716-446655440000" } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookV2UpdateRequest" }, "examples": { "update_url": { "summary": "Update webhook URL", "value": { "url": "https://api.example.com/webhooks/new-endpoint" } }, "disable_webhook": { "summary": "Disable webhook", "value": { "enabled": false } }, "clear_errors": { "summary": "Clear error history", "value": { "clearErrors": true } }, "full_update": { "summary": "Full update", "value": { "eventType": "MESSAGE_REPLY", "url": "https://api.example.com/webhooks/messages", "enabled": true, "clearErrors": true } } } } } }, "responses": { "200": { "description": "Webhook successfully updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebhookV2Response" } } } }, "400": { "description": "Invalid request data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Webhook not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Webhook not found" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } }, "delete": { "tags": [ "Webhooks" ], "summary": "Delete V2 webhook", "description": "Delete an existing V2 webhook by ID.\n\nThis action is permanent and cannot be undone. The webhook will immediately stop\nreceiving events and all associated delivery history will be preserved for audit purposes.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "id", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The UUID of the webhook to delete", "example": "550e8400-e29b-41d4-a716-446655440000" } ], "responses": { "200": { "description": "Webhook successfully deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "message": { "type": "string", "example": "Webhook deleted successfully" } } } } } }, "400": { "description": "Invalid webhook ID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Webhook not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Webhook not found" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/leads-created/{uuid}": { "get": { "tags": [ "Lead inquiries" ], "summary": "Get single lead created by ID (V2)", "description": "Retrieve a specific lead in the \"lead created\" format by its UUID.\n\nThis endpoint returns the same payload format that is sent to LEAD_CREATED webhooks.\nDesigned specifically for Zapier integration requirements.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "uuid", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The UUID of the lead to retrieve", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" } ], "responses": { "200": { "description": "Successfully retrieved lead", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/WebhookV2PayloadLeadCreated" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Lead not found" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/leads-created": { "get": { "tags": [ "Lead inquiries" ], "summary": "List leads created (V2)", "description": "Retrieve a paginated list of leads in the \"lead created\" format.\n\nThis endpoint returns the same payload format that is sent to LEAD_CREATED webhooks.\nReturns up to 100 records per request. Designed specifically for Zapier integration requirements.\n\nThis is the abbreviated format that gets delivered when we first receive a lead,\nbefore the conversation is completed.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "before", "schema": { "type": "string", "format": "date-time" }, "description": "Pagination cursor - get leads created before this timestamp (ISO8601). Defaults to current time if not provided.", "example": "2024-01-01T00:00:00Z" }, { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }, "description": "Maximum number of leads to return (max 100)" } ], "responses": { "200": { "description": "Successfully retrieved leads", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "leads": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookV2PayloadLeadCreated" }, "description": "List of leads in the \"lead created\" format, limited to a maximum of 100 per request" }, "hasMore": { "type": "boolean", "description": "Indicates if there are more results available. To fetch the next page, use the oldest lead's timestamp as the 'before' parameter." } } } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/leads-completed/{uuid}": { "get": { "tags": [ "Conversations" ], "summary": "Get single completed conversation by ID (V2)", "description": "Retrieve a specific lead in the \"conversation completed\" format by its UUID.\n\nThis endpoint returns the same payload format that is sent to CONVERSATION_COMPLETED webhooks.\nDesigned specifically for Zapier integration requirements.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "uuid", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The UUID of the lead to retrieve", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" } ], "responses": { "200": { "description": "Successfully retrieved completed conversation", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/WebhookV2PayloadConversationCompleted" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Lead not found" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/leads-completed": { "get": { "tags": [ "Conversations" ], "summary": "List completed conversations (V2)", "description": "Retrieve a paginated list of leads in the \"conversation completed\" format.\n\nThis endpoint returns the same payload format that is sent to CONVERSATION_COMPLETED webhooks.\nReturns up to 10 records per request due to the larger payload size (includes full message history).\nDesigned specifically for Zapier integration requirements.\n\nThis is what gets delivered to the conversation complete webhook - the same lead data but\nwith full message history and completed qualification information.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "before", "schema": { "type": "string", "format": "date-time" }, "description": "Pagination cursor - get leads created before this timestamp (ISO8601). Defaults to current time if not provided.", "example": "2024-01-01T00:00:00Z" }, { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 10 }, "description": "Maximum number of leads to return (max 10)" } ], "responses": { "200": { "description": "Successfully retrieved completed conversations", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "leads": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookV2PayloadConversationCompleted" }, "description": "List of leads in the \"conversation completed\" format, limited to a maximum of 10 per request" }, "hasMore": { "type": "boolean", "description": "Indicates if there are more results available. To fetch the next page, use the oldest lead's timestamp as the 'before' parameter." } } } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/message-replies": { "get": { "tags": [ "Conversations" ], "summary": "List recent message replies (V2)", "description": "Retrieve the latest inbound SMS replies (from leads/customers) in the exact same format that the MESSAGE_REPLY webhook delivers.\n\nReturns up to 10 entries per request and is designed to power Zapier \"Test trigger\" flows that need live-looking data.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "before", "schema": { "type": "string", "format": "date-time" }, "description": "Pagination cursor – fetch message replies created before this timestamp (ISO8601). When omitted, the newest replies are returned.", "example": "2024-01-15T00:00:00Z" }, { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 10, "default": 10 }, "description": "Maximum number of message replies to return (capped at 10)." } ], "responses": { "200": { "description": "Successfully retrieved message replies", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "messageReplies": { "type": "array", "description": "List of inbound SMS replies formatted exactly like MESSAGE_REPLY webhook payloads.", "items": { "$ref": "#/components/schemas/WebhookV2PayloadMessageReply" } }, "hasMore": { "type": "boolean", "description": "Indicates if more replies exist beyond this page." }, "nextBeforeCursor": { "type": "string", "format": "date-time", "nullable": true, "description": "Pass this timestamp to the `before` query parameter to fetch the next (older) page of replies." } } } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/employees": { "get": { "tags": [ "Team" ], "summary": "List employees", "description": "Returns the limited employee details needed for public API integrations.\n\nUse the returned `id` as `assignedToUserId` when assigning leads through\n`PUT /v2/pub/leads/update`.\n", "security": [ { "ApiKeyAuth": [] } ], "responses": { "200": { "description": "Employees listed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "employees": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "example": "Alexia Morgan" }, "email": { "type": "string", "format": "email", "example": "alexia@example.com" }, "role": { "type": "string", "enum": [ "ADMIN", "MANAGER", "USER" ], "example": "MANAGER" }, "disabled": { "type": "boolean", "example": false }, "createdAt": { "type": "string", "format": "date-time" } } } } } } } }, "example": { "success": true, "data": { "employees": [ { "id": "11111111-1111-4111-8111-111111111111", "name": "Alexia Morgan", "email": "alexia@example.com", "role": "MANAGER", "disabled": false, "createdAt": "2024-01-03T10:00:00.000Z" } ] } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/clients/update": { "put": { "tags": [ "Contacts" ], "summary": "Update contact", "description": "Update Contact pipeline fields in the new Contacts experience.\n\nThis endpoint controls the Contact-level pipeline state used by the\nLeadTruffle app's Contacts and Inbox screens. It does not update legacy\nper-inquiry Lead status fields.\n\nIdentify the Contact with `clientId`, `phone`, or `email`. If multiple\nidentifiers are provided, `clientId` is used first, then `phone`, then\n`email`.\n\nThe request field is named `leadStatus` for API compatibility with the\nstored field name, but it represents the Contact pipeline status shown in\nthe app. Use `GET /v2/pub/clients/statuses` to list valid status IDs for\nthe company.\n\n`assignedToUserId` controls the Contact assignee. Use\n`GET /v2/pub/employees` to find enabled employee IDs.\n\n`archived` controls the Contact's client-pipeline Inbox archive state. It\ndoes not delete the Contact.\n\nSuccessful status, assignment, review-state, and archive-state changes\nare recorded in Contact history.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "clientId": { "type": "string", "format": "uuid", "description": "LeadTruffle Contact ID.", "example": "33333333-3333-4333-8333-333333333333" }, "phone": { "type": "string", "description": "Contact primary phone number.", "example": "+15551234567" }, "email": { "type": "string", "format": "email", "description": "Contact primary email address.", "example": "customer@example.com" }, "leadStatus": { "type": "string", "description": "Contact pipeline status ID. Named leadStatus for API compatibility.", "example": "CONTACTED" }, "assignedToUserId": { "type": "string", "format": "uuid", "nullable": true, "description": "Enabled employee ID to assign the Contact to. Send null to unassign.", "example": "11111111-1111-4111-8111-111111111111" }, "archived": { "type": "boolean", "description": "Whether this Contact should be archived from the active client-pipeline Inbox.", "example": true } } }, "examples": { "updateStatusByPhone": { "summary": "Update Contact status by phone", "value": { "phone": "+15551234567", "leadStatus": "CONTACTED" } }, "assignContact": { "summary": "Assign a Contact", "value": { "clientId": "33333333-3333-4333-8333-333333333333", "assignedToUserId": "11111111-1111-4111-8111-111111111111" } }, "unassignAndUnarchive": { "summary": "Clear assignment and archive state", "value": { "clientId": "33333333-3333-4333-8333-333333333333", "assignedToUserId": null, "archived": false } } } } } }, "responses": { "200": { "description": "Contact updated successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "companyName": { "type": "string", "nullable": true }, "primaryPhone": { "type": "string", "nullable": true }, "primaryEmail": { "type": "string", "nullable": true }, "leadStatus": { "type": "string", "description": "Contact pipeline status ID." }, "leadStatusUpdatedAt": { "type": "string", "format": "date-time", "nullable": true }, "assignedToUserId": { "type": "string", "format": "uuid", "nullable": true }, "assignedAt": { "type": "string", "format": "date-time", "nullable": true }, "archived": { "type": "boolean" }, "archivedAt": { "type": "string", "format": "date-time", "nullable": true }, "latestLeadSubmissionId": { "type": "string", "format": "uuid", "nullable": true }, "updatedAt": { "type": "string", "format": "date-time" } } } } }, "example": { "success": true, "data": { "id": "33333333-3333-4333-8333-333333333333", "companyId": "f096f9e3-001d-49ac-864c-3d73453bbe08", "name": "Sarah Johnson", "firstName": "Sarah", "lastName": "Johnson", "companyName": null, "primaryPhone": "+15551234567", "primaryEmail": "sarah@example.com", "leadStatus": "CONTACTED", "leadStatusUpdatedAt": "2026-05-28T14:30:00.000Z", "assignedToUserId": "11111111-1111-4111-8111-111111111111", "assignedAt": "2026-05-28T14:30:00.000Z", "archived": true, "archivedAt": "2026-05-28T14:30:00.000Z", "latestLeadSubmissionId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "updatedAt": "2026-05-28T14:30:00.000Z" } } } } }, "400": { "description": "Invalid request data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/clients/statuses": { "get": { "tags": [ "Contacts" ], "summary": "List contact pipeline statuses", "description": "List valid Contact pipeline status IDs for the authenticated company.\n\nUse these IDs as the `leadStatus` value in\n`PUT /v2/pub/clients/update`. The field is named `leadStatus` for API\ncompatibility with the stored field name, but these statuses control the\nnew Contact-level pipeline shown in Contacts and Inbox.\n", "security": [ { "ApiKeyAuth": [] } ], "responses": { "200": { "description": "Contact pipeline statuses listed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "statuses": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Status ID to send as leadStatus.", "example": "CONTACTED" }, "label": { "type": "string", "example": "Contacted" }, "description": { "type": "string", "nullable": true }, "order": { "type": "integer" }, "color": { "type": "string", "nullable": true }, "visible": { "type": "boolean" }, "isDefault": { "type": "boolean" }, "locked": { "type": "boolean" } } } } } } } }, "example": { "success": true, "data": { "statuses": [ { "id": "NEW", "label": "New", "description": "Fresh leads awaiting first touch", "order": 0, "color": "INDIGO", "visible": true, "isDefault": true, "locked": false }, { "id": "CONTACTED", "label": "Contacted", "description": "Outbound call or text sent", "order": 1, "color": "BLUE", "visible": true, "isDefault": true, "locked": false } ] } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/clients/{clientId}/notes": { "get": { "tags": [ "Contacts" ], "summary": "List contact notes", "description": "List active notes for a Contact in the new Contacts experience.\n\nThese are Contact-level notes from the `client_notes` table. They are not\nlegacy per-inquiry Lead notes.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "clientId", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "in": "query", "name": "limit", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 } }, { "in": "query", "name": "before", "required": false, "schema": { "type": "string", "format": "date-time" } } ], "responses": { "200": { "description": "Contact notes listed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "notes": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "clientId": { "type": "string", "format": "uuid" }, "body": { "type": "string" }, "authorType": { "type": "string", "enum": [ "EMPLOYEE", "SYSTEM" ] }, "createdByCompanyUserId": { "type": "string", "format": "uuid", "nullable": true }, "createdByName": { "type": "string", "nullable": true }, "createdByEmail": { "type": "string", "format": "email", "nullable": true }, "source": { "type": "string" }, "metadata": { "type": "object", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } } }, "nextBefore": { "type": "string", "format": "date-time", "nullable": true } } } } }, "example": { "success": true, "data": { "notes": [ { "id": "44444444-4444-4444-8444-444444444444", "clientId": "33333333-3333-4333-8333-333333333333", "body": "Customer wants a call next week.", "authorType": "SYSTEM", "createdByCompanyUserId": null, "createdByName": null, "createdByEmail": null, "source": "SYSTEM", "metadata": { "source": "PUBLIC_API" }, "createdAt": "2026-05-28T14:30:00.000Z", "updatedAt": "2026-05-28T14:30:00.000Z" } ], "nextBefore": null } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } }, "post": { "tags": [ "Contacts" ], "summary": "Add contact note", "description": "Add a new API-created note to a Contact in the new Contacts experience.\n\nNotes created here are Contact-level notes. They appear in the Contact\ndetails notes thread and do not modify legacy per-inquiry Lead notes.\nSuccessful note creation is recorded in Contact history.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "clientId", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "body" ], "properties": { "body": { "type": "string", "minLength": 1, "maxLength": 5000, "example": "Customer wants a call next week." } } } } } }, "responses": { "201": { "description": "Contact note created successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object" } } }, "example": { "success": true, "data": { "id": "44444444-4444-4444-8444-444444444444", "clientId": "33333333-3333-4333-8333-333333333333", "body": "Customer wants a call next week.", "authorType": "SYSTEM", "createdByCompanyUserId": null, "createdByName": null, "createdByEmail": null, "source": "SYSTEM", "metadata": { "source": "PUBLIC_API" }, "createdAt": "2026-05-28T14:30:00.000Z", "updatedAt": "2026-05-28T14:30:00.000Z" } } } } }, "400": { "description": "Invalid request data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/chat-widget/config": { "get": { "tags": [ "Widgets" ], "summary": "Get chat widget configuration", "description": "Retrieve the current chat widget configuration for your company.\n\nThis includes styling settings, AI agent configuration, and lead qualification settings.\nIf no configuration exists, a default configuration will be created automatically.\n", "security": [ { "ApiKeyAuth": [] } ], "responses": { "200": { "description": "Successfully retrieved chat widget configuration", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/ChatWidgetConfig" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } }, "put": { "tags": [ "Widgets" ], "summary": "Update chat widget configuration", "description": "Update the chat widget configuration for your company.\n\nYou can partially update the configuration by including only the fields you want to change.\nThis endpoint combines styling updates and AI agent configuration updates.\n\n**Key Configuration Options:**\n- **greetingMessage**: The initial message shown to visitors\n- **agentConfig**: AI agent styling, popup behavior, and appearance\n- **leadQualifierAgentConfig**: AI qualification logic and message limits\n- **Styling**: Primary/secondary colors, background, custom CSS\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatWidgetConfigUpdate" }, "examples": { "basic_styling": { "summary": "Update basic styling", "value": { "greetingMessage": "Welcome to our company! How can we help?", "primaryColor": "#007bff", "secondaryColor": "#6c757d" } }, "agent_config": { "summary": "Update AI agent configuration", "value": { "agentConfig": { "name": "Sarah", "avatarImageUrl": "https://example.com/avatar.png", "popupEnabled": true, "popupOpenDelay": 5 }, "leadQualifierAgentConfig": { "agentInstructions": "You are a helpful assistant for our HVAC company. Ask about their location, timeline, and budget.", "maximumMessagesLimit": 20 } } }, "full_config": { "summary": "Complete configuration update", "value": { "greetingMessage": "Welcome! Text us for immediate help.", "agentConfig": { "name": "Alex", "popupEnabled": true, "popupOpenDelay": 3, "popupHeader1": "Need HVAC Help?", "popupHeader2": "Get a quote in minutes!" }, "leadQualifierAgentEnabled": true, "leadQualifierAgentConfig": { "agentInstructions": "Qualify leads for our HVAC services by asking about their location, project timeline, and budget range.", "maximumMessagesLimit": 20 }, "primaryColor": "#ff6b35", "backgroundColor": "#ffffff" } } } } } }, "responses": { "200": { "description": "Configuration successfully updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/ChatWidgetConfig" } } } } } }, "400": { "description": "Invalid configuration data", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "details": { "type": "object", "description": "Validation error details" } } } ] } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Widget configuration not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/chat-widget/trigger-lead-qualifier": { "post": { "tags": [ "Widgets" ], "summary": "Trigger lead qualifier agent", "description": "**⚠️ RESTRICTED ENDPOINT - APPROVAL REQUIRED ⚠️**\n\nThis endpoint allows you to programmatically submit leads and trigger the AI lead qualification process.\n\n## Prerequisites & Restrictions\n\n**This endpoint may ONLY be used if ALL of the following conditions are met:**\n\n1. **Active 10DLC Phone Number**: Your company must have an active 10DLC (Application-to-Person) phone number provisioned\n2. **Lead Qualifier Agent Enabled**: The lead qualification AI agent must be enabled in your widget configuration\n3. **Approved Usage**: LeadTruffle must have explicitly approved your specific use case and implementation\n4. **SMS Opt-in Compliance**: You must have proper SMS opt-in consent as part of your lead collection flow\n\n## Pre-Approved Use Cases\n\nThe following lead sources have been pre-approved for use with this endpoint:\n- **Facebook Lead Ads** - With proper SMS opt-in checkbox\n- **Google Lead Ads** - With SMS consent collection\n- **Thumbtack Lead Ads** - Following platform opt-in requirements \n- **Yelp Lead Ads** - With SMS permission collection\n\n## Compliance Requirements\n\n**SMS Opt-in Consent**: You MUST collect explicit SMS opt-in consent from leads before using this endpoint. This includes:\n- Clear disclosure that they will receive SMS messages\n- Explicit consent checkbox or opt-in mechanism\n- Compliance with TCPA regulations\n- Documentation of consent for audit purposes\n\n**Approval Process**: Contact support@leadtruffle.com to:\n1. Submit your lead collection workflow for review\n2. Demonstrate proper SMS opt-in implementation\n3. Receive written approval for your specific use case\n4. Get the permission flag enabled on your account\n\n## Important Notes\n\n- **Unauthorized usage will result in immediate API access suspension**\n- Only approved opt-in methods will be accepted\n- LeadTruffle reserves the right to audit and verify compliance\n- All lead submissions are logged and monitored\n- You are responsible for TCPA and SMS compliance\n- If the lead is submitted during the company's quiet hours, the lead is still created immediately, but the initial SMS is queued to retry the next morning starting at 9:00 AM in the company's local timezone\n- A successful `201` response means the lead record was accepted; it does not guarantee the first SMS was sent immediately\n\n## Usage Example\n\n```json\n{\n \"name\": \"John Doe\",\n \"phone\": \"+15551234567\",\n \"additionalData\": {\n \"leadSource\": \"facebook\",\n \"fbid\": \"923849028492034890283094\",\n \"campaignId\": \"summer_hvac_2024\",\n \"smsOptInConfirmed\": \"true\",\n \"optInTimestamp\": \"2024-01-01T12:00:00Z\"\n }\n}\n```\n\nThe lead qualifier AI will automatically initiate an SMS conversation with the provided phone number using your configured qualification questions and company branding.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatWidgetLeadTrigger" }, "examples": { "facebook_lead": { "summary": "Facebook Lead Ad", "value": { "name": "Sarah Johnson", "phone": "+15551234567", "email": "sarah.johnson@example.com", "message": "Interested in HVAC quote", "additionalData": { "leadSource": "facebook", "fbid": "923849028492034890283094", "campaignId": "summer_hvac_2024", "smsOptInConfirmed": "true" } } }, "google_lead": { "summary": "Google Lead Ad", "value": { "name": "Mike Rodriguez", "phone": "+15559876543", "additionalData": { "leadSource": "google", "gclid": "1234567890abcdef", "campaignId": "hvac_repair_campaign", "smsOptInConfirmed": "true" } } }, "yelp_lead": { "summary": "Yelp Lead", "value": { "name": "Lisa Chen", "phone": "+15555555555", "email": "lisa.chen@example.com", "additionalData": { "leadSource": "yelp", "yelpLeadId": "yelp_lead_12345", "serviceRequested": "AC Repair", "smsOptInConfirmed": "true" } } } } } } }, "responses": { "201": { "description": "Lead successfully created. SMS qualification is either started immediately or queued for the next local morning if the lead arrived during quiet hours.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatWidgetLeadTriggerResponse" } } } }, "400": { "description": "Invalid request data or missing required fields", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "details": { "type": "object", "description": "Validation error details" } } } ] }, "examples": { "validation_error": { "summary": "Validation Error", "value": { "success": false, "error": "Invalid input", "details": { "phone": { "_errors": [ "Phone number is required" ] } } } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "403": { "description": "Insufficient permissions or approval required", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "not_approved": { "summary": "Not Approved", "value": { "success": false, "error": "This endpoint requires explicit approval from LeadTruffle. Contact support@leadtruffle.com to request access." } }, "no_10dlc": { "summary": "No 10DLC Number", "value": { "success": false, "error": "Active 10DLC phone number required. Please provision a 10DLC number before using this endpoint." } }, "qualifier_disabled": { "summary": "Lead Qualifier Disabled", "value": { "success": false, "error": "Lead qualifier agent must be enabled in your widget configuration." } } } } } }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" }, "501": { "description": "Feature not yet implemented", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Chat widget lead creation is not yet implemented" } } } } } } }, "/v1/pub/default-calendar/appointments": { "get": { "tags": [ "Default Calendar" ], "summary": "Get Default Calendar Appointments", "description": "Retrieve appointments for the company's default (primary) calendar with optional date filtering and pagination.\n\n**Default Behavior:**\n- If no date range is specified, returns appointments from today to 2 weeks in the future\n- Returns up to 100 appointments per request\n- Results are ordered by appointment start time (ascending)\n- Includes associated client information when available\n\n**Date Filtering:**\n- Use `startDate` and `endDate` query parameters to specify custom date ranges\n- Dates must be in ISO 8601 format (e.g., \"2024-01-15T00:00:00Z\")\n\n**Pagination:**\n- Use `limit` (1-100) and `offset` parameters for pagination\n- Response includes pagination metadata to help navigate through results\n", "parameters": [ { "name": "startDate", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Start date for appointment filtering (ISO 8601 format). Defaults to today.", "example": "2024-01-15T00:00:00Z" }, { "name": "endDate", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "End date for appointment filtering (ISO 8601 format). Defaults to 2 weeks from today.", "example": "2024-01-29T23:59:59Z" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 100 }, "description": "Maximum number of appointments to return", "example": 50 }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Number of appointments to skip for pagination", "example": 0 } ], "responses": { "200": { "description": "Appointments retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "calendar": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Calendar ID" }, "name": { "type": "string", "description": "Calendar name" }, "timeZone": { "type": "string", "description": "Calendar timezone" } }, "example": { "id": "123e4567-e89b-12d3-a456-426614174000", "name": "Primary", "timeZone": "America/New_York" } }, "appointments": { "type": "array", "items": { "$ref": "#/components/schemas/AppointmentCreatedWebhookPayload" } }, "pagination": { "type": "object", "properties": { "limit": { "type": "integer", "description": "Number of appointments requested" }, "offset": { "type": "integer", "description": "Number of appointments skipped" }, "total": { "type": "integer", "description": "Total number of appointments matching filter" }, "hasMore": { "type": "boolean", "description": "Whether there are more appointments beyond this page" } }, "example": { "limit": 100, "offset": 0, "total": 45, "hasMore": false } }, "filters": { "type": "object", "properties": { "startDate": { "type": "string", "format": "date-time", "description": "Applied start date filter" }, "endDate": { "type": "string", "format": "date-time", "description": "Applied end date filter" } }, "example": { "startDate": "2024-01-15T00:00:00.000Z", "endDate": "2024-01-29T23:59:59.999Z" } } } } } } } } }, "400": { "description": "Bad request - invalid query parameters or date range", "content": { "application/json": { "schema": { "allOf": [ { "$ref": "#/components/schemas/Error" }, { "type": "object", "properties": { "details": { "type": "object", "additionalProperties": true, "description": "Field validation errors when query parameters are invalid." } } } ] }, "examples": { "invalid_date_range": { "summary": "Invalid date range", "value": { "success": false, "error": "startDate must be before endDate" } }, "invalid_parameters": { "summary": "Invalid query parameters", "value": { "success": false, "error": "Invalid query parameters", "details": { "limit": { "_errors": [ "Number must be less than or equal to 100" ] } } } } } } } }, "404": { "description": "No primary calendar found for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "No primary calendar found for this company" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/pub/default-calendar/info": { "get": { "tags": [ "Default Calendar" ], "summary": "Get Default Calendar Information", "description": "Retrieve basic information about the company's default (primary) calendar including\nconfiguration settings like booking policies, time zones, and availability rules.\n", "responses": { "200": { "description": "Calendar information retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/Calendar" } } } } } }, "404": { "description": "No primary calendar found for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "No primary calendar found for this company" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/v1/pub/email-gateway/submit": { "post": { "tags": [ "Email gateway" ], "summary": "Submit email gateway message", "description": "Submit email content to the email gateway so LeadTruffle can parse it and initiate follow-up emails.\n\nUse `customerEmail` when you already know the lead's email address to skip AI extraction.\nInclude `messageId` to safely retry without creating duplicates.\nOnly submit leads who have explicitly requested contact and follow applicable email compliance rules.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailGatewaySubmitRequest" }, "examples": { "basic_submission": { "summary": "Basic submission", "value": { "emailGatewayAddress": "c3f3c4a2-1234-5678-9012-abcdefabcdef@m.leadtruffle.com", "emailBodyText": "New web form lead. Name: Jane Doe. Email: jane@example.com. Service: HVAC repair.", "emailSubject": "New website lead" } }, "with_customer_email": { "summary": "With known customer email", "value": { "emailGatewayAddress": "c3f3c4a2-1234-5678-9012-abcdefabcdef@m.leadtruffle.com", "emailBodyText": "Customer requested a quote for landscaping services.", "emailSubject": "Quote request", "customerEmail": "jane@example.com", "messageId": "ext-msg-12345" } } } } } }, "responses": { "200": { "description": "Submission accepted", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EmailGatewaySubmitResponse" }, "examples": { "queued": { "summary": "Queued for processing", "value": { "success": true, "data": { "inboundEmailId": "3c8b9b67-4b4a-4f2b-9f33-6f1e4e8b2f29", "status": "QUEUED", "queuedAction": "QUALIFY_OVER_EMAIL" } } }, "duplicate": { "summary": "Duplicate submission", "value": { "success": true, "data": { "inboundEmailId": "3c8b9b67-4b4a-4f2b-9f33-6f1e4e8b2f29", "status": "DUPLICATE" } } } } } } }, "400": { "description": "Invalid request payload or blocked by policy (opt-out, unsupported action)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "$ref": "#/components/responses/NotFoundError" }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/process-incoming-message": { "post": { "tags": [ "Yelp Lead Agent" ], "summary": "Process incoming Yelp message with AI agent (New Consumer Message)", "description": "Submit a Yelp inbox message to the LeadTruffle Yelp AI agent so it can continue the conversation,\nqualify the lead, and create CRM records if needed.\n\nThis endpoint mirrors the internal Yelp inbox workflow and should be called whenever a new\nmessage is received in Yelp or when you want to simulate a new consumer message. If the reply\nis null, do not send a response message; this can happen when manual takeover is enabled or\nwhen the initial lead reply was already sent by a concurrent event.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpLeadAgentRequest" }, "example": { "yelpLeadId": "lead_abc123", "yelpBusinessId": "business_456", "messageId": "msg_789", "userType": "CONSUMER", "userDisplayName": "Jane Smith", "timeCreated": "2024-12-01T15:04:05Z", "text": "Hi, can I get a quote for gutter cleaning?" } } } }, "responses": { "200": { "description": "Yelp agent processed the message", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpLeadAgentResponse" }, "examples": { "reply_available": { "summary": "Reply generated", "value": { "success": true, "data": { "reply": "Hi Jane! We'd love to help with your gutter cleaning. Can you confirm the best phone number to reach you and whether mornings or afternoons work better next week?", "leadId": "d6c6f5d1-6c17-4ab2-a41d-1e2a0b9acb1f", "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "isDuplicate": false, "manualTakeoverEnabled": false, "qualificationResponse": { "response": "Hi Jane! We'd love to help with your gutter cleaning. Can you confirm the best phone number to reach you and whether mornings or afternoons work better next week?", "contactReason": "Customer needs gutter cleaning and would like it scheduled next week.", "allQuestionsAnswered": false, "isClientReadyToBook": false, "detectedAbuse": false, "shouldSkipReply": false, "hasImages": true, "dataFields": [ { "name": "customer_name", "value": "Jane Smith" }, { "name": "service", "value": "Gutter cleaning" }, { "name": "timeline", "value": "Next week" } ], "commonFields": { "phone": "+15551234567", "email": "jane@example.com", "customerName": "Jane Smith", "fullAddress": "123 Main St, Springfield, IL 62704", "address": "123 Main St", "city": "Springfield", "state": "IL", "zipcode": "62704", "isHomeowner": true } } } } }, "reply_suppressed": { "summary": "Reply suppressed (do not send response)", "value": { "success": true, "data": { "reply": null, "leadId": "d6c6f5d1-6c17-4ab2-a41d-1e2a0b9acb1f", "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "isDuplicate": false, "manualTakeoverEnabled": true, "qualificationResponse": null } } } } } } }, "400": { "description": "Invalid payload", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/process-new-lead": { "post": { "tags": [ "Yelp Lead Agent" ], "summary": "Capture Yelp New Lead payload", "description": "Store the raw Yelp New Lead webhook payload, capture temporary contact info, and ensure the lead/client records are upserted. This endpoint now also generates the initial AI reply when possible and returns it so your Zap can forward it to Yelp. If the reply is null, do not send a response message. The reply may be null when the agent is disabled, manual takeover is enabled, or a duplicate first message is suppressed.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpLeadAgentLeadRequest" }, "example": { "yelpLeadId": "2XS0bA60kwwGv31HZ794eQ", "yelpBusinessId": "VSyxnREXHd5ZHFMSd6338Q", "payload": { "leadUserName": "Megha S.", "leadTimeCreated": "2025-11-11T00:11:07+00:00", "leadTimeUpdated": "2025-11-11T00:11:11+00:00", "temporaryEmail": "leadsapi+94768a8de2304b9b8f908fbf65d80489@messaging.yelp.com", "temporaryEmailExpiry": "2025-12-11T00:11:14+00:00", "temporaryPhone": "+15551230000", "temporaryPhoneExpiry": "2025-12-11T00:11:14+00:00", "project": { "projectSurveyAnswersFormatted": "Q: What do you need done?\nA: Fix a leak.\nQ: When do you need this?\nA: ASAP.\n", "location": { "postalCode": "95337" }, "details": "availability", "availabilityStatus": "ASAP", "jobNames": [ "Plumbing repair" ], "attachmentUrls": [ "https://example.com/attachment1.jpg", "https://example.com/attachment2.jpg" ] }, "business": { "name": "Discount Plumbing", "url": "https://www.yelp.com/biz/discount-plumbing-manteca-6", "formattedAddress": "787 Cottage Ave, Manteca, CA 95336" } } } } } }, "responses": { "200": { "description": "Payload stored.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpLeadAgentLeadResponse" }, "examples": { "reply_available": { "summary": "Reply generated", "value": { "success": true, "data": { "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "leadId": "d6c6f5d1-6c17-4ab2-a41d-1e2a0b9acb1f", "clientId": "f3bbf751-0a6e-4ffb-8cb1-0bbca9698a7d", "reply": "Thanks for reaching out! Can you share the best number and a couple details about the leak?", "manualTakeoverEnabled": false, "qualificationResponse": { "response": "Thanks for reaching out! Can you share the best number and a couple details about the leak?", "contactReason": "Plumbing leak repair", "allQuestionsAnswered": false, "isClientReadyToBook": false, "detectedAbuse": false, "shouldSkipReply": false, "hasImages": true, "dataFields": [ { "name": "customer_name", "value": "Megha S." }, { "name": "phone", "value": "" } ], "commonFields": { "phone": null, "email": null, "fullAddress": null, "address": null, "zipcode": "95337", "state": null, "city": null, "isHomeowner": null, "customerName": "Megha S." } } } } }, "reply_suppressed": { "summary": "Reply suppressed (do not send response)", "value": { "success": true, "data": { "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "leadId": "d6c6f5d1-6c17-4ab2-a41d-1e2a0b9acb1f", "clientId": "f3bbf751-0a6e-4ffb-8cb1-0bbca9698a7d", "reply": null, "manualTakeoverEnabled": true, "qualificationResponse": null, "initialReplySkipped": true } } } } } } }, "400": { "description": "Invalid payload", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/process-phone-availability": { "post": { "tags": [ "Yelp Lead Agent" ], "summary": "Capture Yelp Phone Number Available payload", "description": "Receive Yelp's \"Phone Number Available\" webhook from Zapier and attach the customer phone number to the matching LeadTruffle Yelp lead.\n\nFor the simplest Zapier setup, send only `yelpLeadId` and `temporaryPhone`. The endpoint also accepts `phone` and `temporaryPhoneNumber` as aliases for the same value. The phone is treated as the customer phone number after validation/normalization, even though Yelp/Zapier may still label it as temporary.\n\nThis endpoint enriches the existing Yelp conversation and LeadTruffle lead created by the earlier Yelp New Lead webhook. The linked Client primary phone is filled when blank. If the Client already has a different primary phone, LeadTruffle leaves it unchanged and still updates the lead record with the Yelp phone. If no matching conversation or lead exists, the endpoint returns a successful no-op response with `skippedReason`.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "yelpLeadId" ], "anyOf": [ { "required": [ "temporaryPhone" ] }, { "required": [ "phone" ] }, { "required": [ "temporaryPhoneNumber" ] } ], "properties": { "yelpLeadId": { "type": "string", "description": "Yelp Lead ID from the Phone Number Available trigger.", "example": "1RQghT7x6uLKiGhzgzJZXQ" }, "temporaryPhone": { "type": "string", "description": "Customer phone number from Yelp's Temporary Phone Number field.", "example": "+17206416322" }, "yelpBusinessId": { "type": "string", "description": "Yelp Business ID when available.", "example": "i4EmGWPneZPCiOJQHyfluQ" }, "phone": { "type": "string", "description": "Optional alias for `temporaryPhone`.", "example": "+17206416322" }, "temporaryPhoneNumber": { "type": "string", "description": "Optional alias for `temporaryPhone`.", "example": "+17206416322" }, "phoneExpiry": { "type": "string", "description": "Optional Yelp phone expiration timestamp.", "example": "2099-12-31T23:59:59+00:00" }, "leadUserName": { "type": "string", "description": "Optional Yelp lead display name.", "example": "Andrew B." }, "leadTimeCreated": { "type": "string", "description": "Optional Yelp lead creation timestamp.", "example": "2026-04-20T18:48:40+00:00" }, "leadTimeUpdated": { "type": "string", "description": "Optional Yelp lead update timestamp.", "example": "2026-04-20T18:49:01+00:00" }, "payload": { "type": "object", "additionalProperties": true, "description": "Optional raw Yelp/Zapier payload fields to merge into the stored Yelp metadata." } } }, "example": { "yelpLeadId": "1RQghT7x6uLKiGhzgzJZXQ", "temporaryPhone": "+17206416322" } } } }, "responses": { "200": { "description": "Phone captured and lead/client records synchronized.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "conversationId": { "type": "string", "nullable": true, "format": "uuid" }, "leadId": { "type": "string", "nullable": true, "format": "uuid" }, "clientId": { "type": "string", "nullable": true, "format": "uuid" }, "phone": { "type": "string", "example": "+17206416322" }, "leadUpdated": { "type": "boolean" }, "clientUpdated": { "type": "boolean" }, "clientPhoneUpdateSkipped": { "type": "boolean" }, "skippedReason": { "type": "string", "nullable": true, "enum": [ "CONVERSATION_NOT_FOUND", "LEAD_NOT_FOUND" ] } } } } }, "example": { "success": true, "data": { "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "leadId": "d6c6f5d1-6c17-4ab2-a41d-1e2a0b9acb1f", "clientId": "f3bbf751-0a6e-4ffb-8cb1-0bbca9698a7d", "phone": "+17206416322", "leadUpdated": true, "clientUpdated": true, "clientPhoneUpdateSkipped": false } } } } }, "400": { "description": "Invalid payload or phone number", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/process-business-message": { "post": { "tags": [ "Yelp Lead Agent" ], "summary": "Process incoming business message from Yelp", "description": "Receive a message sent by the business directly from their Yelp account (via Zapier's \"New Business Message\" trigger).\n\nThis endpoint stores the message in the conversation history but does NOT trigger any AI response.\nUse this when the business replies manually from Yelp and you want to keep the conversation\nhistory in sync.\n\n**Deduplication:** Messages are deduplicated based on content to prevent duplicates when\nZapier fires after the AI or webhook already sent the same message.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpBusinessMessageRequest" }, "example": { "yelpLeadId": "lead_abc123", "messageId": "msg_business_789", "yelpBusinessId": "business_456", "text": "Thanks for reaching out! We can schedule you for next Tuesday. Does 10am work?", "timeCreated": "2024-12-01T16:00:00Z", "userDisplayName": "ABC Plumbing" } } } }, "responses": { "200": { "description": "Business message processed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpBusinessMessageResponse" }, "examples": { "stored": { "summary": "Message stored", "value": { "success": true, "data": { "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "messageId": "msg_business_789" } } }, "skipped": { "summary": "Duplicate message skipped", "value": { "success": true, "data": { "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "messageId": "msg_business_789", "skipped": true, "reason": "DUPLICATE_CONTENT" } } } } } } }, "400": { "description": "Invalid payload", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Conversation not found for the given yelpLeadId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/send-message": { "post": { "tags": [ "Yelp Lead Agent" ], "summary": "Send a follow-up message to a Yelp lead", "description": "Send a message to a Yelp lead via webhook. This fires a `YELP_MESSAGE_OUTBOUND` webhook\nevent that Zapier can receive and relay to Yelp using the \"Create Message\" action.\n\n**Prerequisites:**\n1. Register a webhook for event type `YELP_MESSAGE_OUTBOUND` via `POST /v2/pub/webhooks`\n2. Point the webhook URL to a Zapier \"Webhooks by Zapier\" trigger\n3. Connect that trigger to Yelp's \"Create Message\" action\n\n**Identifying the conversation:**\nYou must provide either `conversationId` (our internal UUID) or `externalLeadId` (the Yelp lead ID).\n\n**What happens:**\n1. Message is appended to conversation history with `userType: BUSINESS_OUTBOUND`\n2. Webhook fires to all registered `YELP_MESSAGE_OUTBOUND` URLs\n3. Zapier receives the webhook and sends the message to Yelp\n\nIf manual takeover is enabled for the conversation, this endpoint returns `409`\nand no outbound message is appended or sent.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpSendMessageRequest" }, "example": { "externalLeadId": "lead_abc123", "message": "Hi! Just following up on your request. Are you still interested in getting a quote?", "attachmentUrls": [] } } } }, "responses": { "200": { "description": "Message sent successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpSendMessageResponse" }, "example": { "success": true, "data": { "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "webhooksTriggered": 2 } } } } }, "400": { "description": "Invalid payload or missing conversationId/externalLeadId", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Conversation not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "Manual takeover is enabled for this conversation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/conversations": { "get": { "tags": [ "Yelp Lead Agent" ], "summary": "List Yelp AI conversations", "description": "Retrieve paginated Yelp conversation history for the authenticated company.", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }, "description": "Maximum number of conversations to return." }, { "in": "query", "name": "offset", "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Number of records to skip for pagination." } ], "responses": { "200": { "description": "Conversations retrieved.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpLeadConversationListResponse" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/conversations/lead/{externalLeadId}": { "get": { "tags": [ "Yelp Lead Agent" ], "summary": "Get Yelp conversation by Yelp lead ID", "description": "Fetch a single Yelp conversation using the Yelp lead identifier (stored as externalLeadId).", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "path", "name": "externalLeadId", "required": true, "schema": { "type": "string" }, "description": "Yelp lead identifier to look up." } ], "responses": { "200": { "description": "Conversation retrieved.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpLeadConversationResponse" } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Conversation not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v1/pub/yelp-lead-agent/outbound-messages": { "get": { "tags": [ "Yelp Lead Agent" ], "summary": "List outbound messages in webhook payload format", "description": "Retrieve recent outbound messages (messages sent from business to Yelp leads) formatted\nas `YELP_MESSAGE_OUTBOUND` webhook payloads.\n\n**Purpose:** This endpoint is designed to provide test data for Zapier webhook registration.\nWhen setting up a Zapier trigger for `YELP_MESSAGE_OUTBOUND` webhooks, Zapier needs sample\ndata to configure the workflow. This endpoint returns real messages in the exact format\nthat webhooks deliver.\n\n**What's included:**\n- Messages sent via the `/send-message` endpoint\n- Messages triggered from the LeadTruffle UI\n- Messages with `triggeredBy` metadata (API, UI, or SCHEDULED)\n\n**Format:** Each item in the response array matches the `YELP_MESSAGE_OUTBOUND` webhook payload structure.\n", "security": [ { "ApiKeyAuth": [] } ], "parameters": [ { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 }, "description": "Maximum number of messages to return." }, { "in": "query", "name": "offset", "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Number of records to skip for pagination." } ], "responses": { "200": { "description": "Outbound messages retrieved in webhook payload format.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/YelpOutboundMessagesListResponse" }, "example": { "success": true, "data": [ { "eventType": "YELP_MESSAGE_OUTBOUND", "eventTypeDetails": "YELP_FOLLOWUP_MESSAGE", "yelpLeadId": "lead_abc123", "yelpBusinessId": "business_456", "conversationId": "ad10af01-6d8a-4b46-83aa-4a7d38c35172", "companyId": "comp_99999999-8888-7777-6666-444444444444", "message": "Hi! Thanks for reaching out. Are you still interested in getting a quote?", "attachmentUrls": [], "timestamp": "2024-01-16T10:00:00.000Z", "triggeredBy": "UI", "leadId": null, "clientId": null, "leadInformation": { "name": "John Doe", "phone": "+15551234567", "email": "john.doe@example.com" } } ], "pagination": { "total": 25, "limit": 10, "offset": 0, "hasMore": true } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/leads/conversion": { "put": { "tags": [ "Lead inquiries" ], "summary": "Upsert (create or update) lead conversion data", "description": "Report conversion data (revenue, status) for a specific lead.\n\nThis endpoint allows CRMs and external systems to report back when a lead converts to a customer.\nIf a conversion already exists for the lead, it will be updated. Otherwise, a new conversion record is created.\n\n**Upsert Behavior:**\n- First call for a lead → Creates new conversion\n- Subsequent calls → Updates existing conversion\n- Every successful call also updates the lead record's `conversionStatus`,\n which is the status shown on the Lead Workflow board.\n\n**Input**:\n- EITHER - leadId or phone is required to associate the conversion with.\n\n**Use Cases:**\n- CRM reports when a lead converts to a paying customer\n- Update revenue amount when final billing is processed\n- Track conversion status changes (QUOTED → WON)\n- Store integration-specific data in sourceData field\n\n**Important Notes:**\n- Revenue amount is optional (you can track conversions without revenue)\n- `conversionStatus` updates both the conversion record and the lead's\n workflow-board status.\n- Currency defaults to USD if not specified\n- Source field helps track which CRM/system reported the conversion\n- Source defaults to 'API' if not specified\n- sourceData field can store CRM-specific metadata (max 10KB)\n- Identify the lead by providing either leadId or phone in the request body. If both are provided, leadId is used.\n\n**Webhook Integration:**\nWhen you report conversions via this API, the conversion data will be automatically\nincluded in all future webhook payloads for this lead (LEAD_CREATED, CONVERSATION_COMPLETED).\nThis allows downstream systems to have complete lead lifecycle data including revenue tracking.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConversionUpsertRequest" }, "examples": { "wonWithRevenue": { "summary": "Won deal with revenue", "value": { "leadId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "WON", "revenueAmount": "1500.00", "currency": "USD", "source": "HousecallPro", "notes": "Customer signed annual contract", "sourceData": { "crmId": "SF-12345", "salesRep": "John Doe", "closedDate": "2024-01-15" } } }, "lostDeal": { "summary": "Lost deal", "value": { "phone": "+15551234567", "conversionStatus": "LOST", "source": "Jobber", "notes": "Customer went with competitor" } }, "quotedNoRevenue": { "summary": "Quoted without revenue", "value": { "leadId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "QUOTED", "source": "ServiceTitan", "notes": "Sent estimate via email" } } } } } }, "responses": { "200": { "description": "Successfully updated existing conversion", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConversionUpsertResponse" }, "example": { "success": true, "data": { "id": "conv_abc123", "companyId": "comp_xyz789", "leadFormSubmissionId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "WON", "revenueAmount": "1500.00", "currency": "USD", "source": "HousecallPro", "notes": "Customer signed annual contract", "sourceData": { "crmId": "SF-12345", "salesRep": "John Doe" }, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T14:45:00Z", "isUpdate": true, "lead": { "id": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "WON", "conversionStatusUpdatedAt": "2024-01-15T14:45:00Z" } } } } } }, "201": { "description": "Successfully created new conversion", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConversionUpsertResponse" }, "example": { "success": true, "data": { "id": "conv_abc123", "companyId": "comp_xyz789", "leadFormSubmissionId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "WON", "revenueAmount": "1500.00", "currency": "USD", "source": "HousecallPro", "notes": "Customer signed annual contract", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-15T10:30:00Z", "isUpdate": false, "lead": { "id": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "conversionStatus": "WON", "conversionStatusUpdatedAt": "2024-01-15T10:30:00Z" } } } } } }, "400": { "description": "Invalid request - validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "missingIdentifier": { "summary": "Missing lead identifier", "value": { "success": false, "error": "You must provide either leadId or phone" } }, "invalidPhone": { "summary": "Invalid phone format", "value": { "success": false, "error": "Invalid phone number format" } }, "invalidStatus": { "summary": "Invalid conversion status", "value": { "success": false, "error": "Invalid conversion status. Must be one of: WON, LOST, QUOTED, PENDING, CONTACTED, NURTURING, CLOSED" } }, "invalidRevenue": { "summary": "Invalid revenue amount", "value": { "success": false, "error": "Invalid revenue amount. Must be a positive number with max 2 decimal places (0.01 - 999,999,999.99)" } }, "invalidCurrency": { "summary": "Invalid currency", "value": { "success": false, "error": "Invalid currency. Must be one of: USD, CAD" } }, "notesTooLong": { "summary": "Notes too long", "value": { "success": false, "error": "Notes must be 1000 characters or less" } }, "sourceDataTooLarge": { "summary": "Source data too large", "value": { "success": false, "error": "Source data is too large (max 10KB)" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Lead not found" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/leads/update": { "put": { "tags": [ "Lead inquiries" ], "summary": "Update lead", "description": "Update mutable fields on a LeadTruffle lead.\n\nIdentify the lead with either `leadId` or `phone`. If both are provided,\n`leadId` is used. Provide at least one update field.\n\n`archived` controls whether the lead is hidden from active lead boards.\nIt does not delete the lead.\n\n`assignedToUserId` assigns the lead to an enabled employee. Use\n`GET /v2/pub/employees` to find employee IDs. Send null to unassign.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "leadId": { "type": "string", "format": "uuid", "description": "LeadTruffle lead ID. Used first when both leadId and phone are provided.", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" }, "phone": { "type": "string", "description": "Lead phone number. LeadTruffle normalizes US phone numbers before matching.", "example": "+15551234567" }, "conversionStatus": { "type": "string", "enum": [ "NEW", "WON", "LOST", "QUOTED", "PENDING", "CONTACTED", "NURTURING", "CLOSED" ], "description": "Optional lead conversion/status value.", "example": "LOST" }, "notes": { "type": "string", "nullable": true, "maxLength": 1000, "description": "Optional lead notes. Send null to clear notes.", "example": "Customer went with another provider." }, "leadNotes": { "type": "string", "nullable": true, "maxLength": 1000, "description": "Alias for notes. Send either notes or leadNotes, not both." }, "archived": { "type": "boolean", "description": "Whether this lead should be hidden from active lead boards.", "example": true }, "assignedToUserId": { "type": "string", "format": "uuid", "nullable": true, "description": "Enabled employee ID to assign the lead to. Send null to unassign.", "example": "11111111-1111-4111-8111-111111111111" } } }, "examples": { "updateStatusByPhone": { "summary": "Update status by phone", "value": { "phone": "+15551234567", "conversionStatus": "LOST" } }, "updateNotesAndArchive": { "summary": "Update notes and archive by lead ID", "value": { "leadId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "notes": "Customer booked through Workiz.", "archived": true } }, "clearNotes": { "summary": "Clear notes", "value": { "phone": "+15551234567", "notes": null } }, "assignLead": { "summary": "Assign a lead to an employee", "value": { "leadId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "assignedToUserId": "11111111-1111-4111-8111-111111111111" } }, "unassignLead": { "summary": "Clear lead assignment", "value": { "leadId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "assignedToUserId": null } } } } } }, "responses": { "200": { "description": "Lead updated successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "phone": { "type": "string", "nullable": true }, "email": { "type": "string", "nullable": true }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "conversionStatus": { "type": "string", "nullable": true }, "conversionStatusUpdatedAt": { "type": "string", "format": "date-time", "nullable": true }, "leadNotes": { "type": "string", "nullable": true }, "archived": { "type": "boolean" }, "assignedToUserId": { "type": "string", "format": "uuid", "nullable": true }, "assignedAt": { "type": "string", "format": "date-time", "nullable": true }, "assignedToEmployee": { "type": "object", "nullable": true, "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string" }, "email": { "type": "string", "format": "email" }, "role": { "type": "string", "enum": [ "ADMIN", "MANAGER", "USER" ] }, "disabled": { "type": "boolean" }, "createdAt": { "type": "string", "format": "date-time" } } }, "updatedAt": { "type": "string", "format": "date-time" } } } } }, "example": { "success": true, "data": { "id": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b", "companyId": "f096f9e3-001d-49ac-864c-3d73453bbe08", "phone": "+15551234567", "email": "customer@example.com", "firstName": "Victor", "lastName": null, "conversionStatus": "LOST", "conversionStatusUpdatedAt": "2026-05-28T14:30:00.000Z", "leadNotes": "Customer went with another provider.", "archived": true, "assignedToUserId": "11111111-1111-4111-8111-111111111111", "assignedAt": "2026-05-28T14:30:00.000Z", "assignedToEmployee": { "id": "11111111-1111-4111-8111-111111111111", "name": "Alexia Morgan", "email": "alexia@example.com", "role": "MANAGER", "disabled": false, "createdAt": "2024-01-03T10:00:00.000Z" }, "updatedAt": "2026-05-28T14:30:00.000Z" } } } } }, "400": { "description": "Invalid request data", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "arrayWrapped": { "summary": "Zapier array wrapping is enabled", "value": { "success": false, "error": "Request body must be a single JSON object. If you are using Zapier, turn off \"Wrap in Array\"." } }, "typo": { "summary": "Wrong status field name", "value": { "success": false, "error": "Unknown field `conversationStatus`. Did you mean `conversionStatus`?" } }, "noUpdateFields": { "summary": "No update fields supplied", "value": { "success": false, "error": "At least one update field is required. Use one or more of: conversionStatus, notes, archived, assignedToUserId." } }, "disabledEmployee": { "summary": "Assignment target is disabled", "value": { "success": false, "error": "Assigned employee is disabled." } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "leadNotFound": { "value": { "success": false, "error": "Lead not found" } }, "phoneNotFound": { "value": { "success": false, "error": "Lead not found for provided phone" } } } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/_EXPERIMENTAL/v2/pub/leads/review-gathering": { "post": { "tags": [ "Experimental" ], "summary": "Trigger review gathering for a lead", "description": "Creates a review request associated with a lead. If a review request already exists\nfor the lead, the call is idempotent and returns the existing request with `isDuplicate: true`.\n\nIdentify the lead by providing either `leadId` or `phone` in the request body. If both are provided,\n`leadId` is used.\n", "security": [ { "ApiKeyAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadReviewRequestCreate" }, "examples": { "byLeadId": { "summary": "Trigger by leadId", "value": { "leadId": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" } }, "byPhone": { "summary": "Trigger by phone", "value": { "phone": "+15551234567" } } } } } }, "responses": { "200": { "description": "Review request already exists (idempotent)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadReviewRequestResponse" }, "example": { "success": true, "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "PENDING", "isDuplicate": true } } } } }, "201": { "description": "Review request created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadReviewRequestResponse" }, "example": { "success": true, "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "PENDING", "isDuplicate": false } } } } }, "400": { "description": "Invalid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "examples": { "missingIdentifier": { "summary": "Missing lead identifier", "value": { "success": false, "error": "You must provide either leadId or phone" } }, "invalidPhone": { "summary": "Invalid phone format", "value": { "success": false, "error": "Invalid phone number format" } } } } } }, "401": { "$ref": "#/components/responses/UnauthorizedError" }, "404": { "description": "Lead not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "success": false, "error": "Lead not found" } } } }, "500": { "$ref": "#/components/responses/InternalError" } } } }, "/v2/pub/inbox": { "get": { "tags": [ "Inbox" ], "x-previous-tags": [ "Experimental" ], "summary": "List the contact inbox", "operationId": "listPublicInbox", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Start here to discover recently active contacts or build a human-takeover queue with humanEscalation=open. Save clientId and use the contact message endpoint for current history. For a known contact, use clientId with view=all. Inbox activity is a discovery signal, not a lossless message feed.\n\n[Step-by-step CRM workflow](/guides/inbox-workflows/).\n\nOrders contacts by last indexed conversation activity (or inquiry/contact creation), then contact ID, descending. Internal notes and read/bookkeeping events do not advance activity. Defaults to unarchived contacts; view=all includes archives. Filters apply before pagination, AND across families and OR within repeated parameters (repeat the query key, not comma-separated values). Source uses inquiry acquisition source; originalSource uses latest resolved provenance; channel uses indexed message/call history or existing reply targets. Search covers contact names, phone and email only. aiState describes reply controls; an open escalation takes precedence as paused, and controlState preserves underlying mixed settings. These settings do not guarantee provider or global automation readiness. All booking predicates must match the same recorded booking. bookingOrigin=ai&bookingView=upcoming requires a confirmed BOOKED appointment in the future; requested external slots with UNKNOWN status do not qualify. recently-booked defaults to the preceding seven days using booking creation time, not appointment time. asOf fixes relative dates across pages. hasUpcomingAiBooking is null when no confirmed upcoming appointment exists but external AI booking status is unknown. Use unknownAiBookingCount and bookingCoverage rather than interpreting unknown as false. Follow nextCursor with unchanged filters. Signed tenant/filter-bound cursors expire after 24 hours. Pages are not a historical snapshot: deduplicate stable IDs and periodically reconcile. Requires inbox:read for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key independently of subscription sending eligibility. Unknown query fields are rejected. Date bounds are inclusive from and exclusive to. No sending, read markers, backfills, AI control or provider mutations occur.", "parameters": [ { "name": "limit", "in": "query", "description": "Page size: default 25, maximum 100.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "cursor", "in": "query", "description": "Opaque nextCursor from the previous page; keep all filters unchanged.", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 } }, { "name": "view", "in": "query", "description": "Defaults to inbox (unarchived contacts). Use archived for archived contacts or all for a complete CRM reconciliation.", "schema": { "type": "string", "enum": [ "inbox", "archived", "all" ], "default": "inbox" } }, { "name": "clientId", "in": "query", "description": "Contact UUID owned by the authenticated company. This is not a lead inquiry ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "search", "in": "query", "description": "Case-insensitive contact name, phone or email search. Verify the returned contact ID before saving a CRM association.", "schema": { "type": "string", "minLength": 1, "maxLength": 200 } }, { "name": "assignedTo", "in": "query", "description": "Repeat teammate UUIDs; cannot be combined with unassigned.", "schema": { "type": "array", "items": { "type": "string", "format": "uuid" }, "minItems": 1, "maxItems": 20 }, "style": "form", "explode": true }, { "name": "unassigned", "in": "query", "description": "true for unassigned, false for assigned contacts; cannot be combined with assignedTo.", "schema": { "type": "boolean" } }, { "name": "leadStatus", "in": "query", "description": "Repeat pipeline status values to match any selected status.", "schema": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 64 }, "minItems": 1, "maxItems": 20 }, "style": "form", "explode": true }, { "name": "source", "in": "query", "description": "Repeat acquisition sources to match any; independent of conversation channel.", "schema": { "type": "array", "items": { "type": "string", "enum": [ "WEBSITE_TEXTING", "WEBCHAT", "MISSED_CALL", "COLD_TEXT_INBOUND", "AI_HANDLED_CALL", "EMAIL_QUALIFICATION", "CALENDAR_BOOKING", "YELP_LEAD", "THUMBTACK_LEAD", "ANGI_LEAD", "GOOGLE_LSA_DIRECT", "META_MESSENGER" ] }, "minItems": 1, "maxItems": 20 }, "style": "form", "explode": true }, { "name": "originalSource", "in": "query", "description": "Repeat original-source labels from latest resolved inquiry provenance.", "schema": { "type": "array", "items": { "type": "string", "minLength": 1, "maxLength": 200 }, "minItems": 1, "maxItems": 20 }, "style": "form", "explode": true }, { "name": "channel", "in": "query", "description": "Conversation medium; independent of original lead source.", "schema": { "type": "array", "items": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat", "phone" ] }, "minItems": 1, "maxItems": 20 }, "style": "form", "explode": true }, { "name": "aiState", "in": "query", "description": "Filter effective reply controls: enabled, paused, mixed or unavailable. An open human escalation takes precedence as paused.", "schema": { "type": "string", "enum": [ "enabled", "paused", "mixed", "unavailable" ] } }, { "name": "humanEscalation", "in": "query", "description": "open finds contacts needing human attention; resolved finds resolved escalations; any includes either. An ordinary AI pause does not imply escalation.", "schema": { "type": "string", "enum": [ "open", "resolved", "any" ] } }, { "name": "bookingOrigin", "in": "query", "description": "Match booking provenance (AI, human, customer or unknown), not whether the contact has used AI.", "schema": { "type": "string", "enum": [ "ai", "human", "customer", "unknown" ] } }, { "name": "bookingState", "in": "query", "description": "Match a recorded booking status. UNKNOWN means external evidence does not establish current provider status.", "schema": { "type": "string", "enum": [ "BOOKED", "CANCELLED", "COMPLETED", "NO_SHOW", "UNKNOWN" ] } }, { "name": "bookingView", "in": "query", "description": "all requires a booking; upcoming requires a confirmed future appointment; recently-booked defaults to booking creation in the preceding seven days.", "schema": { "type": "string", "enum": [ "all", "upcoming", "recently-booked" ] } }, { "name": "appointmentStartFrom", "in": "query", "description": "Include confirmed/recorded appointment start times at or after this ISO timestamp. Distinct from booking creation time.", "schema": { "type": "string", "format": "date-time" } }, { "name": "appointmentStartTo", "in": "query", "description": "Exclude appointment start times at or after this ISO timestamp.", "schema": { "type": "string", "format": "date-time" } }, { "name": "bookedFrom", "in": "query", "description": "Include bookings created at or after this ISO timestamp. For recently-booked, omitting both bounds uses the preceding seven days.", "schema": { "type": "string", "format": "date-time" } }, { "name": "bookedTo", "in": "query", "description": "Exclude bookings created at or after this ISO timestamp.", "schema": { "type": "string", "format": "date-time" } }, { "name": "hasOpenBookingChangeRequest", "in": "query", "description": "Filter whether an open request exists, optionally of bookingChangeRequestType.", "schema": { "type": "boolean" } }, { "name": "bookingChangeRequestType", "in": "query", "description": "Match open requests of this type; combined with the boolean filter on the same request.", "schema": { "type": "string", "enum": [ "CANCEL", "RESCHEDULE" ] } }, { "name": "activityFrom", "in": "query", "description": "Include contacts whose last indexed activity/inquiry/contact creation is at or after this ISO timestamp. Not a lossless message-change watermark.", "schema": { "type": "string", "format": "date-time" } }, { "name": "activityTo", "in": "query", "description": "Exclude contacts whose last activity is at or after this ISO timestamp. Keep the same bounds through a traversal.", "schema": { "type": "string", "format": "date-time" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxPage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/conversation": { "get": { "tags": [ "Inbox" ], "x-previous-tags": [ "Experimental" ], "summary": "Read a contact conversation", "operationId": "getPublicInboxConversation", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Use this when opening a contact in your CRM. It combines current contact and automation state with bounded message/target previews; use the message list for ongoing synchronization.\n\n[Step-by-step CRM workflow](/guides/inbox-workflows/).\n\nReturns an allowlisted contact profile, AI/escalation and booking summary, up to 25 inquiry summaries, 10 recent stored messages and 25 send targets. Continue message and target previews using their nextCursor at the respective list endpoint. inquiriesHaveMore indicates a partial inquiry preview; the existing lead-inquiry API supplies full inquiry records. Use the combined timeline for messages, call summaries, bookings, escalations and activity; calls and bookings also have dedicated list/detail endpoints with separate scopes. version is a fingerprint of this bounded read, not a write precondition or a lossless synchronization token. No read markers are changed. Requires inbox:read for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key independently of subscription sending eligibility. Unknown query fields are rejected. Date bounds are inclusive from and exclusive to. No sending, read markers, backfills, AI control or provider mutations occur.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact UUID owned by the authenticated company. This is not a lead inquiry ID.", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxConversation" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/timeline": { "get": { "tags": [ "Inbox" ], "x-previous-tags": [ "Experimental" ], "summary": "Read a combined contact timeline", "operationId": "listPublicInboxTimeline", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Use this for a chronological CRM activity feed that combines conversation messages with calls, bookings and contact activity. Use the message-only endpoint when only inbox:read is granted.\n\n[Step-by-step CRM workflow](/guides/inbox-workflows/).\n\nRequires all of inbox:read, calls:read and bookings:read, even with a kind filter. Combines stored messages, call summaries, native/recorded bookings, escalation decisions, inquiry creation, booking-change requests and allowlisted indexed activity. Call and booking items show current saved state at record creation time, not every historical transition. Booking-change activity includes bookingChangeRequestId for the dedicated detail endpoint. coverage=stored_sources_and_index; this is not an exhaustive audit log or a live provider read. Raw provider payloads, internal notes, transcripts, recordings and attachments are excluded. Messages use the same IDs and text limits as the message list; use the body endpoint for full stored text. Orders by occurredAt and ID. Date bounds include createdFrom and exclude createdTo. Follow nextCursor with unchanged filters; cursors expire after 24 hours. Requires an active company/key. NULL and ALL_SCOPES grant all permissions. No sending, read markers, backfills or provider mutations occur. Unknown query fields are rejected. Responses are not historical snapshots; periodically reconcile.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact UUID owned by the authenticated company. This is not a lead inquiry ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 }, "description": "Records per page." }, { "name": "cursor", "in": "query", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 }, "description": "Opaque nextCursor from the previous page; retain endpoint and filters." }, { "name": "order", "in": "query", "schema": { "type": "string", "enum": [ "asc", "desc" ], "default": "desc" }, "description": "desc returns newest first; asc returns oldest first. Ordering uses occurrence time then opaque ID. Keep unchanged while following a cursor." }, { "name": "kind", "in": "query", "schema": { "type": "string", "enum": [ "message", "call", "booking", "escalation", "activity" ] }, "description": "Limit the combined timeline to messages, calls, bookings, escalations or activities. All three timeline read scopes remain required." }, { "name": "createdFrom", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Include records occurring at or after this ISO timestamp. For messages this filters occurredAt, not ingestedAt; late imports require periodic reconciliation." }, { "name": "createdTo", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Exclude records occurring at or after this ISO timestamp. Use a fixed upper bound while paging a polling window." } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxTimelinePage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/messages": { "post": { "tags": [ "Messages" ], "summary": "Send an immediate reply to a contact", "operationId": "sendPublicInboxMessage", "description": "Sends synchronously through the selected existing inbox channel. Requires messages:send; legacy NULL scopes and ALL_SCOPES include this permission. No queue, background send or automatic provider retry is created. First read the conversation and discover send-methods. Copy the chosen target's version into expectedConversationVersion. Always choose sendMethod, sendTargetId and aiReplies. An open human escalation blocks sending. Contact-wide takeover timers cannot be overridden here. off pauses the selected inquiry/conversation before sending; on enables ordinary paused controls without resolving escalations. Other threads are unchanged. The AI choice persists if dispatch is attempted and the provider outcome is uncertain. Every channel requires an active subscription/license and lead usage at or below its limit. SMS/email additionally enforce their hard billing-period limits against existing company usage. SMS requires the exact approved, active 10DLC sender. Final SMS content including compliance text must fit 600 characters; it is never silently truncated. Attachments are unsupported. accepted means the provider accepted the reply (Yelp: at least one configured webhook accepted it; webchat: stored for the widget), not recipient delivery. unknown means acceptance could not be confirmed. After a timeout or unknown response, repeat the identical request and Idempotency-Key to inspect its result; never retry with a fresh key. Results are retained in Redis for 24 hours; this is not an exactly-once guarantee across loss of that store or expiry. Rate limits apply across keys/channels. No more than three attempted API replies per contact without a new inbound message; unknown outcomes count. SMS/email recipient budgets also apply across duplicate contact IDs. Employee company/channel stops return 403 API_SENDING_PAUSED. See the Inbox workflow guide for limits.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }, { "name": "Idempotency-Key", "in": "header", "required": true, "description": "Unique stable identifier for one intended message; reuse only with identical content.", "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{16,128}$" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": false, "required": [ "sendMethod", "sendTargetId", "aiReplies", "expectedConversationVersion", "text" ], "properties": { "sendMethod": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat" ] }, "sendTargetId": { "type": "string", "minLength": 1, "maxLength": 500 }, "aiReplies": { "type": "string", "enum": [ "off", "on" ] }, "expectedConversationVersion": { "type": "string", "minLength": 1, "maxLength": 500 }, "text": { "type": "string", "minLength": 1, "maxLength": 5000 }, "externalReference": { "type": "string", "maxLength": 200, "description": "Optional CRM reference saved with existing message audit metadata for support lookup; not a public search filter." } } } } } }, "responses": { "200": { "description": "Immediate outcome; inspect status. Unknown is not safe to resend with a new key.", "content": { "application/json": { "schema": { "type": "object", "required": [ "success", "data" ], "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "type": "object", "required": [ "requestId", "status", "sendMethod", "sendTargetId", "aiReplies", "acceptedAt", "providerMessageId" ], "properties": { "requestId": { "type": "string", "format": "uuid" }, "status": { "type": "string", "enum": [ "accepted", "unknown" ] }, "sendMethod": { "type": "string" }, "sendTargetId": { "type": "string" }, "aiReplies": { "type": "string", "enum": [ "off", "on" ], "nullable": true }, "acceptedAt": { "type": "string", "format": "date-time", "nullable": true }, "providerMessageId": { "type": "string", "nullable": true } } } } } } } }, "400": { "description": "Missing or invalid request fields or idempotency key." }, "401": { "description": "Invalid or inactive API key." }, "403": { "description": "Missing messages:send scope, inactive subscription, lead limit exceeded, or SMS/email limit exceeded." }, "404": { "description": "Contact or target unavailable to this company." }, "409": { "description": "Stale target, open escalation, protected AI control, idempotency conflict, or no-inbound reply budget exhausted." }, "422": { "description": "Invalid channel/content or failure before provider dispatch." }, "429": { "description": "Send budget or company concurrency limit reached.", "headers": { "Retry-After": { "description": "Minimum seconds before another attempt.", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, billing, limiter or preparation unavailable. For transport timeouts, reuse the identical idempotency key to inspect the result." } } }, "get": { "tags": [ "Messages" ], "x-previous-tags": [ "Experimental" ], "summary": "List contact messages", "operationId": "listPublicInboxMessages", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "To check the latest messages, use order=desc&limit=25; inspect direction in returned items for incoming customer messages (there is no direction query filter). To watch this contact, poll overlapping createdFrom/createdTo windows with order=asc, follow every nextCursor with unchanged filters, and upsert by message ID. Start each new poll without the previous cursor and periodically reconcile older history.\n\n[Step-by-step CRM workflow](/guides/inbox-workflows/).\n\nReads existing stored SMS, email, Facebook Messenger, Google LSA, Thumbtack, Yelp and webchat records, merged with index-only messages. coverage=stored_sources_and_index. No backfill, provider refresh or write occurs. Duplicate source/index identities collapse; equal text remains distinct. IDs are opaque strings (message:hash or history:bigint); retain them unchanged. Legacy messages without IDs use stored array positions; reconcile if a thread is reordered. Orders by occurredAt and ID, ascending or descending. createdFrom/createdTo filter occurrence time with inclusive/exclusive bounds. Follow nextCursor with unchanged filters; cursors expire after 24 hours. textCompleteness reports complete, preview, truncated or unavailable. List text caps at 20000 Unicode code points; retrieve remaining text using the message body endpoint. HTML-only email is converted to plain text. Index-only email may remain a preview when the original is absent. Unknown outbound authors stay unknown. Attachments and raw provider payloads are excluded. Requires inbox:read; NULL and ALL_SCOPES retain access. No subscription sending eligibility is required for reads.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact UUID owned by the authenticated company. This is not a lead inquiry ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Page size: default 25, maximum 100.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "cursor", "in": "query", "description": "Opaque nextCursor from the previous page; keep all filters unchanged.", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 } }, { "name": "order", "in": "query", "description": "desc returns newest first; asc returns oldest first. Ordering uses occurrence time then opaque ID. Keep unchanged while following a cursor.", "schema": { "type": "string", "enum": [ "asc", "desc" ], "default": "desc" } }, { "name": "channel", "in": "query", "description": "Conversation medium; independent of original lead source.", "schema": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat", "phone" ] } }, { "name": "inquiryId", "in": "query", "description": "Limit messages to this linked lead inquiry UUID. Omit to read all conversations for the contact.", "schema": { "type": "string", "format": "uuid" } }, { "name": "createdFrom", "in": "query", "description": "Include records occurring at or after this ISO timestamp. For messages this filters occurredAt, not ingestedAt; late imports require periodic reconciliation.", "schema": { "type": "string", "format": "date-time" } }, { "name": "createdTo", "in": "query", "description": "Exclude records occurring at or after this ISO timestamp. Use a fixed upper bound while paging a polling window.", "schema": { "type": "string", "format": "date-time" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxMessagesPage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/messages/{messageId}/body": { "get": { "tags": [ "Messages" ], "x-previous-tags": [ "Experimental" ], "summary": "Read complete stored message text", "operationId": "getPublicInboxMessageBody", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Use the messageId returned by the message list or timeline when textCompleteness=truncated. Concatenate text pages until nextCursor is null.\n\n[Step-by-step CRM workflow](/guides/inbox-workflows/).\n\nRequires inbox:read. Returns stored message text in plain-text pages. limit counts Unicode code points (default 10000; maximum 20000); concatenate pages in cursor order. HTML-only email is converted to text, without embedded assets or scripts. availability=preview means only the index preview survives; unavailable means no saved text. totalCharacters describes the available stored text, not missing original content. Signed cursors bind company, contact, message and content revision and expire after 24 hours. Restart without a cursor on 409 MESSAGE_CHANGED. Requires an active company/key. NULL and ALL_SCOPES grant all permissions. No sending, read markers, backfills or provider mutations occur. Unknown query fields are rejected. Responses are not historical snapshots; periodically reconcile.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact UUID owned by the authenticated company. This is not a lead inquiry ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "messageId", "in": "path", "required": true, "description": "Opaque ID returned by messages or timeline.", "schema": { "type": "string", "pattern": "^(?:message:[0-9a-f]{32}(?::[1-9][0-9]*)?|history:[1-9][0-9]*)$" } }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 20000, "default": 10000 }, "description": "Unicode code points per page." }, { "name": "cursor", "in": "query", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 }, "description": "Opaque nextCursor from the previous page; retain endpoint and filters." } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxMessageBody" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "409": { "description": "Stored message changed; restart without the cursor.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/send-methods": { "get": { "tags": [ "Messages" ], "x-previous-tags": [ "Experimental" ], "summary": "Discover contact reply methods", "operationId": "listPublicInboxSendMethods", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Before preparing a reply, refresh this list and the contact conversation state. Select a returned sendMethod and exact sendTargetId; a contact can have multiple targets on the same medium. Inspect blockers and present an explicit AI on/off choice from allowedAiReplies. Use POST on the contact messages endpoint to send immediately after selecting a target.\n\n[Step-by-step CRM workflow](/guides/inbox-workflows/).\n\nEnumerates existing reply targets across SMS, email, Facebook Messenger, Google LSA, Thumbtack, Yelp and webchat. A method without existing targets reports NO_REPLY_TARGET. Target enabled describes stored eligibility at asOf, including strict billing, opt-outs, approved active 10DLC SMS sender, connection and reply-window checks; it is not permission to send. sendingAvailable indicates send availability; an employee company-wide stop disables it. Targets include API_SENDING_PAUSED when company/channel replies are stopped. Google LSA refreshes and checks its provider history during sending. Open escalation blocks targets and leaves allowedAiReplies empty. Sending requires an explicit off/on choice from allowedAiReplies, plus sendMethod and sendTargetId. No default channel, fallback sender or implicit SMS bridge is created. Targets are ordered by thread update time and target ID descending. Copy the selected target version into expectedConversationVersion when sending; dispatch revalidates eligibility. Attachments are unsupported. Follow nextCursor with unchanged filters. Signed tenant/filter-bound cursors expire after 24 hours. Pages are not a historical snapshot: deduplicate stable IDs and periodically reconcile. Requires inbox:read for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key independently of subscription sending eligibility. Unknown query fields are rejected. Date bounds are inclusive from and exclusive to. No sending, read markers, backfills, AI control or provider mutations occur.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact UUID owned by the authenticated company. This is not a lead inquiry ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "limit", "in": "query", "description": "Page size: default 25, maximum 100.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "cursor", "in": "query", "description": "Opaque nextCursor from the previous page; keep all filters unchanged.", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 } }, { "name": "sendMethod", "in": "query", "description": "Limit reply targets to one medium. Omit to discover all seven supported methods. Multiple targets can exist on the same medium.", "schema": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat" ] } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxSendTargetsPage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/calls": { "get": { "tags": [ "Calls" ], "x-previous-tags": [ "Experimental" ], "summary": "List contact calls", "operationId": "listPublicInboxCalls", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "calls:read. Includes AI calls, outbound click-to-call and missed calls/voicemails. Namespaced IDs refer to application records, not provider call IDs. Summaries omit transcripts, recordings and raw provider data. createdFrom/createdTo filter record creation time, not call start time. Ordered by creation time descending, then public ID descending, preserving database timestamp precision. Follow nextCursor with unchanged filters; deduplicate IDs and periodically reconcile because pages are not a historical snapshot. Requires the stated scope for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key, independently of subscription sending eligibility. Unknown filters are rejected. Lists use signed, tenant/resource/filter-bound cursors that expire after 24 hours. Date ranges include the lower bound and exclude the upper bound. No messaging, AI control, read-marker, archive, booking or provider mutations occur.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "kind", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "enum": [ "ai", "outbound", "missed" ] } }, { "name": "createdFrom", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "format": "date-time" } }, { "name": "createdTo", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "format": "date-time" } }, { "name": "limit", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "cursor", "in": "query", "description": "Opaque nextCursor from the previous page.", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxCallsPage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter, storage or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/calls/{callId}": { "get": { "tags": [ "Calls" ], "x-previous-tags": [ "Experimental" ], "summary": "Get call details", "operationId": "getPublicInboxCall", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "calls:read. Returns current stored metadata and summary. hasTranscript/hasRecording indicate saved content or links; they do not guarantee media availability. Missing start time/duration remains null. Requires the stated scope for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key, independently of subscription sending eligibility. Unknown filters are rejected. Lists use signed, tenant/resource/filter-bound cursors that expire after 24 hours. Date ranges include the lower bound and exclude the upper bound. No messaging, AI control, read-marker, archive, booking or provider mutations occur.", "parameters": [ { "name": "callId", "in": "path", "required": true, "description": "Namespaced ID returned by the list endpoint.", "schema": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxCall" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter, storage or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/calls/{callId}/transcript": { "get": { "tags": [ "Calls" ], "x-previous-tags": [ "Experimental" ], "summary": "Read a call transcript", "operationId": "getPublicInboxCallTranscript", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "calls:read. Returns stored plain text in pages of Unicode code points (limit defaults to 10000, maximum 20000). No invented speakers or timings. not_available means no nonblank saved transcript, not a promise that processing will finish. A transcript changed during pagination returns 409 TRANSCRIPT_CHANGED; restart without the cursor. Requires the stated scope for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key, independently of subscription sending eligibility. Unknown filters are rejected. Lists use signed, tenant/resource/filter-bound cursors that expire after 24 hours. Date ranges include the lower bound and exclude the upper bound. No messaging, AI control, read-marker, archive, booking or provider mutations occur.", "parameters": [ { "name": "callId", "in": "path", "required": true, "description": "Namespaced ID returned by the list endpoint.", "schema": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" } }, { "name": "limit", "in": "query", "description": "Maximum Unicode code points per page.", "schema": { "type": "integer", "minimum": 1, "maximum": 20000, "default": 10000 } }, { "name": "cursor", "in": "query", "description": "Opaque nextCursor from the previous page.", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxTranscript" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "409": { "description": "TRANSCRIPT_CHANGED: restart without cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter, storage or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/calls/{callId}/recordings": { "get": { "tags": [ "Calls" ], "x-previous-tags": [ "Experimental" ], "summary": "Get temporary recording download links", "operationId": "getPublicInboxCallRecordings", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Requires calls:media:read for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Returns five-minute automations proxy links for allowlisted archived recordings when configured. GET and HEAD downloads recheck the key, company, media scope and current call ownership on every request. The proxy supports single byte ranges; downloads count against the API read rate budget. It permits two concurrent downloads per company and eight per automations instance, with a 100 MiB object limit and a 60-second request timeout. Fetch a new link after expiry. proxy_pending means proxy issuance is not configured; not_available means no saved recording; not_archived means only unsupported/provider links exist. Link issuance does not verify file existence. No bucket URLs, provider URLs or S3 signatures are exposed. Legacy origin storage can remain public independently of proxy expiry; private origin rollout is a separate operational requirement. Reads require an active company/key, independently of subscription sending eligibility. Unknown query parameters are rejected. Foreign or missing calls return 404.", "parameters": [ { "name": "callId", "in": "path", "required": true, "description": "Namespaced ID returned by the list endpoint.", "schema": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxRecordings" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter, storage or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/clients/{clientId}/bookings": { "get": { "tags": [ "Bookings" ], "x-previous-tags": [ "Experimental" ], "summary": "List contact bookings", "operationId": "listPublicInboxBookings", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "bookings:read. Combines native appointments with recorded successful booking actions across all contact inquiries. origin=ai requires voice-agent source or successful booking-action evidence. Native matches and repeated external IDs are deduplicated. Filters apply before pagination: bookedFrom/bookedTo concern booking creation, appointmentStartFrom/appointmentStartTo concern scheduled time. view=upcoming uses the current clock and includes only BOOKED records with timeSource=appointment. External UNKNOWN requested slots are excluded. External status is UNKNOWN with statusFreshness=last_observed; startAt/endAt reflect the last requested slot, not a fresh CRM lookup. Malformed/missing external times are null. Coverage does not include bookings absent from both local appointments and recorded actions. Ordered by creation time descending, then public ID descending, preserving database timestamp precision. Follow nextCursor with unchanged filters; deduplicate IDs and periodically reconcile because pages are not a historical snapshot. Requires the stated scope for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key, independently of subscription sending eligibility. Unknown filters are rejected. Lists use signed, tenant/resource/filter-bound cursors that expire after 24 hours. Date ranges include the lower bound and exclude the upper bound. No messaging, AI control, read-marker, archive, booking or provider mutations occur.", "parameters": [ { "name": "clientId", "in": "path", "required": true, "description": "Contact ID.", "schema": { "type": "string", "format": "uuid" } }, { "name": "origin", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "enum": [ "ai", "human", "customer", "unknown" ] } }, { "name": "provider", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "minLength": 1, "maxLength": 64 } }, { "name": "view", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "enum": [ "all", "upcoming" ], "default": "all" } }, { "name": "appointmentStartFrom", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "format": "date-time" } }, { "name": "appointmentStartTo", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "format": "date-time" } }, { "name": "bookedFrom", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "format": "date-time" } }, { "name": "bookedTo", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "string", "format": "date-time" } }, { "name": "limit", "in": "query", "description": "Optional filter or page bound.", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "cursor", "in": "query", "description": "Opaque nextCursor from the previous page.", "schema": { "type": "string", "minLength": 1, "maxLength": 3000 } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxBookingsPage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter, storage or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/bookings/{bookingId}": { "get": { "tags": [ "Bookings" ], "x-previous-tags": [ "Experimental" ], "summary": "Get booking details", "operationId": "getPublicInboxBooking", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "bookings:read. Use the namespaced id returned by list bookings. Returns origin evidence, times, status freshness and pending change-request count. External status stays UNKNOWN without current provider evidence. Resolving a change request does not update provider status. Requires the stated scope for restricted keys; NULL and ALL_SCOPES grant all current and future scopes. Reads require an active company/key, independently of subscription sending eligibility. Unknown filters are rejected. Lists use signed, tenant/resource/filter-bound cursors that expire after 24 hours. Date ranges include the lower bound and exclude the upper bound. No messaging, AI control, read-marker, archive, booking or provider mutations occur.", "parameters": [ { "name": "bookingId", "in": "path", "required": true, "description": "Namespaced ID returned by the list endpoint.", "schema": { "type": "string", "pattern": "^(?:appointment:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|booking-action:[1-9][0-9]{0,18})$" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxBooking" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Authorization, rate limiter, storage or read dependency temporarily unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/appointment-change-requests": { "get": { "tags": [ "Bookings" ], "x-previous-tags": [ "Experimental" ], "summary": "List booking change requests", "operationId": "listPublicInboxBookingChanges", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Inbox API preview using existing active company API keys after deployment. Requires bookings:read for restricted keys; null or ALL_SCOPES grants all current and future scopes. New keys default to ALL_SCOPES. Deployment requires the scopes column on the existing company key table. Returns cancellation/reschedule requests, newest first by creation time and ID. Includes requests without a linked contact. Filter with clientId for one contact. Unknown filters are rejected. Changes can occur while paging; periodically refresh the first page to reconcile statuses.", "parameters": [ { "name": "status", "in": "query", "schema": { "type": "string", "enum": [ "REQUESTED", "COMPLETED", "DECLINED", "SUPERSEDED", "ALL" ], "default": "REQUESTED" }, "description": "Filter or page option: status." }, { "name": "clientId", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Filter or page option: clientId." }, { "name": "requestType", "in": "query", "schema": { "type": "string", "enum": [ "CANCEL", "RESCHEDULE" ] }, "description": "Filter or page option: requestType." }, { "name": "bookingProvider", "in": "query", "schema": { "type": "string", "maxLength": 64 }, "description": "Filter or page option: bookingProvider." }, { "name": "createdFrom", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Inclusive lower creation bound." }, { "name": "createdTo", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "Exclusive upper creation bound." }, { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }, "description": "Filter or page option: limit." }, { "name": "cursor", "in": "query", "schema": { "type": "string", "maxLength": 3000 }, "description": "Opaque company/filter-bound cursor, valid for 24 hours." } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/PublicInboxChangeRequest" } }, "nextCursor": { "type": "string", "nullable": true } }, "required": [ "items", "nextCursor" ], "additionalProperties": false } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Dependency or billing state unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/appointment-change-requests/{requestId}": { "get": { "tags": [ "Bookings" ], "x-previous-tags": [ "Experimental" ], "summary": "Get booking change request", "operationId": "getPublicInboxBookingChange", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Inbox API preview using existing active company API keys after deployment. Requires bookings:read for restricted keys; null or ALL_SCOPES grants all current and future scopes. New keys default to ALL_SCOPES. Deployment requires the scopes column on the existing company key table. Read request status and requested slot. COMPLETED describes the request workflow, not proof that this endpoint changed a provider appointment.", "parameters": [ { "name": "requestId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxChangeRequest" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Dependency or billing state unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } }, "/v2/pub/account/usage": { "get": { "tags": [ "Usage" ], "x-previous-tags": [ "Experimental" ], "summary": "Get Inbox API usage and limits", "operationId": "getPublicInboxUsage", "externalDocs": { "description": "Watch contacts and prepare replies", "url": "https://api-docs.leadtruffle.com/guides/inbox-workflows/" }, "description": "Inbox API preview using existing active company API keys after deployment. Requires usage:read for restricted keys; null or ALL_SCOPES grants all current and future scopes. New keys default to ALL_SCOPES. Separate SMS/email plan limits with nullable subscription or license overrides. Deployment requires new columns on existing tables. SMS units are segments. Billing eligibility is not send permission: sendingAvailable reports endpoint availability; dispatch rechecks eligibility and hard limits. Expired subscriptions can still inspect usage. Missing billing facts return 503, never zero usage.", "parameters": [], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/PublicInboxUsage" } }, "required": [ "success", "data" ], "additionalProperties": false } } } }, "400": { "description": "Invalid query, ID or cursor", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "401": { "description": "Invalid API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "403": { "description": "Missing scope or key no longer authorized for this company", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "404": { "description": "Requested resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } }, "429": { "description": "Read rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } }, "headers": { "Retry-After": { "description": "Seconds before retrying", "schema": { "type": "integer", "minimum": 1 } } } }, "503": { "description": "Dependency or billing state unavailable", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PublicInboxError" } } } } } } } }, "components": { "schemas": { "WebhookPayload": { "type": "object", "description": "Payload that will be sent to your webhook endpoints when a lead conversation is completed", "properties": { "type": { "type": "string", "enum": [ "conversation_completed" ], "example": "conversation_completed" }, "clientId": { "type": "string", "example": "456" }, "leadId": { "type": "string", "format": "uuid", "example": "123" }, "companyId": { "type": "string", "format": "uuid", "example": "789" }, "leadQualificationStatus": { "type": "string", "enum": [ "COMPLETED", "INCOMPLETE" ], "example": "COMPLETED" }, "leadInformation": { "type": "object", "properties": { "name": { "type": "string", "example": "John Doe" }, "firstName": { "type": "string", "example": "John" }, "lastName": { "type": "string", "example": "Doe" }, "email": { "type": "string", "format": "email", "example": "john@example.com" }, "phone": { "type": "string", "example": "+18001234567" }, "additionalData": { "type": "object", "properties": { "source": { "type": "string", "example": "popup" }, "message": { "type": "string", "example": "I need help with..." } } } } }, "trackingData": { "type": "object", "properties": { "source": { "type": "string", "example": "popup" }, "utm_source": { "type": "string", "example": "google" }, "utm_medium": { "type": "string", "example": "cpc" }, "utm_campaign": { "type": "string", "example": "home_renovation" }, "utm_term": { "type": "string", "example": "home_renovation_cost" }, "utm_content": { "type": "string", "example": "home_renovation_cost" }, "gclid": { "type": "string", "example": "1234567890" }, "fbclid": { "type": "string", "example": "1234567890" }, "msclkid": { "type": "string", "example": "1234567890" }, "ttclid": { "type": "string", "example": "1234567890" }, "snapcid": { "type": "string", "example": "1234567890" }, "gbraid": { "type": "string", "nullable": true, "example": "gbraid_example" }, "wbraid": { "type": "string", "nullable": true, "example": "wbraid_example" }, "gad_source": { "type": "string", "nullable": true, "example": "google_ads" }, "igshid": { "type": "string", "nullable": true, "example": "ig_share_id" }, "gclsrc": { "type": "string", "nullable": true, "example": "aw.ds" }, "srsltid": { "type": "string", "nullable": true, "example": "search_result_id" }, "ga_client_id": { "type": "string", "nullable": true, "description": "Google Analytics Client ID", "example": "123456789.987654321" }, "ga_session_id": { "type": "string", "nullable": true, "description": "Google Analytics Session ID", "example": "1700000000" }, "hubspotutk": { "type": "string", "nullable": true, "example": "1234567890" }, "pageInfo": { "type": "object", "properties": { "title": { "type": "string", "nullable": true, "example": "Home - Best HVAC Services" }, "referrer": { "type": "string", "nullable": true, "example": "https://www.google.com/" }, "currentUrl": { "type": "string", "nullable": true, "example": "http://localhost:3002/index.html" } } } } }, "qualifyingData": { "type": "object", "description": "Dynamic fields collected during the conversation", "additionalProperties": true, "example": { "example_budget": "$5000", "example_timeline": "Within 3 months", "example_projectType": "Home Renovation" } }, "commonFields": { "type": "object", "description": "Standardized fields extracted from the conversation", "properties": { "fullAddress": { "type": "string", "example": "111 main st, Austin TX 73301" }, "address": { "type": "string", "example": "111 main st" }, "zipcode": { "type": "string", "example": "73301" }, "state": { "type": "string", "example": "TX" }, "city": { "type": "string", "example": "Austin" }, "country": { "type": "string", "example": "US" }, "isHomeowner": { "type": "boolean", "example": true }, "customerName": { "type": "string", "example": "John" } } }, "qualifyingDataSummary": { "type": "string", "example": "The client lives in a 3 bedroom house.\\nHas a budget of $2000.\\nzipcode is 45150." }, "contactReason": { "type": "string", "example": "Client is interested in a home renovation project..." }, "timestamp": { "type": "string", "format": "date-time", "example": "2024-01-01T00:00:00Z" }, "isRepeatLead": { "type": "boolean", "description": "Indicates if this lead has previously submitted a form or contacted the company", "example": false }, "messageHistory": { "type": "array", "items": { "type": "object", "properties": { "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "name": { "type": "string", "example": "AI Agent" }, "message": { "type": "string", "example": "How can we help you..." }, "date": { "type": "string", "format": "date-time", "example": "2024-01-01T00:00:00Z" } } } }, "userMedia": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "example": "image/jpeg" }, "url": { "type": "string", "example": "https://tooldesk-public-user-uploads.s3.us-west-2.amazonaws.com/email-assets/leadtruffle-Wordmark-white.png" } } } } } }, "LeadList": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "leads": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookPayload" }, "description": "List of leads, limited to a maximum of 10 per request" }, "hasMore": { "type": "boolean", "description": "Indicates if there are more results available. To fetch the next page, use the oldest lead's timestamp as the 'before' parameter." } } } } }, "Error": { "type": "object", "properties": { "success": { "type": "boolean", "example": false }, "error": { "type": "string", "example": "Invalid API key" } } }, "WebhookList": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "chatWidgetWebhooks": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "List of webhook URLs for chat widget lead completion" }, "missedCallWebhooks": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "List of webhook URLs for missed call lead completion" } } } } }, "WebhookResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "webhooks": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "Updated list of webhook URLs" } } } } }, "ClientUpsertRequest": { "type": "object", "required": [ "phone" ], "properties": { "phone": { "type": "string", "description": "Phone number in E.164 format or US national format (e.g., +18001234567 or 8001234567)", "example": "8001234567" }, "firstName": { "type": "string", "example": "John" }, "lastName": { "type": "string", "example": "Doe" }, "email": { "type": "string", "format": "email", "example": "john.doe@example.com" }, "address1": { "type": "string", "example": "123 Main St" }, "address2": { "type": "string", "example": "Unit 456" }, "city": { "type": "string", "example": "Austin" }, "state": { "type": "string", "example": "TX" }, "zip": { "type": "string", "example": "78701" }, "country": { "type": "string", "example": "US" } } }, "Client": { "type": "object", "description": "Client record data", "properties": { "id": { "type": "string", "format": "uuid", "example": "415f2b29-39e6-4182-9d6f-ec2d817f01c2" }, "companyId": { "type": "string", "format": "uuid", "example": "f096f9e3-001d-49ac-864c-3d73453bbe08" }, "primaryPhone": { "type": "string", "example": "+17345520800", "description": "Phone number in E.164 format" }, "firstName": { "type": "string", "nullable": true, "example": "John" }, "lastName": { "type": "string", "nullable": true, "example": "Doe" }, "primaryEmail": { "type": "string", "nullable": true, "example": "john.doe@example.com" }, "address": { "type": "string", "nullable": true, "example": "123 Main St" }, "address2": { "type": "string", "nullable": true, "example": "Unit 456" }, "city": { "type": "string", "nullable": true, "example": "Austin" }, "state": { "type": "string", "nullable": true, "example": "TX" }, "zipCode": { "type": "string", "nullable": true, "example": "78701" }, "country": { "type": "string", "nullable": true, "example": "US" }, "isLead": { "type": "boolean", "example": true }, "leadStatus": { "type": "string", "nullable": true, "example": "NEW" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } }, "ClientUpsertResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/Client" }, "action": { "type": "string", "enum": [ "CREATED", "UPDATED", "UNCHANGED" ], "example": "CREATED", "description": "Indicates whether the client was created, updated, or left unchanged" } } }, "WebhookV2": { "type": "object", "description": "V2 Webhook configuration object", "properties": { "id": { "type": "string", "format": "uuid", "example": "550e8400-e29b-41d4-a716-446655440000" }, "companyId": { "type": "string", "format": "uuid", "example": "f096f9e3-001d-49ac-864c-3d73453bbe08" }, "eventType": { "type": "string", "enum": [ "CONVERSATION_COMPLETED", "MESSAGE_REPLY", "LEAD_CREATED", "LEAD_STATUS_CHANGED", "CLIENT_STATUS_CHANGED", "NEW_APPOINTMENT", "YELP_MESSAGE_OUTBOUND" ], "example": "LEAD_CREATED", "description": "Type of event that triggers this webhook. LEAD_STATUS_CHANGED is a legacy lead pipeline event; use CLIENT_STATUS_CHANGED for the new Contacts/client pipeline." }, "targetUrl": { "type": "string", "format": "uri", "example": "https://api.example.com/webhooks/leads", "description": "URL where webhook payloads will be sent" }, "enabled": { "type": "boolean", "example": true, "description": "Whether the webhook is active" }, "failedAttempts": { "type": "integer", "example": 0, "description": "Number of consecutive failed delivery attempts" }, "lastFailedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of last failed delivery attempt" }, "lastSuccessAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of last successful delivery" }, "integrationSource": { "type": "string", "example": "API", "description": "Source that created this webhook" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } }, "WebhookV2ListResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookV2" } } } }, "WebhookV2CreateRequest": { "type": "object", "required": [ "eventType", "url" ], "properties": { "eventType": { "type": "string", "enum": [ "CONVERSATION_COMPLETED", "MESSAGE_REPLY", "LEAD_CREATED", "LEAD_STATUS_CHANGED", "CLIENT_STATUS_CHANGED", "NEW_APPOINTMENT", "YELP_MESSAGE_OUTBOUND" ], "example": "LEAD_CREATED", "description": "Type of event that triggers this webhook:\n- `CONVERSATION_COMPLETED`: AI qualification finished\n- `MESSAGE_REPLY`: Lead replied via SMS\n- `LEAD_CREATED`: New lead created\n- `LEAD_STATUS_CHANGED`: Legacy lead pipeline status changed. Use only for accounts still using the legacy Leads system.\n- `CLIENT_STATUS_CHANGED`: Contact Status changed in the new Contacts/client pipeline\n- `NEW_APPOINTMENT`: Calendar booking created\n- `YELP_MESSAGE_OUTBOUND`: Send message to Yelp via Zapier\n" }, "url": { "type": "string", "format": "uri", "maxLength": 1000, "example": "https://api.example.com/webhooks/leads", "description": "URL where webhook payloads will be sent via HTTP POST" }, "secret": { "type": "string", "maxLength": 200, "nullable": true, "description": "Optional shared secret used to sign webhook deliveries with HMAC SHA-256. When set, deliveries include x-leadtruffle-signature and x-leadtruffle-timestamp headers." } } }, "WebhookV2Response": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "$ref": "#/components/schemas/WebhookV2" } } }, "WebhookV2UpdateRequest": { "type": "object", "properties": { "eventType": { "type": "string", "enum": [ "CONVERSATION_COMPLETED", "MESSAGE_REPLY", "LEAD_CREATED", "LEAD_STATUS_CHANGED", "CLIENT_STATUS_CHANGED", "NEW_APPOINTMENT", "YELP_MESSAGE_OUTBOUND" ], "example": "LEAD_CREATED", "description": "Type of event that triggers this webhook. LEAD_STATUS_CHANGED is a legacy lead pipeline event; use CLIENT_STATUS_CHANGED for the new Contacts/client pipeline." }, "url": { "type": "string", "format": "uri", "maxLength": 1000, "example": "https://api.example.com/webhooks/leads", "description": "URL where webhook payloads will be sent via HTTP POST" }, "secret": { "type": "string", "maxLength": 200, "nullable": true, "description": "Optional shared secret used to sign webhook deliveries with HMAC SHA-256. Send null or an empty value to remove signing." }, "enabled": { "type": "boolean", "description": "Whether the webhook is active" }, "clearErrors": { "type": "boolean", "description": "Clear any existing error history for this webhook" } } }, "MissedCallWebhookV2": { "type": "object", "description": "Missed call information in V2 webhook payloads", "properties": { "id": { "type": "string", "format": "uuid" }, "originalDialedNumber": { "type": "string", "nullable": true }, "inboundCaller": { "type": "string", "nullable": true }, "voicemailFileLink": { "type": "string", "nullable": true }, "voicemailTranscription": { "type": "string", "nullable": true }, "voicemailSummary": { "type": "string", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" } } }, "AIHandledCallWebhookV2": { "type": "object", "description": "AI handled call information in V2 webhook payloads", "properties": { "id": { "type": "string", "format": "uuid" }, "transcriptSummary": { "type": "string", "nullable": true }, "status": { "type": "string" }, "fullTranscript": { "type": "string", "nullable": true }, "inboundCaller": { "type": "string" }, "originalDialedNumber": { "type": "string", "nullable": true }, "durationSeconds": { "type": "integer", "nullable": true }, "recordingUrl": { "type": "string", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" } } }, "WebhookV2PayloadLeadCreated": { "type": "object", "description": "V2 Webhook payload for LEAD_CREATED events", "properties": { "eventType": { "type": "string", "enum": [ "LEAD_CREATED" ], "example": "LEAD_CREATED" }, "eventTypeDetails": { "type": "string", "enum": [ "NEW_MISSED_CALL", "NEW_AI_CALL_HANDLED", "NEW_CHAT_WIDGET_SUBMISSION", "NEW_ANGI_LEAD", "NEW_CALENDAR_BOOKING", "NEW_INBOUND_EMAIL", "NEW_EMAIL_QUALIFICATION", "NEW_YELP_LEAD", "NEW_THUMBTACK_LEAD", "NEW_GOOGLE_LSA_LEAD" ], "example": "NEW_CHAT_WIDGET_SUBMISSION" }, "leadId": { "type": "string", "format": "uuid" }, "clientId": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "name": { "type": "string", "nullable": true, "example": "John Doe" }, "firstName": { "type": "string", "nullable": true, "example": "John" }, "lastName": { "type": "string", "nullable": true, "example": "Doe" }, "email": { "type": "string", "nullable": true, "example": "john@example.com" }, "phone": { "type": "string", "example": "+18001234567" }, "message": { "type": "string", "nullable": true, "example": "I need help with HVAC repair" }, "timestamp": { "type": "string", "format": "date-time" }, "isRepeatLead": { "type": "boolean" }, "qualificationSource": { "type": "string", "enum": [ "MISSED_CALL", "WEBSITE_TEXTING", "WEBCHAT", "COLD_TEXT_INBOUND", "AI_HANDLED_CALL", "CALENDAR_BOOKING", "INBOUND_EMAIL", "ANGI_LEAD", "EMAIL_QUALIFICATION", "YELP_LEAD", "THUMBTACK_LEAD", "GOOGLE_LSA_DIRECT" ], "description": "Source channel for the lead's qualification journey. Use this to differentiate between inbound channels like email, chat/SMS, third-party marketplaces, and AI handled calls.\n" }, "isManualTakeoverEnabled": { "type": "boolean" }, "trackingData": { "type": "object", "properties": { "source": { "type": "string", "nullable": true }, "utm_source": { "type": "string", "nullable": true }, "utm_medium": { "type": "string", "nullable": true }, "utm_campaign": { "type": "string", "nullable": true }, "utm_term": { "type": "string", "nullable": true }, "utm_content": { "type": "string", "nullable": true }, "gclid": { "type": "string", "nullable": true }, "fbclid": { "type": "string", "nullable": true }, "msclkid": { "type": "string", "nullable": true }, "ttclid": { "type": "string", "nullable": true }, "snapcid": { "type": "string", "nullable": true }, "gbraid": { "type": "string", "nullable": true }, "wbraid": { "type": "string", "nullable": true }, "gad_source": { "type": "string", "nullable": true }, "igshid": { "type": "string", "nullable": true }, "gclsrc": { "type": "string", "nullable": true }, "srsltid": { "type": "string", "nullable": true }, "ga_client_id": { "type": "string", "nullable": true }, "ga_session_id": { "type": "string", "nullable": true }, "hubspotutk": { "type": "string", "nullable": true }, "pageInfo": { "type": "object", "properties": { "currentUrl": { "type": "string", "nullable": true }, "referrer": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true } } } } }, "missedCall": { "oneOf": [ { "$ref": "#/components/schemas/MissedCallWebhookV2" }, { "type": "object", "nullable": true, "enum": [ null ] } ], "description": "Only present for missed call webhooks" }, "aiHandledCall": { "oneOf": [ { "$ref": "#/components/schemas/AIHandledCallWebhookV2" }, { "type": "object", "nullable": true, "enum": [ null ] } ], "description": "Only present for AI handled call webhooks" } } }, "WebhookV2PayloadConversationCompleted": { "type": "object", "description": "V2 Webhook payload for CONVERSATION_COMPLETED events", "properties": { "eventType": { "type": "string", "enum": [ "CONVERSATION_COMPLETED" ], "example": "CONVERSATION_COMPLETED" }, "eventTypeDetails": { "type": "string", "enum": [ "CONVERSATION_COMPLETE_MISSED_CALL", "CONVERSATION_COMPLETE_CHAT_WIDGET", "CONVERSATION_COMPLETE_AI_CALL_HANDLED", "CONVERSATION_COMPLETE_CALENDAR_BOOKING", "CONVERSATION_COMPLETE_INBOUND_EMAIL", "CONVERSATION_COMPLETE_EMAIL_QUALIFICATION", "CONVERSATION_COMPLETE_YELP_LEAD", "CONVERSATION_COMPLETE_THUMBTACK_LEAD", "CONVERSATION_COMPLETE_GOOGLE_LSA_LEAD" ], "example": "CONVERSATION_COMPLETE_CHAT_WIDGET" }, "clientId": { "type": "string", "format": "uuid" }, "leadId": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "isRepeatLead": { "type": "boolean" }, "leadInformation": { "type": "object", "properties": { "name": { "type": "string" }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "email": { "type": "string", "nullable": true }, "phone": { "type": "string", "nullable": true }, "additionalData": { "type": "object", "additionalProperties": true } } }, "commonFields": { "type": "object", "description": "Standardized fields extracted from the conversation", "properties": { "fullAddress": { "type": "string", "nullable": true }, "address": { "type": "string", "nullable": true }, "zipcode": { "type": "string", "nullable": true }, "state": { "type": "string", "nullable": true }, "city": { "type": "string", "nullable": true }, "country": { "type": "string", "nullable": true }, "isHomeowner": { "type": "boolean", "nullable": true }, "customerName": { "type": "string", "nullable": true }, "email": { "type": "string", "nullable": true } } }, "trackingData": { "type": "object", "properties": { "source": { "type": "string", "nullable": true }, "utm_source": { "type": "string", "nullable": true }, "utm_medium": { "type": "string", "nullable": true }, "utm_campaign": { "type": "string", "nullable": true }, "utm_term": { "type": "string", "nullable": true }, "utm_content": { "type": "string", "nullable": true }, "gclid": { "type": "string", "nullable": true }, "fbclid": { "type": "string", "nullable": true }, "msclkid": { "type": "string", "nullable": true }, "ttclid": { "type": "string", "nullable": true }, "snapcid": { "type": "string", "nullable": true }, "gbraid": { "type": "string", "nullable": true }, "wbraid": { "type": "string", "nullable": true }, "gad_source": { "type": "string", "nullable": true }, "igshid": { "type": "string", "nullable": true }, "gclsrc": { "type": "string", "nullable": true }, "srsltid": { "type": "string", "nullable": true }, "ga_client_id": { "type": "string", "nullable": true }, "ga_session_id": { "type": "string", "nullable": true }, "hubspotutk": { "type": "string", "nullable": true }, "pageInfo": { "type": "object", "properties": { "currentUrl": { "type": "string", "nullable": true }, "referrer": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true } } } } }, "leadQualificationStatus": { "type": "string", "enum": [ "COMPLETED", "IN_PROGRESS" ], "nullable": true }, "qualificationSource": { "type": "string", "enum": [ "MISSED_CALL", "WEBSITE_TEXTING", "WEBCHAT", "COLD_TEXT_INBOUND", "AI_HANDLED_CALL", "CALENDAR_BOOKING", "INBOUND_EMAIL", "ANGI_LEAD", "EMAIL_QUALIFICATION", "YELP_LEAD", "THUMBTACK_LEAD", "GOOGLE_LSA_DIRECT" ], "nullable": true, "description": "Channel where the conversation originated. Use this field to distinguish lead types in v2 webhooks (e.g., EMAIL_QUALIFICATION, YELP_LEAD, THUMBTACK_LEAD, GOOGLE_LSA_DIRECT).\n" }, "isManualTakeoverEnabled": { "type": "boolean" }, "qualifyingData": { "type": "object", "description": "Dynamic fields collected during the conversation", "properties": { "contactReason": { "type": "string" } }, "additionalProperties": true }, "qualifyingDataSummary": { "type": "string", "nullable": true }, "contactReason": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "messageHistory": { "type": "array", "items": { "type": "object", "properties": { "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "name": { "type": "string" }, "message": { "type": "string" }, "date": { "type": "string", "format": "date-time" }, "mediaUrl": { "type": "string", "nullable": true }, "additionalMediaUrls": { "type": "array", "items": { "type": "string" }, "nullable": true } } } }, "userMedia": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string" }, "url": { "type": "string" } } } }, "aiHandledCall": { "oneOf": [ { "$ref": "#/components/schemas/AIHandledCallWebhookV2" }, { "type": "object", "nullable": true, "enum": [ null ] } ], "description": "Only present for AI handled call webhooks" }, "missedCall": { "oneOf": [ { "$ref": "#/components/schemas/MissedCallWebhookV2" }, { "type": "object", "nullable": true, "enum": [ null ] } ], "description": "Only present for missed call webhooks" } } }, "WebhookV2PayloadMessageReply": { "type": "object", "description": "V2 Webhook payload for MESSAGE_REPLY events", "properties": { "eventType": { "type": "string", "enum": [ "MESSAGE_REPLY" ], "example": "MESSAGE_REPLY" }, "eventTypeDetails": { "type": "string", "enum": [ "NEW_MESSAGE_REPLY" ], "example": "NEW_MESSAGE_REPLY" }, "messageId": { "type": "string", "format": "uuid" }, "leadId": { "type": "string", "format": "uuid" }, "clientId": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "from": { "type": "string", "example": "+18001234567" }, "to": { "type": "string", "example": "+18009876543" }, "body": { "type": "string", "example": "Yes, I am interested in getting a quote" }, "mediaUrl": { "type": "string", "nullable": true }, "additionalMediaUrls": { "type": "array", "items": { "type": "string" }, "nullable": true }, "timestamp": { "type": "string", "format": "date-time" }, "leadQualificationStatus": { "type": "string", "nullable": true }, "qualificationSource": { "type": "string", "enum": [ "MISSED_CALL", "WEBSITE_TEXTING", "WEBCHAT", "COLD_TEXT_INBOUND", "AI_HANDLED_CALL", "CALENDAR_BOOKING", "INBOUND_EMAIL", "ANGI_LEAD", "EMAIL_QUALIFICATION", "YELP_LEAD", "THUMBTACK_LEAD", "GOOGLE_LSA_DIRECT" ], "nullable": true, "description": "Source channel for the lead. Use this to differentiate reply context (e.g., EMAIL_QUALIFICATION vs YELP_LEAD).\n" }, "isRepeatLead": { "type": "boolean" }, "isManualTakeoverEnabled": { "type": "boolean" }, "leadInformation": { "type": "object", "properties": { "name": { "type": "string" }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "email": { "type": "string", "nullable": true }, "phone": { "type": "string", "nullable": true }, "additionalData": { "type": "object", "additionalProperties": true } } } } }, "ChatWidgetConfig": { "type": "object", "description": "Chat widget configuration settings", "properties": { "id": { "type": "string", "format": "uuid", "example": "550e8400-e29b-41d4-a716-446655440000" }, "name": { "type": "string", "example": "Default Widget" }, "greetingMessage": { "type": "string", "nullable": true, "example": "Welcome to our company! Text us now for immediate response.", "description": "The initial greeting message shown to visitors" }, "agentConfig": { "type": "object", "nullable": true, "description": "AI agent styling and behavior configuration", "properties": { "name": { "type": "string", "example": "Sarah" }, "avatarImageUrl": { "type": "string", "format": "uri", "example": "https://example.com/avatar.png" }, "popupEnabled": { "type": "boolean", "example": true }, "popupOpenDelay": { "type": "number", "example": 5, "description": "Delay in seconds before popup opens" }, "popupExitIntentEnabled": { "type": "boolean", "example": true }, "popupHeader1": { "type": "string", "example": "Need Help?" }, "popupHeader2": { "type": "string", "example": "Chat with us now!" }, "popupBannerImage": { "type": "string", "format": "uri" }, "popupLogoImage": { "type": "string", "format": "uri" }, "popupBgColor": { "type": "string", "example": "#ffffff" }, "popupTextColor": { "type": "string", "example": "#000000" }, "popupMinNumberMessagesToday": { "type": "number", "example": 0 }, "popupCustomStyles": { "type": "string", "description": "Custom CSS for popup styling" } } }, "leadQualifierAgentConfig": { "type": "object", "nullable": true, "description": "Lead qualification AI agent configuration", "properties": { "agentInstructions": { "type": "string", "example": "You are a helpful assistant that qualifies leads for our HVAC company...", "description": "Custom instructions for the AI agent" }, "maximumMessagesLimit": { "type": "number", "minimum": 4, "maximum": 30, "example": 20, "description": "Maximum number of messages the AI will send before ending conversation" } } }, "primaryColor": { "type": "string", "nullable": true, "example": "#007bff", "description": "Primary color for widget styling" }, "secondaryColor": { "type": "string", "nullable": true, "example": "#6c757d", "description": "Secondary color for widget styling" }, "backgroundColor": { "type": "string", "nullable": true, "example": "#ffffff", "description": "Background color for widget" }, "customCss": { "type": "string", "nullable": true, "description": "Custom CSS for advanced widget styling" }, "isActive": { "type": "boolean", "example": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } }, "ChatWidgetConfigUpdate": { "type": "object", "description": "Request payload for updating chat widget configuration", "properties": { "greetingMessage": { "type": "string", "maxLength": 1000, "nullable": true, "example": "Welcome to our company! How can we help you today?", "description": "The initial greeting message shown to visitors" }, "agentConfig": { "type": "object", "nullable": true, "description": "AI agent styling and behavior configuration", "properties": { "name": { "type": "string", "maxLength": 200, "example": "Sarah" }, "avatarImageUrl": { "type": "string", "maxLength": 1000, "format": "uri", "example": "https://example.com/avatar.png" }, "popupEnabled": { "type": "boolean", "example": true }, "popupOpenDelay": { "type": "number", "example": 5, "description": "Delay in seconds before popup opens" }, "popupExitIntentEnabled": { "type": "boolean", "example": true }, "popupHeader1": { "type": "string", "maxLength": 1000, "example": "Need Help?" }, "popupHeader2": { "type": "string", "maxLength": 1000, "example": "Chat with us now!" }, "popupBannerImage": { "type": "string", "maxLength": 1000, "format": "uri" }, "popupLogoImage": { "type": "string", "maxLength": 1000, "format": "uri" }, "popupBgColor": { "type": "string", "maxLength": 10, "example": "#ffffff" }, "popupTextColor": { "type": "string", "maxLength": 10, "example": "#000000" }, "popupMinNumberMessagesToday": { "type": "number", "example": 0 }, "popupCustomStyles": { "type": "string", "maxLength": 10000, "description": "Custom CSS for popup styling" } } }, "leadQualifierAgentEnabled": { "type": "boolean", "nullable": true, "example": true, "description": "Enable or disable the lead qualification AI agent" }, "leadQualifierAgentConfig": { "type": "object", "nullable": true, "description": "Lead qualification AI agent configuration", "properties": { "agentInstructions": { "type": "string", "maxLength": 10000, "example": "You are a helpful assistant for our HVAC company. Ask about their location, timeline, and budget.", "description": "Custom instructions for the AI agent" }, "maximumMessagesLimit": { "type": "number", "minimum": 4, "maximum": 20, "example": 8, "description": "Maximum number of messages the AI will send before ending conversation" } } }, "primaryColor": { "type": "string", "maxLength": 50, "nullable": true, "example": "#007bff", "description": "Primary color for widget styling" }, "secondaryColor": { "type": "string", "maxLength": 50, "nullable": true, "example": "#6c757d", "description": "Secondary color for widget styling" }, "backgroundColor": { "type": "string", "maxLength": 50, "nullable": true, "example": "#ffffff", "description": "Background color for widget" }, "customCss": { "type": "string", "maxLength": 10000, "nullable": true, "description": "Custom CSS for advanced widget styling" } } }, "ChatWidgetLeadTrigger": { "type": "object", "description": "Request payload for triggering lead qualifier", "required": [ "name", "phone" ], "properties": { "name": { "type": "string", "minLength": 1, "maxLength": 200, "example": "John Doe", "description": "Full name of the lead" }, "phone": { "type": "string", "minLength": 1, "maxLength": 50, "example": "+15551234567", "description": "Phone number in E.164 format or US national format" }, "email": { "type": "string", "format": "email", "nullable": true, "example": "john.doe@example.com", "description": "Email address of the lead (optional)" }, "message": { "type": "string", "maxLength": 1000, "nullable": true, "example": "Interested in HVAC services for my home", "description": "Initial message or inquiry from the lead" }, "additionalData": { "type": "object", "additionalProperties": { "type": "string", "maxLength": 1000 }, "example": { "leadSource": "facebook", "fbid": "923849028492034890283094", "campaignId": "summer_hvac_2024" }, "description": "Additional tracking data and lead source information" } } }, "ChatWidgetLeadTriggerResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "client": { "$ref": "#/components/schemas/Client" }, "leadSubmission": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "clientId": { "type": "string", "format": "uuid" }, "firstName": { "type": "string" }, "lastName": { "type": "string" }, "email": { "type": "string", "nullable": true }, "phone": { "type": "string" }, "qualificationSource": { "type": "string", "enum": [ "MISSED_CALL", "WEBSITE_TEXTING", "WEBCHAT", "COLD_TEXT_INBOUND", "AI_HANDLED_CALL", "CALENDAR_BOOKING", "INBOUND_EMAIL", "ANGI_LEAD", "EMAIL_QUALIFICATION", "YELP_LEAD", "THUMBTACK_LEAD", "GOOGLE_LSA_DIRECT" ], "example": "WEBSITE_TEXTING" }, "createdAt": { "type": "string", "format": "date-time" } } }, "wasExistingClient": { "type": "boolean", "example": false, "description": "Whether this phone number was already in the system" }, "isRepeatSubmission": { "type": "boolean", "example": false, "description": "Whether this lead has submitted recently" } } } } }, "AppointmentCreatedWebhookPayload": { "type": "object", "required": [ "eventType", "eventTypeDetails", "appointmentId", "companyId", "calendarId", "clientId", "appointmentDetails", "clientInformation", "calendarInformation", "companyInformation" ], "properties": { "eventType": { "type": "string", "enum": [ "NEW_APPOINTMENT" ], "description": "The webhook event type", "example": "NEW_APPOINTMENT" }, "eventTypeDetails": { "type": "string", "enum": [ "NEW_APPOINTMENT" ], "description": "Detailed event type", "example": "NEW_APPOINTMENT" }, "appointmentId": { "type": "string", "format": "uuid", "description": "Unique identifier for the appointment", "example": "apt_123e4567-e89b-12d3-a456-426614174000" }, "companyId": { "type": "string", "format": "uuid", "description": "Company identifier", "example": "comp_123e4567-e89b-12d3-a456-426614174000" }, "calendarId": { "type": "string", "format": "uuid", "description": "Calendar identifier", "example": "cal_123e4567-e89b-12d3-a456-426614174000" }, "clientId": { "type": "string", "format": "uuid", "description": "Client identifier", "example": "client_123e4567-e89b-12d3-a456-426614174000" }, "appointmentDetails": { "type": "object", "required": [ "title", "startAt", "endAt", "status", "source", "createdAt" ], "properties": { "title": { "type": "string", "description": "Appointment title", "example": "Appointment with ABC Home Services" }, "description": { "type": "string", "nullable": true, "description": "Appointment description", "example": "Initial consultation for kitchen renovation project" }, "startAt": { "type": "string", "format": "date-time", "description": "Appointment start time in ISO 8601 format", "example": "2024-01-15T14:00:00.000Z" }, "endAt": { "type": "string", "format": "date-time", "description": "Appointment end time in ISO 8601 format", "example": "2024-01-15T15:00:00.000Z" }, "status": { "type": "string", "enum": [ "BOOKED", "CANCELLED", "COMPLETED", "NO_SHOW" ], "description": "Appointment status", "example": "BOOKED" }, "source": { "type": "string", "enum": [ "WEB_PORTAL", "VOICE_AGENT", "ADMIN_UI", "SYNC" ], "description": "How the appointment was created", "example": "WEB_PORTAL" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the appointment was created", "example": "2024-01-10T10:30:00.000Z" }, "appointmentNotifications": { "type": "array", "description": "Notifications sent for this appointment", "items": { "type": "object", "properties": { "medium": { "type": "string", "enum": [ "EMAIL", "SMS", "PUSH", "WEBHOOK", "OTHER" ], "example": "EMAIL" }, "templateId": { "type": "string", "nullable": true, "example": "appt-confirmation-v1" }, "sentAt": { "type": "string", "format": "date-time", "nullable": true, "example": "2024-01-10T10:31:00.000Z" }, "status": { "type": "string", "enum": [ "QUEUED", "SENT", "FAILED" ], "example": "SENT" }, "recipients": { "type": "array", "items": { "type": "object", "properties": { "contact": { "type": "string", "nullable": true, "description": "Phone, email, or webhook URL", "example": "john.smith@example.com" }, "role": { "type": "string", "nullable": true, "enum": [ "CLIENT", "COMPANY_USER", "ADDITIONAL_CONTACT", "OTHER" ], "example": "CLIENT" }, "name": { "type": "string", "nullable": true, "example": "John Smith" }, "medium": { "type": "string", "nullable": true, "enum": [ "EMAIL", "SMS", "PUSH", "WEBHOOK", "OTHER" ], "example": "EMAIL" } } } }, "metadata": { "type": "object", "additionalProperties": true, "nullable": true, "example": { "messageId": "email-msg-1" } } } } }, "postBookingActions": { "type": "array", "description": "Follow-up actions taken after booking", "items": { "type": "object", "properties": { "action": { "type": "string", "example": "SYNCED_TO_GOOGLE_CAL" }, "performedAt": { "type": "string", "format": "date-time", "nullable": true, "example": "2024-01-10T10:31:10.000Z" }, "status": { "type": "string", "enum": [ "PENDING", "SUCCESS", "FAILED" ], "example": "SUCCESS" }, "metadata": { "type": "object", "additionalProperties": true, "nullable": true, "example": { "calendarEventId": "gcal-evt-123" } } } } } } }, "clientInformation": { "type": "object", "properties": { "firstName": { "type": "string", "nullable": true, "description": "Client's first name", "example": "John" }, "lastName": { "type": "string", "nullable": true, "description": "Client's last name", "example": "Smith" }, "fullName": { "type": "string", "nullable": true, "description": "Client's full name", "example": "John Smith" }, "primaryEmail": { "type": "string", "nullable": true, "description": "Client's primary email address", "example": "john.smith@example.com" }, "primaryPhone": { "type": "string", "nullable": true, "description": "Client's primary phone number", "example": "+15551234567" }, "companyName": { "type": "string", "nullable": true, "description": "Client's company name" }, "address": { "type": "string", "nullable": true, "description": "Client's address", "example": "123 Main Street" }, "city": { "type": "string", "nullable": true, "description": "Client's city", "example": "Springfield" }, "state": { "type": "string", "nullable": true, "description": "Client's state", "example": "IL" }, "zipCode": { "type": "string", "nullable": true, "description": "Client's ZIP code", "example": "62701" }, "country": { "type": "string", "nullable": true, "description": "Client's country", "example": "US" } } }, "calendarInformation": { "type": "object", "required": [ "name", "calendarType", "timeZone" ], "properties": { "name": { "type": "string", "description": "Calendar name", "example": "Primary" }, "calendarType": { "type": "string", "enum": [ "PRIMARY", "CREW", "USER" ], "description": "Type of calendar", "example": "PRIMARY" }, "timeZone": { "type": "string", "description": "Calendar timezone", "example": "America/Chicago" } } }, "companyInformation": { "type": "object", "required": [ "name", "timeZone" ], "properties": { "name": { "type": "string", "description": "Company name", "example": "ABC Home Services" }, "timeZone": { "type": "string", "description": "Company timezone", "example": "America/Chicago" } } }, "leadFormData": { "type": "object", "nullable": true, "description": "Optional lead form data if appointment was created via lead form", "properties": { "message": { "type": "string", "nullable": true, "description": "Lead message", "example": "I need help with a kitchen renovation. Looking for a consultation." }, "additionalData": { "type": "object", "additionalProperties": true, "description": "Additional form data", "example": { "projectType": "Kitchen Renovation", "budgetRange": "$15,000 - $25,000", "timeframe": "Next 3 months", "source": "website_form", "utm_source": "google", "utm_medium": "cpc", "utm_campaign": "kitchen_renovation" } } } } } }, "Calendar": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the calendar" }, "name": { "type": "string", "description": "Name of the calendar" }, "description": { "type": "string", "nullable": true, "description": "Optional description of the calendar" }, "calendarType": { "type": "string", "enum": [ "PRIMARY", "CREW", "USER" ], "description": "Type of calendar" }, "timeZone": { "type": "string", "description": "IANA timezone identifier for the calendar" }, "minBookAheadHours": { "type": "integer", "description": "Minimum hours required to book ahead" }, "maxBookAheadDays": { "type": "integer", "description": "Maximum days allowed to book ahead" }, "timeBlockMinutes": { "type": "integer", "description": "Duration of each appointment time block in minutes" }, "isDoubleBookingAllowed": { "type": "boolean", "description": "Whether double booking is allowed on this calendar" }, "bufferMinutes": { "type": "integer", "description": "Buffer time in minutes between appointments" }, "isActive": { "type": "boolean", "description": "Whether the calendar is active" }, "createdAt": { "type": "string", "format": "date-time", "description": "When the calendar was created" }, "updatedAt": { "type": "string", "format": "date-time", "description": "When the calendar was last updated" } }, "required": [ "id", "name", "calendarType", "timeZone", "minBookAheadHours", "maxBookAheadDays", "timeBlockMinutes", "isDoubleBookingAllowed", "bufferMinutes", "isActive", "createdAt", "updatedAt" ], "example": { "id": "123e4567-e89b-12d3-a456-426614174000", "name": "Primary", "description": "Main company calendar", "calendarType": "PRIMARY", "timeZone": "America/New_York", "minBookAheadHours": 24, "maxBookAheadDays": 14, "timeBlockMinutes": 60, "isDoubleBookingAllowed": false, "bufferMinutes": 0, "isActive": true, "createdAt": "2024-01-15T10:00:00Z", "updatedAt": "2024-01-15T10:00:00Z" } }, "EmailGatewaySubmitRequest": { "type": "object", "description": "Payload for submitting an email gateway message", "required": [ "emailGatewayAddress", "emailBodyText" ], "properties": { "emailGatewayAddress": { "type": "string", "format": "email", "example": "c3f3c4a2-1234-5678-9012-abcdefabcdef@m.leadtruffle.com", "description": "The email gateway address assigned to your company" }, "emailBodyText": { "type": "string", "minLength": 1, "maxLength": 50000, "example": "Customer submitted a form requesting a quote for HVAC repair...", "description": "Plain text body that will be parsed by the email gateway" }, "emailSubject": { "type": "string", "maxLength": 500, "nullable": true, "example": "New website lead", "description": "Optional subject line used for context" }, "customerEmail": { "type": "string", "format": "email", "nullable": true, "example": "jane.doe@example.com", "description": "Optional known customer email to skip AI email extraction" }, "messageId": { "type": "string", "maxLength": 200, "nullable": true, "example": "ext-msg-12345", "description": "Optional idempotency key to prevent duplicate processing" } } }, "EmailGatewaySubmitResponse": { "type": "object", "description": "Response after submitting an email gateway payload", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "inboundEmailId": { "type": "string", "format": "uuid", "nullable": true, "example": "3c8b9b67-4b4a-4f2b-9f33-6f1e4e8b2f29", "description": "Identifier for the inbound email record (null for duplicate without lookup)" }, "status": { "type": "string", "enum": [ "QUEUED", "DUPLICATE" ], "example": "QUEUED", "description": "Processing status for this submission" }, "queuedAction": { "type": "string", "enum": [ "QUALIFY_OVER_EMAIL", "CREATE_LEAD" ], "nullable": true, "example": "QUALIFY_OVER_EMAIL", "description": "Action queued for processing (only present when status is QUEUED)" } } } } }, "YelpLeadAgentRequest": { "type": "object", "required": [ "yelpLeadId", "yelpBusinessId", "messageId", "text" ], "properties": { "yelpLeadId": { "type": "string", "description": "Yelp lead identifier provided by Yelp messaging platform." }, "yelpBusinessId": { "type": "string", "description": "Identifier for the Yelp business profile receiving the message." }, "messageId": { "type": "string", "description": "Unique identifier for the message event from Yelp." }, "userType": { "type": "string", "enum": [ "CONSUMER", "BUSINESS" ], "description": "Denotes whether the sender is the consumer or business. Defaults to `CONSUMER` when omitted." }, "userDisplayName": { "type": "string", "description": "Display name for the Yelp user if the source provides one." }, "timeCreated": { "type": "string", "format": "date-time", "description": "ISO8601 timestamp when the Yelp message was created. Defaults to the current time when omitted or invalid." }, "text": { "type": "string", "description": "Raw text content from the Yelp message." }, "attachmentUrls": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ], "description": "Optional attachment URLs from Yelp. You can provide a single string or an array of strings—single values will automatically be wrapped into an array.\n" }, "attachmentText": { "type": "string", "nullable": true, "description": "Optional OCR or caption content provided for attachments." } } }, "YelpLeadAgentQualificationResponse": { "type": "object", "required": [ "response", "contactReason", "allQuestionsAnswered", "detectedAbuse", "shouldSkipReply", "hasImages", "dataFields", "commonFields" ], "properties": { "response": { "type": "string", "description": "AI-generated reply that will be sent back to the Yelp user." }, "contactReason": { "type": "string", "description": "Summary of why the customer reached out." }, "allQuestionsAnswered": { "type": "boolean", "description": "Indicates whether all required qualification questions are answered." }, "isClientReadyToBook": { "type": "boolean", "description": "True when the customer clearly indicates they are ready to book service." }, "detectedAbuse": { "type": "boolean", "description": "True if abusive or inappropriate content was detected." }, "shouldSkipReply": { "type": "boolean", "description": "True if the AI recommends not replying (e.g., user requested no contact)." }, "hasImages": { "type": "boolean", "description": "True if attachments/images were included in the conversation." }, "dataFields": { "type": "array", "description": "List of structured data points gathered during the conversation.", "items": { "type": "object", "required": [ "name", "value" ], "properties": { "name": { "type": "string" }, "value": { "type": "string" } } } }, "commonFields": { "type": "object", "required": [ "phone", "fullAddress", "address", "zipcode", "state", "city", "isHomeowner", "customerName", "email" ], "properties": { "phone": { "type": "string", "nullable": true }, "fullAddress": { "type": "string", "nullable": true }, "address": { "type": "string", "nullable": true }, "zipcode": { "type": "string", "nullable": true }, "state": { "type": "string", "nullable": true }, "city": { "type": "string", "nullable": true }, "isHomeowner": { "type": "boolean", "nullable": true }, "customerName": { "type": "string", "nullable": true }, "email": { "type": "string", "nullable": true } } } } }, "YelpLeadAgentResponse": { "oneOf": [ { "type": "object", "required": [ "success", "data" ], "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "type": "object", "required": [ "reply", "conversationId", "qualificationResponse", "manualTakeoverEnabled" ], "properties": { "reply": { "type": "string", "nullable": true, "description": "AI response that should be sent back to Yelp. This is null when manualTakeoverEnabled\nis true or when the initial lead reply was already sent by a concurrent event. When null,\ndo not send a response message to Yelp.\n" }, "leadId": { "type": "string", "nullable": true, "description": "LeadTruffle lead ID if one was created." }, "conversationId": { "type": "string", "description": "Internal Yelp conversation identifier." }, "qualificationResponse": { "oneOf": [ { "$ref": "#/components/schemas/YelpLeadAgentQualificationResponse" }, { "type": "object", "nullable": true, "enum": [ null ] } ] }, "isDuplicate": { "type": "boolean", "description": "Returns true when the API replays the last reply because the inbound message matched a previous message." }, "manualTakeoverEnabled": { "type": "boolean", "description": "Indicates whether the conversation is in manual takeover mode (AI replies paused)." } } } } }, { "type": "object", "required": [ "success", "code" ], "properties": { "success": { "type": "boolean", "enum": [ false ] }, "code": { "type": "string", "description": "Machine readable error code.", "enum": [ "INVALID_INPUT", "AGENT_DISABLED", "MISSING_CONFIG", "AI_ERROR", "UNKNOWN_ERROR" ] }, "error": { "description": "Additional error detail.", "oneOf": [ { "type": "string" }, { "type": "object" } ] } } } ] }, "YelpLeadAgentLeadPayload": { "type": "object", "description": "Raw payload forwarded from Yelp's `NEW_LEAD` webhook. Every field is optional because Yelp may omit values depending on the template completed by the consumer.\n", "properties": { "yelpLeadId": { "type": "string", "description": "Optional lead identifier supplied inside the payload. Defaults to the root `yelpLeadId` when omitted." }, "yelpBusinessId": { "type": "string", "description": "Optional business identifier supplied inside the payload. Defaults to the root `yelpBusinessId` when omitted." }, "leadUserName": { "type": "string", "description": "Display name for the Yelp consumer." }, "leadTimeCreated": { "type": "string", "format": "date-time", "description": "Timestamp when Yelp recorded the lead card." }, "leadTimeUpdated": { "type": "string", "format": "date-time", "description": "Timestamp when Yelp last updated the card." }, "temporaryEmail": { "type": "string", "format": "email", "description": "Temporary Yelp relay email address for the consumer." }, "temporaryEmailExpiry": { "type": "string", "format": "date-time", "description": "Expiration timestamp of the temporary email address." }, "temporaryPhone": { "type": "string", "description": "Temporary Yelp relay phone number for the consumer." }, "temporaryPhoneExpiry": { "type": "string", "format": "date-time", "description": "Expiration timestamp of the temporary phone number." }, "project": { "type": "object", "description": "Nested project details supplied by Yelp. Contents are stored verbatim and not validated.\n", "additionalProperties": true }, "business": { "type": "object", "description": "Snapshot of the business profile returned with the lead.", "additionalProperties": true }, "metadata": { "type": "object", "description": "Additional metadata captured by any upstream integrations.", "additionalProperties": true } }, "additionalProperties": true }, "YelpLeadAgentLeadRequest": { "type": "object", "required": [ "yelpLeadId", "yelpBusinessId", "payload" ], "properties": { "yelpLeadId": { "type": "string", "description": "Yelp lead identifier supplied as the root payload ID." }, "yelpBusinessId": { "type": "string", "description": "Yelp business identifier tied to the inbox." }, "payload": { "$ref": "#/components/schemas/YelpLeadAgentLeadPayload" } } }, "YelpLeadAgentLeadResponse": { "oneOf": [ { "type": "object", "required": [ "success", "data" ], "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "type": "object", "required": [ "conversationId", "reply", "manualTakeoverEnabled", "qualificationResponse" ], "properties": { "conversationId": { "type": "string", "description": "Internal Yelp conversation identifier that stores the payload." }, "leadId": { "type": "string", "nullable": true, "description": "LeadTruffle lead ID when one exists for the conversation." }, "clientId": { "type": "string", "nullable": true, "description": "Client identifier created or updated from the payload." }, "reply": { "type": "string", "nullable": true, "description": "AI reply to send back to Yelp for the initial lead card. This can be null\nwhen manual takeover is enabled, the agent is disabled, or the initial\nreply was already sent by a concurrent consumer message. When null, do\nnot send a response message to Yelp.\n" }, "manualTakeoverEnabled": { "type": "boolean", "description": "Indicates whether the conversation is in manual takeover mode (AI replies paused)." }, "qualificationResponse": { "oneOf": [ { "$ref": "#/components/schemas/YelpLeadAgentQualificationResponse" }, { "type": "object", "nullable": true, "enum": [ null ] } ] }, "initialReplySkipped": { "type": "boolean", "description": "True when the initial reply was intentionally suppressed to avoid a duplicate response." } } } } }, { "type": "object", "required": [ "success", "code" ], "properties": { "success": { "type": "boolean", "enum": [ false ] }, "code": { "type": "string", "enum": [ "INVALID_INPUT", "UNKNOWN_ERROR" ] }, "error": { "description": "Additional error detail.", "oneOf": [ { "type": "string" }, { "type": "object" } ] } } } ] }, "YelpBusinessMessageRequest": { "type": "object", "required": [ "yelpLeadId", "messageId", "text" ], "properties": { "yelpLeadId": { "type": "string", "description": "Yelp lead identifier (maps to externalLeadId in our system)", "example": "lead_abc123" }, "messageId": { "type": "string", "description": "Unique message identifier from Yelp", "example": "msg_business_789" }, "yelpBusinessId": { "type": "string", "nullable": true, "description": "Yelp business identifier", "example": "business_456" }, "text": { "type": "string", "description": "The message content sent by the business", "example": "Thanks for reaching out! We can schedule you for next Tuesday." }, "timeCreated": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp when the message was created in Yelp", "example": "2024-12-01T16:00:00Z" }, "attachmentUrls": { "type": "array", "items": { "type": "string", "format": "uri" }, "nullable": true, "description": "URLs of any attachments sent with the message" }, "attachmentText": { "type": "string", "nullable": true, "description": "Text description of attachments" }, "userDisplayName": { "type": "string", "nullable": true, "description": "Display name of the business user who sent the message", "example": "ABC Plumbing" } } }, "YelpBusinessMessageResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the request was successful", "example": true }, "data": { "type": "object", "properties": { "conversationId": { "type": "string", "format": "uuid", "description": "Internal conversation identifier", "example": "ad10af01-6d8a-4b46-83aa-4a7d38c35172" }, "messageId": { "type": "string", "description": "The message ID that was processed", "example": "msg_business_789" }, "skipped": { "type": "boolean", "description": "If true, the message was skipped due to deduplication", "example": false }, "reason": { "type": "string", "description": "Reason for skipping (only present if skipped is true)", "enum": [ "DUPLICATE_CONTENT" ], "example": "DUPLICATE_CONTENT" } } }, "error": { "type": "string", "description": "Error message if success is false" }, "code": { "type": "string", "description": "Error code if success is false", "enum": [ "INVALID_INPUT", "CONVERSATION_NOT_FOUND", "UNKNOWN_ERROR" ] } } }, "YelpSendMessageRequest": { "type": "object", "required": [ "message" ], "properties": { "conversationId": { "type": "string", "format": "uuid", "description": "Internal conversation ID. Provide either this or externalLeadId.\n", "example": "ad10af01-6d8a-4b46-83aa-4a7d38c35172" }, "externalLeadId": { "type": "string", "description": "Yelp lead ID (yelpLeadId). Provide either this or conversationId.\n", "example": "lead_abc123" }, "message": { "type": "string", "minLength": 1, "maxLength": 5000, "description": "The message text to send to the Yelp lead", "example": "Hi! Just following up on your request. Are you still interested in getting a quote?" }, "attachmentUrls": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "Optional URLs of attachments to include with the message", "example": [] } } }, "YelpSendMessageResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the request was successful", "example": true }, "data": { "type": "object", "properties": { "conversationId": { "type": "string", "format": "uuid", "description": "Internal conversation identifier", "example": "ad10af01-6d8a-4b46-83aa-4a7d38c35172" }, "webhooksTriggered": { "type": "integer", "description": "Number of registered webhook endpoints triggered for this outbound message", "example": 2 } } }, "error": { "type": "string", "description": "Error message if success is false" }, "code": { "type": "string", "description": "Error code if success is false", "enum": [ "INVALID_INPUT", "CONVERSATION_NOT_FOUND", "MANUAL_TAKEOVER_ENABLED", "NO_WEBHOOKS_CONFIGURED", "UNKNOWN_ERROR" ] } } }, "YelpConversationMedia": { "type": "object", "required": [ "type", "url" ], "properties": { "type": { "type": "string", "description": "Media type provided by Yelp.", "enum": [ "image", "video", "audio" ] }, "url": { "type": "string", "format": "uri", "description": "Direct URL to the media asset." }, "thumbnailUrl": { "type": "string", "format": "uri", "nullable": true, "description": "Optional thumbnail preview URL." } } }, "YelpConversationMessage": { "type": "object", "required": [ "role", "timestamp", "content" ], "properties": { "role": { "type": "string", "enum": [ "user", "assistant" ], "description": "Indicates whether the message was sent by the consumer or the AI agent." }, "timestamp": { "type": "string", "format": "date-time", "description": "ISO8601 timestamp when the message was logged." }, "content": { "type": "string", "description": "Text content of the message." }, "additionalData": { "type": "object", "nullable": true, "description": "Supplemental metadata captured for the message." }, "media": { "type": "array", "items": { "$ref": "#/components/schemas/YelpConversationMedia" }, "description": "Optional media attachments included with the message." } } }, "YelpConversationHistory": { "type": "object", "required": [ "messages" ], "properties": { "messages": { "type": "array", "description": "Ordered message history captured for the Yelp lead.", "items": { "$ref": "#/components/schemas/YelpConversationMessage" } } } }, "YelpLeadConversationActionMetadata": { "type": "object", "properties": { "actionsTaken": { "type": "array", "description": "Downstream system actions triggered for the conversation.", "items": { "type": "object", "required": [ "action", "actionStatus", "actionDataResult" ], "properties": { "action": { "type": "string", "enum": [ "EMAIL_SENT", "LEAD_CREATED", "TEXT_SENT" ] }, "actionStatus": { "type": "string", "enum": [ "SUCCESS", "FAILURE" ] }, "actionDataResult": { "type": "object", "description": "Arbitrary payload describing the action result." } } } } } }, "YelpLeadConversation": { "type": "object", "required": [ "id", "companyId", "eventType", "createdAt", "updatedAt", "manualTakeoverEnabled" ], "properties": { "id": { "type": "string", "format": "uuid" }, "companyId": { "type": "string", "format": "uuid" }, "eventType": { "type": "string", "description": "Yelp webhook event that opened the conversation (e.g., NEW_CONSUMER_MESSAGE)." }, "externalLeadId": { "type": "string", "description": "Lead identifier from Yelp." }, "conversationHistory": { "$ref": "#/components/schemas/YelpConversationHistory" }, "latestQualificationResponse": { "$ref": "#/components/schemas/YelpLeadAgentQualificationResponse" }, "actionMetadata": { "$ref": "#/components/schemas/YelpLeadConversationActionMetadata" }, "manualTakeoverEnabled": { "type": "boolean", "description": "Indicates if the company has paused AI replies and is handling the conversation manually." }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" } } }, "YelpLeadConversationListResponse": { "type": "object", "required": [ "success", "data", "pagination" ], "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/YelpLeadConversation" } }, "pagination": { "type": "object", "required": [ "total", "limit", "offset", "hasMore" ], "properties": { "total": { "type": "integer", "minimum": 0 }, "limit": { "type": "integer", "minimum": 1, "maximum": 100 }, "offset": { "type": "integer", "minimum": 0 }, "hasMore": { "type": "boolean" } } } } }, "YelpLeadConversationResponse": { "type": "object", "required": [ "success", "data" ], "properties": { "success": { "type": "boolean", "enum": [ true ] }, "data": { "$ref": "#/components/schemas/YelpLeadConversation" } } }, "WebhookV2PayloadYelpMessageOutbound": { "type": "object", "description": "Webhook payload for YELP_MESSAGE_OUTBOUND events. Fired when you want to send\na follow-up message to a Yelp lead. Configure Zapier to receive this webhook\nand relay the message to Yelp via their \"Create Message\" action.\n", "required": [ "eventType", "eventTypeDetails", "yelpLeadId", "conversationId", "companyId", "message", "timestamp", "triggeredBy" ], "properties": { "eventType": { "type": "string", "enum": [ "YELP_MESSAGE_OUTBOUND" ], "description": "Event type identifier", "example": "YELP_MESSAGE_OUTBOUND" }, "eventTypeDetails": { "type": "string", "enum": [ "YELP_FOLLOWUP_MESSAGE" ], "description": "Specific event detail", "example": "YELP_FOLLOWUP_MESSAGE" }, "yelpLeadId": { "type": "string", "description": "Yelp lead identifier. Use this in Zapier to identify which conversation\nto send the message to in Yelp.\n", "example": "lead_abc123" }, "yelpBusinessId": { "type": "string", "nullable": true, "description": "Yelp business identifier", "example": "business_456" }, "conversationId": { "type": "string", "format": "uuid", "description": "Internal LeadTruffle conversation ID", "example": "ad10af01-6d8a-4b46-83aa-4a7d38c35172" }, "companyId": { "type": "string", "format": "uuid", "description": "Company identifier", "example": "comp_99999999-8888-7777-6666-444444444444" }, "message": { "type": "string", "description": "The message text to send to the Yelp lead", "example": "Hi! Thanks for reaching out. Are you still interested in getting a quote?" }, "attachmentUrls": { "type": "array", "items": { "type": "string", "format": "uri" }, "description": "Optional attachment URLs", "example": [] }, "timestamp": { "type": "string", "format": "date-time", "description": "When the webhook was triggered", "example": "2024-01-16T10:00:00.000Z" }, "triggeredBy": { "type": "string", "enum": [ "API", "UI", "SCHEDULED" ], "description": "How the message send was triggered", "example": "API" }, "leadId": { "type": "string", "format": "uuid", "nullable": true, "description": "Associated lead ID if available", "example": "lead_87654321-dcba-4321-8765-987654321def" }, "clientId": { "type": "string", "format": "uuid", "nullable": true, "description": "Associated client ID if available", "example": "client_11111111-2222-3333-4444-555555555555" }, "leadInformation": { "type": "object", "nullable": true, "description": "Optional lead context for filtering/logging in Zapier", "properties": { "name": { "type": "string", "nullable": true, "example": "John Doe" }, "phone": { "type": "string", "nullable": true, "example": "+15551234567" }, "email": { "type": "string", "nullable": true, "example": "john.doe@example.com" } } } } }, "YelpOutboundMessagesListResponse": { "type": "object", "description": "Response containing a list of outbound messages in YELP_MESSAGE_OUTBOUND webhook payload format.", "properties": { "success": { "type": "boolean", "description": "Whether the request was successful", "example": true }, "data": { "type": "array", "description": "List of outbound messages in webhook payload format", "items": { "$ref": "#/components/schemas/WebhookV2PayloadYelpMessageOutbound" } }, "pagination": { "type": "object", "properties": { "total": { "type": "integer", "description": "Total number of outbound messages available", "example": 25 }, "limit": { "type": "integer", "description": "Maximum number of messages per page", "example": 10 }, "offset": { "type": "integer", "description": "Number of messages skipped", "example": 0 }, "hasMore": { "type": "boolean", "description": "Whether there are more messages beyond this page", "example": true } } }, "error": { "type": "string", "description": "Error message if success is false" } } }, "ConversionUpsertRequest": { "type": "object", "required": [ "conversionStatus" ], "properties": { "leadId": { "type": "string", "format": "uuid", "description": "Lead ID to associate the conversion with. If provided, this is used directly.\nYou may provide either leadId or phone. If both are provided, leadId is used.\n", "example": "123e4567-e89b-12d3-a456-426614174000" }, "phone": { "type": "string", "description": "Phone number of the lead to resolve the most recent lead submission for this company.\nE.164 format is recommended (e.g., +15551234567); US 10-digit formats are also accepted and normalized.\nYou may provide either phone or leadId. If both are provided, leadId is used.\n", "example": "+15551234567" }, "conversionStatus": { "type": "string", "enum": [ "WON", "LOST", "QUOTED", "PENDING", "CONTACTED", "NURTURING", "CLOSED" ], "description": "The status of the lead conversion. Required field.", "example": "WON" }, "revenueAmount": { "type": "string", "pattern": "^\\d+(\\.\\d{1,2})?$", "description": "Revenue amount as a decimal string with max 2 decimal places.\nCan be a string like \"150.00\" or a number.\nValid range: 0.01 to 999,999,999.99\nOptional field.\n", "example": "1500.00" }, "currency": { "type": "string", "enum": [ "USD", "CAD" ], "default": "USD", "description": "Currency code. Defaults to USD if not specified. Optional field.", "example": "USD" }, "notes": { "type": "string", "maxLength": 1000, "description": "Optional notes about the conversion. Maximum 1000 characters.", "example": "Customer signed annual contract via email" }, "source": { "type": "string", "maxLength": 100, "description": "Optional source identifier indicating where this conversion came from.\nExamples: 'Jobber', 'HousecallPro', 'ServiceTitan', 'Salesforce', 'Manual Entry'\n\nDefaults to 'API' if not specified.\nMax length: 100 characters.\n", "example": "HousecallPro" }, "sourceData": { "type": "object", "description": "Optional JSON object to store integration-specific data.\nCan contain any valid JSON data (e.g., CRM IDs, sales rep info, custom fields).\nMaximum size: 10KB when serialized.\n", "example": { "crmId": "SF-12345", "salesRep": "John Doe", "closedDate": "2024-01-15", "dealValue": 1500 }, "additionalProperties": true } }, "anyOf": [ { "required": [ "leadId" ] }, { "required": [ "phone" ] } ] }, "ConversionUpsertResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true, "description": "Indicates whether the operation was successful" }, "data": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the conversion record", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "companyId": { "type": "string", "format": "uuid", "description": "Company ID that owns this conversion", "example": "comp_xyz789" }, "leadFormSubmissionId": { "type": "string", "format": "uuid", "description": "The lead ID this conversion is associated with", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" }, "conversionStatus": { "type": "string", "enum": [ "WON", "LOST", "QUOTED", "PENDING", "CONTACTED", "NURTURING", "CLOSED" ], "description": "The conversion status", "example": "WON" }, "revenueAmount": { "type": "string", "nullable": true, "description": "Revenue amount as a decimal string. Null if not provided.", "example": "1500.00" }, "currency": { "type": "string", "description": "Currency code (USD or CAD)", "example": "USD" }, "source": { "type": "string", "nullable": true, "description": "Source of the conversion data (e.g., \"HousecallPro\", \"Jobber\", \"API\", \"MANUAL\"). Defaults to \"API\" if not specified.", "example": "HousecallPro" }, "notes": { "type": "string", "nullable": true, "description": "Optional notes about the conversion", "example": "Customer signed annual contract" }, "sourceData": { "type": "object", "nullable": true, "description": "Integration-specific metadata", "example": { "crmId": "SF-12345", "salesRep": "John Doe" }, "additionalProperties": true }, "createdByCompanyUserId": { "type": "string", "format": "uuid", "nullable": true, "description": "User ID who created the conversion (null for API calls)", "example": null }, "createdAt": { "type": "string", "format": "date-time", "description": "Timestamp when the conversion was created", "example": "2024-01-15T10:30:00Z" }, "updatedAt": { "type": "string", "format": "date-time", "description": "Timestamp when the conversion was last updated", "example": "2024-01-15T14:45:00Z" }, "isUpdate": { "type": "boolean", "description": "Indicates whether this was an update (true) or a new creation (false)", "example": true }, "lead": { "type": "object", "description": "Lead record status updated for the Lead Workflow board.", "properties": { "id": { "type": "string", "format": "uuid", "description": "Lead ID whose workflow-board status was updated" }, "conversionStatus": { "type": "string", "description": "Current lead workflow-board status", "example": "WON" }, "conversionStatusUpdatedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the lead workflow-board status was updated" } } } } } } }, "LeadReviewRequestCreate": { "type": "object", "properties": { "leadId": { "type": "string", "format": "uuid", "description": "Lead ID to associate the review request with. If provided, this is used directly.\nYou may provide either leadId or phone. If both are provided, leadId is used.\n", "example": "123e4567-e89b-12d3-a456-426614174000" }, "phone": { "type": "string", "description": "Phone number of the lead to resolve the most recent lead submission for this company.\nE.164 format is recommended (e.g., +15551234567); US 10-digit formats are also accepted and normalized.\nYou may provide either phone or leadId. If both are provided, leadId is used.\n", "example": "+15551234567" } }, "anyOf": [ { "required": [ "leadId" ] }, { "required": [ "phone" ] } ] }, "LeadReviewRequestResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true }, "data": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "description": "ID of the review request", "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }, "status": { "type": "string", "description": "Status of the review request (PENDING, SENT, COMPLETED, FAILED)", "example": "PENDING" }, "isDuplicate": { "type": "boolean", "description": "True if a review request already existed and was not duplicated", "example": false } } } } }, "PublicInboxAiState": { "type": "string", "enum": [ "enabled", "paused", "mixed", "unavailable" ] }, "PublicInboxAutomation": { "type": "object", "properties": { "aiState": { "$ref": "#/components/schemas/PublicInboxAiState" }, "controlState": { "$ref": "#/components/schemas/PublicInboxAiState" }, "hasOpenHumanEscalation": { "type": "boolean" }, "openEscalationCount": { "type": "integer", "minimum": 0 } }, "required": [ "aiState", "controlState", "hasOpenHumanEscalation", "openEscalationCount" ], "additionalProperties": false }, "PublicInboxSummary": { "type": "object", "properties": { "clientId": { "type": "string", "format": "uuid" }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "companyName": { "type": "string", "nullable": true }, "primaryPhone": { "type": "string", "nullable": true }, "primaryEmail": { "type": "string", "nullable": true }, "leadStatus": { "type": "string" }, "assignedTo": { "type": "string", "format": "uuid", "nullable": true }, "lastActivityAt": { "type": "string", "format": "date-time" }, "archived": { "type": "boolean" }, "automation": { "$ref": "#/components/schemas/PublicInboxAutomation" }, "lastAiBookedAt": { "type": "string", "format": "date-time", "nullable": true }, "hasUpcomingAiBooking": { "type": "boolean", "nullable": true }, "nextUpcomingAiBookingId": { "type": "string", "nullable": true }, "unknownAiBookingCount": { "type": "integer", "minimum": 0 }, "bookingCoverage": { "type": "string", "enum": [ "local_appointments_and_recorded_booking_actions" ] }, "openBookingChangeRequestCount": { "type": "integer", "minimum": 0 }, "hasOpenBookingChangeRequest": { "type": "boolean" }, "matchedBookingIds": { "type": "array", "items": { "type": "string" }, "maxItems": 25 }, "matchedBookingsHaveMore": { "type": "boolean" } }, "required": [ "clientId", "firstName", "lastName", "companyName", "primaryPhone", "primaryEmail", "leadStatus", "assignedTo", "lastActivityAt", "archived", "automation", "lastAiBookedAt", "hasUpcomingAiBooking", "nextUpcomingAiBookingId", "unknownAiBookingCount", "bookingCoverage", "openBookingChangeRequestCount", "hasOpenBookingChangeRequest", "matchedBookingIds", "matchedBookingsHaveMore" ], "additionalProperties": false }, "PublicInboxPage": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/PublicInboxSummary" } }, "nextCursor": { "type": "string", "nullable": true }, "asOf": { "type": "string", "format": "date-time" } }, "required": [ "items", "nextCursor", "asOf" ], "additionalProperties": false }, "PublicInboxError": { "type": "object", "properties": { "success": { "type": "boolean", "enum": [ false ] }, "error": { "type": "object", "properties": { "code": { "type": "string" }, "message": { "type": "string" }, "retryAfterSeconds": { "type": "integer", "minimum": 1 } }, "required": [ "code", "message" ], "additionalProperties": false } }, "required": [ "success", "error" ], "additionalProperties": false }, "PublicInboxChannel": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat", "phone" ] }, "PublicInboxMessage": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string", "format": "uuid" }, "leadInquiryId": { "type": "string", "format": "uuid", "nullable": true }, "kind": { "type": "string", "enum": [ "message" ] }, "channel": { "$ref": "#/components/schemas/PublicInboxChannel" }, "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "actor": { "type": "string", "enum": [ "contact", "ai", "human", "api", "automation", "unknown" ] }, "text": { "type": "string", "nullable": true }, "textCompleteness": { "type": "string", "enum": [ "complete", "preview", "truncated", "unavailable" ] }, "subject": { "type": "string", "nullable": true }, "occurredAt": { "type": "string", "format": "date-time" }, "ingestedAt": { "type": "string", "format": "date-time" }, "deliveryStatus": { "type": "string", "nullable": true } }, "required": [ "id", "clientId", "leadInquiryId", "kind", "channel", "direction", "actor", "text", "textCompleteness", "subject", "occurredAt", "ingestedAt", "deliveryStatus" ], "additionalProperties": false }, "PublicInboxMessagesPage": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/PublicInboxMessage" } }, "nextCursor": { "type": "string", "nullable": true }, "coverage": { "type": "string", "enum": [ "stored_sources_and_index" ] } }, "required": [ "items", "nextCursor", "coverage" ], "additionalProperties": false }, "PublicInboxSendTarget": { "type": "object", "properties": { "sendTargetId": { "type": "string" }, "sendMethod": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat" ] }, "leadInquiryId": { "type": "string", "format": "uuid", "nullable": true }, "label": { "type": "string" }, "enabled": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string" } }, "aiState": { "$ref": "#/components/schemas/PublicInboxAiState" }, "allowedAiReplies": { "type": "array", "items": { "type": "string", "enum": [ "off", "on" ] } }, "version": { "type": "string" }, "maxTextLength": { "type": "integer", "minimum": 0 }, "supportsAttachments": { "type": "boolean", "enum": [ false ] }, "requiresProviderRefresh": { "type": "boolean" } }, "required": [ "sendTargetId", "sendMethod", "leadInquiryId", "label", "enabled", "blockedReasonCodes", "aiState", "allowedAiReplies", "version", "maxTextLength", "supportsAttachments", "requiresProviderRefresh" ], "additionalProperties": false }, "PublicInboxSendTargetsPage": { "type": "object", "properties": { "clientId": { "type": "string", "format": "uuid" }, "asOf": { "type": "string", "format": "date-time" }, "sendingAvailable": { "type": "boolean" }, "items": { "type": "array", "items": { "$ref": "#/components/schemas/PublicInboxSendTarget" } }, "nextCursor": { "type": "string", "nullable": true }, "methods": { "type": "array", "items": { "type": "object", "properties": { "sendMethod": { "type": "string", "enum": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat" ] }, "hasTargets": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string" } } }, "required": [ "sendMethod", "hasTargets", "blockedReasonCodes" ], "additionalProperties": false } } }, "required": [ "clientId", "asOf", "sendingAvailable", "items", "nextCursor", "methods" ], "additionalProperties": false }, "PublicInboxConversation": { "type": "object", "properties": { "contact": { "type": "object", "properties": { "clientId": { "type": "string", "format": "uuid" }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "companyName": { "type": "string", "nullable": true }, "primaryPhone": { "type": "string", "nullable": true }, "primaryEmail": { "type": "string", "nullable": true }, "leadStatus": { "type": "string" }, "assignedTo": { "type": "string", "format": "uuid", "nullable": true }, "lastActivityAt": { "type": "string", "format": "date-time" }, "archived": { "type": "boolean" }, "automation": { "$ref": "#/components/schemas/PublicInboxAutomation" }, "lastAiBookedAt": { "type": "string", "format": "date-time", "nullable": true }, "hasUpcomingAiBooking": { "type": "boolean", "nullable": true }, "nextUpcomingAiBookingId": { "type": "string", "nullable": true }, "unknownAiBookingCount": { "type": "integer", "minimum": 0 }, "bookingCoverage": { "type": "string", "enum": [ "local_appointments_and_recorded_booking_actions" ] }, "openBookingChangeRequestCount": { "type": "integer", "minimum": 0 }, "hasOpenBookingChangeRequest": { "type": "boolean" }, "matchedBookingIds": { "type": "array", "items": { "type": "string" }, "maxItems": 25 }, "matchedBookingsHaveMore": { "type": "boolean" }, "primaryPhoneOptOut": { "type": "boolean" }, "primaryEmailOptOut": { "type": "boolean" }, "address": { "type": "string", "nullable": true }, "city": { "type": "string", "nullable": true }, "state": { "type": "string", "nullable": true }, "zipCode": { "type": "string", "nullable": true }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": [ "clientId", "firstName", "lastName", "companyName", "primaryPhone", "primaryEmail", "leadStatus", "assignedTo", "lastActivityAt", "archived", "automation", "lastAiBookedAt", "hasUpcomingAiBooking", "nextUpcomingAiBookingId", "unknownAiBookingCount", "bookingCoverage", "openBookingChangeRequestCount", "hasOpenBookingChangeRequest", "matchedBookingIds", "matchedBookingsHaveMore", "primaryPhoneOptOut", "primaryEmailOptOut", "address", "city", "state", "zipCode", "tags" ], "additionalProperties": false }, "version": { "type": "string" }, "asOf": { "type": "string", "format": "date-time" }, "inquiries": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "source": { "type": "string", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "status": { "type": "string", "nullable": true } }, "required": [ "id", "source", "createdAt", "status" ], "additionalProperties": false } }, "inquiriesHaveMore": { "type": "boolean" }, "messages": { "$ref": "#/components/schemas/PublicInboxMessagesPage" }, "sendMethods": { "$ref": "#/components/schemas/PublicInboxSendTargetsPage" } }, "required": [ "contact", "version", "asOf", "inquiries", "inquiriesHaveMore", "messages", "sendMethods" ], "additionalProperties": false }, "PublicInboxCall": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, "clientId": { "type": "string", "format": "uuid", "nullable": true }, "kind": { "type": "string", "enum": [ "ai", "outbound", "missed" ] }, "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "status": { "type": "string" }, "fromPhone": { "type": "string", "nullable": true }, "toPhone": { "type": "string", "nullable": true }, "summary": { "type": "string", "nullable": true }, "durationSeconds": { "type": "integer", "minimum": 0, "nullable": true }, "startedAt": { "type": "string", "format": "date-time", "nullable": true }, "endedAt": { "type": "string", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "hasTranscript": { "type": "boolean" }, "hasRecording": { "type": "boolean" } }, "required": [ "id", "clientId", "kind", "direction", "status", "fromPhone", "toPhone", "summary", "durationSeconds", "startedAt", "endedAt", "createdAt", "updatedAt", "hasTranscript", "hasRecording" ], "additionalProperties": false }, "PublicInboxBooking": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^(?:appointment:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|booking-action:[1-9][0-9]{0,18})$" }, "clientId": { "type": "string", "format": "uuid", "nullable": true }, "leadInquiryId": { "type": "string", "format": "uuid", "nullable": true }, "provider": { "type": "string" }, "externalBookingId": { "type": "string", "nullable": true }, "appointmentId": { "type": "string", "format": "uuid", "nullable": true }, "origin": { "type": "string", "enum": [ "ai", "human", "customer", "unknown" ] }, "originEvidence": { "type": "string", "enum": [ "voice_agent", "successful_booking_action", "admin_ui", "web_portal", "unknown" ] }, "status": { "type": "string" }, "statusFreshness": { "type": "string", "enum": [ "local_record", "last_observed" ] }, "observedAt": { "type": "string", "format": "date-time" }, "title": { "type": "string", "nullable": true }, "startAt": { "type": "string", "format": "date-time", "nullable": true }, "endAt": { "type": "string", "format": "date-time", "nullable": true }, "timeSource": { "type": "string", "enum": [ "appointment", "requested_slot" ] }, "bookedAt": { "type": "string", "format": "date-time" }, "openChangeRequestCount": { "type": "integer", "minimum": 0 } }, "required": [ "id", "clientId", "leadInquiryId", "provider", "externalBookingId", "appointmentId", "origin", "originEvidence", "status", "statusFreshness", "observedAt", "title", "startAt", "endAt", "timeSource", "bookedAt", "openChangeRequestCount" ], "additionalProperties": false }, "PublicInboxTimelineItem": { "oneOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" }, "kind": { "type": "string", "enum": [ "message" ] }, "message": { "$ref": "#/components/schemas/PublicInboxMessage" } }, "required": [ "id", "occurredAt", "kind", "message" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" }, "kind": { "type": "string", "enum": [ "call" ] }, "call": { "$ref": "#/components/schemas/PublicInboxCall" } }, "required": [ "id", "occurredAt", "kind", "call" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" }, "kind": { "type": "string", "enum": [ "booking" ] }, "booking": { "$ref": "#/components/schemas/PublicInboxBooking" } }, "required": [ "id", "occurredAt", "kind", "booking" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" }, "kind": { "type": "string", "enum": [ "escalation" ] }, "escalation": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "status": { "type": "string" } }, "required": [ "id", "status" ], "additionalProperties": false } }, "required": [ "id", "occurredAt", "kind", "escalation" ], "additionalProperties": false }, { "type": "object", "properties": { "id": { "type": "string" }, "occurredAt": { "type": "string", "format": "date-time" }, "kind": { "type": "string", "enum": [ "activity" ] }, "activity": { "type": "object", "properties": { "type": { "type": "string" }, "leadInquiryId": { "type": "string", "format": "uuid", "nullable": true }, "bookingChangeRequestId": { "type": "string", "format": "uuid" }, "status": { "type": "string" } }, "required": [ "type", "leadInquiryId" ], "additionalProperties": false } }, "required": [ "id", "occurredAt", "kind", "activity" ], "additionalProperties": false } ] }, "PublicInboxTimelinePage": { "type": "object", "properties": { "items": { "type": "array", "items": { "$ref": "#/components/schemas/PublicInboxTimelineItem" } }, "nextCursor": { "type": "string", "nullable": true }, "coverage": { "type": "string", "enum": [ "stored_sources_and_index" ] } }, "required": [ "items", "nextCursor", "coverage" ], "additionalProperties": false }, "PublicInboxMessageId": { "type": "string", "pattern": "^(?:message:[0-9a-f]{32}(?::[1-9][0-9]*)?|history:[1-9][0-9]*)$" }, "PublicInboxMessageBody": { "type": "object", "properties": { "messageId": { "$ref": "#/components/schemas/PublicInboxMessageId" }, "format": { "type": "string", "enum": [ "plain_text" ] }, "availability": { "type": "string", "enum": [ "complete", "preview", "unavailable" ] }, "text": { "type": "string" }, "totalCharacters": { "type": "integer", "minimum": 0 }, "nextCursor": { "type": "string", "nullable": true } }, "required": [ "messageId", "format", "availability", "text", "totalCharacters", "nextCursor" ], "additionalProperties": false }, "PublicInboxCallsPage": { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, "clientId": { "type": "string", "format": "uuid", "nullable": true }, "kind": { "type": "string", "enum": [ "ai", "outbound", "missed" ] }, "direction": { "type": "string", "enum": [ "inbound", "outbound" ] }, "status": { "type": "string" }, "fromPhone": { "type": "string", "nullable": true }, "toPhone": { "type": "string", "nullable": true }, "summary": { "type": "string", "nullable": true }, "durationSeconds": { "type": "integer", "minimum": 0, "nullable": true }, "startedAt": { "type": "string", "format": "date-time", "nullable": true }, "endedAt": { "type": "string", "format": "date-time", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "hasTranscript": { "type": "boolean" }, "hasRecording": { "type": "boolean" } }, "required": [ "id", "clientId", "kind", "direction", "status", "fromPhone", "toPhone", "summary", "durationSeconds", "startedAt", "endedAt", "createdAt", "updatedAt", "hasTranscript", "hasRecording" ], "additionalProperties": false } }, "nextCursor": { "type": "string", "nullable": true } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }, "PublicInboxTranscript": { "type": "object", "properties": { "callId": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, "availability": { "type": "string", "enum": [ "available", "not_available" ] }, "format": { "type": "string", "enum": [ "plain_text" ] }, "text": { "type": "string" }, "nextCursor": { "type": "string", "nullable": true } }, "required": [ "callId", "availability", "format", "text", "nextCursor" ], "additionalProperties": false }, "PublicInboxRecordings": { "oneOf": [ { "type": "object", "properties": { "callId": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, "availability": { "type": "string", "enum": [ "available" ] }, "recordings": { "type": "array", "minItems": 1, "maxItems": 2, "items": { "type": "object", "properties": { "variant": { "type": "string", "enum": [ "mono", "stereo", "voicemail" ] }, "url": { "type": "string", "format": "uri", "description": "Five-minute bearer link to the automations streaming proxy. Treat as a secret." }, "expiresAt": { "type": "string", "format": "date-time" } }, "required": [ "variant", "url", "expiresAt" ], "additionalProperties": false } } }, "required": [ "callId", "availability", "recordings" ], "additionalProperties": false }, { "type": "object", "properties": { "callId": { "type": "string", "pattern": "^(?:ai|outbound|missed):[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, "availability": { "type": "string", "enum": [ "proxy_pending", "not_available", "not_archived" ], "description": "proxy_pending means downloads are not configured; not_available means no saved link; not_archived means only unsupported/provider links exist." }, "recordings": { "type": "array", "items": {}, "maxItems": 0 } }, "required": [ "callId", "availability", "recordings" ], "additionalProperties": false } ] }, "PublicInboxBookingsPage": { "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^(?:appointment:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|booking-action:[1-9][0-9]{0,18})$" }, "clientId": { "type": "string", "format": "uuid", "nullable": true }, "leadInquiryId": { "type": "string", "format": "uuid", "nullable": true }, "provider": { "type": "string" }, "externalBookingId": { "type": "string", "nullable": true }, "appointmentId": { "type": "string", "format": "uuid", "nullable": true }, "origin": { "type": "string", "enum": [ "ai", "human", "customer", "unknown" ] }, "originEvidence": { "type": "string", "enum": [ "voice_agent", "successful_booking_action", "admin_ui", "web_portal", "unknown" ] }, "status": { "type": "string" }, "statusFreshness": { "type": "string", "enum": [ "local_record", "last_observed" ] }, "observedAt": { "type": "string", "format": "date-time" }, "title": { "type": "string", "nullable": true }, "startAt": { "type": "string", "format": "date-time", "nullable": true }, "endAt": { "type": "string", "format": "date-time", "nullable": true }, "timeSource": { "type": "string", "enum": [ "appointment", "requested_slot" ] }, "bookedAt": { "type": "string", "format": "date-time" }, "openChangeRequestCount": { "type": "integer", "minimum": 0 } }, "required": [ "id", "clientId", "leadInquiryId", "provider", "externalBookingId", "appointmentId", "origin", "originEvidence", "status", "statusFreshness", "observedAt", "title", "startAt", "endAt", "timeSource", "bookedAt", "openChangeRequestCount" ], "additionalProperties": false } }, "nextCursor": { "type": "string", "nullable": true }, "coverage": { "type": "string", "enum": [ "local_appointments_and_recorded_booking_actions" ] } }, "required": [ "items", "nextCursor", "coverage" ], "additionalProperties": false }, "PublicInboxChangeRequest": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "clientId": { "type": "string", "format": "uuid", "nullable": true }, "leadInquiryId": { "type": "string", "format": "uuid", "nullable": true }, "appointmentId": { "type": "string", "format": "uuid", "nullable": true }, "leadBookingActionId": { "type": "integer", "nullable": true }, "bookingProvider": { "type": "string", "nullable": true }, "externalBookingId": { "type": "string", "nullable": true }, "requestType": { "type": "string", "enum": [ "CANCEL", "RESCHEDULE" ] }, "status": { "type": "string", "enum": [ "REQUESTED", "COMPLETED", "DECLINED", "SUPERSEDED" ] }, "currentSlot": { "type": "object", "properties": { "startIso": { "type": "string", "nullable": true }, "endIso": { "type": "string", "nullable": true }, "timezone": { "type": "string", "nullable": true }, "label": { "type": "string", "nullable": true } }, "required": [ "startIso", "endIso", "timezone", "label" ], "additionalProperties": false }, "requestedSlot": { "type": "object", "properties": { "startIso": { "type": "string", "nullable": true }, "endIso": { "type": "string", "nullable": true }, "timezone": { "type": "string", "nullable": true }, "label": { "type": "string", "nullable": true } }, "required": [ "startIso", "endIso", "timezone", "label" ], "additionalProperties": false }, "reason": { "type": "string", "nullable": true }, "customerMessage": { "type": "string", "nullable": true }, "resolutionNote": { "type": "string", "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "resolvedAt": { "type": "string", "format": "date-time", "nullable": true } }, "required": [ "id", "clientId", "leadInquiryId", "appointmentId", "leadBookingActionId", "bookingProvider", "externalBookingId", "requestType", "status", "currentSlot", "requestedSlot", "reason", "customerMessage", "resolutionNote", "createdAt", "updatedAt", "resolvedAt" ], "additionalProperties": false }, "PublicInboxUsage": { "type": "object", "properties": { "asOf": { "type": "string", "format": "date-time" }, "periodStart": { "type": "string", "format": "date-time" }, "periodEnd": { "type": "string", "format": "date-time" }, "subscriptionStatus": { "type": "string" }, "entitlementSource": { "type": "string", "enum": [ "subscription", "license" ] }, "hasActiveSubscription": { "type": "boolean" }, "overLeadLimit": { "type": "boolean" }, "apiBillingEligible": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } }, "meters": { "type": "object", "properties": { "lead": { "type": "object", "properties": { "unit": { "type": "string", "enum": [ "lead", "sms_segment", "email" ] }, "used": { "type": "number", "minimum": 0 }, "reserved": { "type": "number", "minimum": 0 }, "limit": { "type": "number", "minimum": 0 }, "remaining": { "type": "number", "minimum": 0 }, "source": { "type": "string", "enum": [ "plan", "employee_override" ] } }, "required": [ "unit", "used", "reserved", "limit", "remaining", "source" ], "additionalProperties": false }, "sms": { "type": "object", "properties": { "unit": { "type": "string", "enum": [ "lead", "sms_segment", "email" ] }, "used": { "type": "number", "minimum": 0 }, "reserved": { "type": "number", "minimum": 0 }, "limit": { "type": "number", "minimum": 0 }, "remaining": { "type": "number", "minimum": 0 }, "source": { "type": "string", "enum": [ "plan", "employee_override" ] } }, "required": [ "unit", "used", "reserved", "limit", "remaining", "source" ], "additionalProperties": false }, "email": { "type": "object", "properties": { "unit": { "type": "string", "enum": [ "lead", "sms_segment", "email" ] }, "used": { "type": "number", "minimum": 0 }, "reserved": { "type": "number", "minimum": 0 }, "limit": { "type": "number", "minimum": 0 }, "remaining": { "type": "number", "minimum": 0 }, "source": { "type": "string", "enum": [ "plan", "employee_override" ] } }, "required": [ "unit", "used", "reserved", "limit", "remaining", "source" ], "additionalProperties": false } }, "required": [ "lead", "sms", "email" ], "additionalProperties": false }, "sendingAvailable": { "type": "boolean", "description": "Whether API sending is available company-wide. A company-wide API sending stop returns false; inspect send methods for channel-specific stops and eligibility." }, "billingEligibility": { "type": "object", "properties": { "sms": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false }, "email": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false }, "facebook-messenger": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false }, "google-lsa": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false }, "thumbtack": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false }, "yelp": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false }, "webchat": { "type": "object", "properties": { "allowed": { "type": "boolean" }, "blockedReasonCodes": { "type": "array", "items": { "type": "string", "enum": [ "SUBSCRIPTION_INACTIVE", "LEAD_LIMIT_EXCEEDED", "SMS_LIMIT_EXCEEDED", "EMAIL_LIMIT_EXCEEDED", "BILLING_STATE_UNAVAILABLE" ] } } }, "required": [ "allowed", "blockedReasonCodes" ], "additionalProperties": false } }, "required": [ "sms", "email", "facebook-messenger", "google-lsa", "thumbtack", "yelp", "webchat" ], "additionalProperties": false } }, "required": [ "asOf", "periodStart", "periodEnd", "subscriptionStatus", "entitlementSource", "hasActiveSubscription", "overLeadLimit", "apiBillingEligible", "blockedReasonCodes", "meters", "sendingAvailable", "billingEligibility" ], "additionalProperties": false }, "Lead": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid", "example": "db8db5a7-2f26-4e88-9c2a-969bad0f6a5b" }, "companyId": { "type": "string", "format": "uuid", "example": "f096f9e3-001d-49ac-864c-3d73453bbe08" }, "leadFormId": { "type": "string", "format": "uuid", "nullable": true }, "clientId": { "type": "string", "format": "uuid", "example": "415f2b29-39e6-4182-9d6f-ec2d817f01c2" }, "phone": { "type": "string", "example": "+17345520800", "description": "Phone number in E.164 format" }, "email": { "type": "string", "nullable": true, "example": "john.doe@example.com" }, "firstName": { "type": "string", "nullable": true }, "lastName": { "type": "string", "nullable": true }, "leadFormData": { "type": "object", "properties": { "source": { "type": "string", "example": "missed-call" }, "message": { "type": "string", "example": "Just calling to see how this would work. Thanks." } }, "additionalProperties": false }, "userAgent": { "type": "string", "nullable": true }, "ipAddress": { "type": "string", "nullable": true }, "leadStatus": { "type": "string", "nullable": true }, "leadNotes": { "type": "string", "nullable": true }, "leadQualificationResult": { "type": "object", "nullable": true, "properties": { "response": { "type": "string", "example": "Thank you for this! Bryan our Co-Founder & CEO will be reviewing the conversation shortly and be in touch if there's a fit." }, "hasImages": { "type": "boolean", "default": false }, "dataFields": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string", "example": "customer_name" }, "value": { "type": "string", "example": "Marcus" } } } }, "commonFields": { "type": "object", "properties": { "city": { "type": "string", "nullable": true }, "state": { "type": "string", "nullable": true }, "address": { "type": "string", "nullable": true }, "zipcode": { "type": "string", "nullable": true }, "fullAddress": { "type": "string", "nullable": true }, "isHomeowner": { "type": "boolean", "nullable": true }, "customerName": { "type": "string", "nullable": true } } }, "contactReason": { "type": "string", "nullable": true }, "detectedAbuse": { "type": "boolean", "default": false }, "shouldSkipReply": { "type": "boolean", "default": false }, "allQuestionsAnswered": { "type": "boolean" }, "isClientReadyToBook": { "type": "boolean", "default": false } } }, "leadQualificationStatus": { "type": "string", "enum": [ null, "COMPLETED" ], "nullable": true }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": "string", "format": "date-time" }, "qualificationSource": { "type": "string", "enum": [ "MISSED_CALL", "WEBSITE_TEXTING", "WEBCHAT", "COLD_TEXT_INBOUND", "AI_HANDLED_CALL", "CALENDAR_BOOKING", "INBOUND_EMAIL", "ANGI_LEAD", "YELP_LEAD", "THUMBTACK_LEAD", "EMAIL_QUALIFICATION", "GOOGLE_LSA_DIRECT" ], "example": "EMAIL_QUALIFICATION" }, "qualificationStartedAt": { "type": "string", "format": "date-time", "nullable": true }, "qualificationCompletedAt": { "type": "string", "format": "date-time", "nullable": true }, "isRepeatLead": { "type": "boolean", "default": false }, "previousLeadSubmissionId": { "type": "string", "format": "uuid", "nullable": true }, "conversionStatus": { "type": "string", "nullable": true }, "conversionStatusUpdatedAt": { "type": "string", "format": "date-time", "nullable": true }, "notifiedCompanyOn": { "type": "string", "format": "date-time", "example": "2025-02-16T15:20:51.422Z" }, "missedCallId": { "type": "string", "format": "uuid", "example": "99bd4118-7d7b-4376-9ecf-dc5c0fa37962" }, "lineType": { "type": "string", "enum": [ "mobile", "landline", "voip" ], "example": "mobile" }, "manualTakeoverEnabled": { "type": "boolean", "default": false } } }, "WebhookV2PayloadLeadStatusChanged": { "type": "object", "description": "V2 webhook payload for LEAD_STATUS_CHANGED events.\n\nLegacy lead pipeline event. Use only if your account still uses the legacy\nLeads system. For the new Contacts/client pipeline, use CLIENT_STATUS_CHANGED.\n", "properties": { "eventType": { "type": "string", "enum": [ "LEAD_STATUS_CHANGED" ], "example": "LEAD_STATUS_CHANGED" }, "eventTypeDetails": { "type": "string", "enum": [ "LEAD_STATUS_CHANGED" ], "example": "LEAD_STATUS_CHANGED" }, "leadId": { "type": "string", "format": "uuid" }, "clientId": { "type": "string", "format": "uuid", "nullable": true }, "companyId": { "type": "string", "format": "uuid" }, "previousStatus": { "type": "string", "nullable": true, "example": "NEW" }, "newStatus": { "type": "string", "example": "WON" }, "timestamp": { "type": "string", "format": "date-time" }, "changedByUserId": { "type": "string", "nullable": true, "description": "User who changed the status in LeadTruffle, when available." }, "changedByUserEmail": { "type": "string", "nullable": true, "description": "Email for the user who changed the status, when available." }, "lead": { "type": "object", "additionalProperties": true, "description": "Full lead submission payload after the status change." } } }, "WebhookV2PayloadClientStatusChanged": { "type": "object", "description": "V2 webhook payload for CLIENT_STATUS_CHANGED events in the new Contacts/client pipeline.", "properties": { "eventType": { "type": "string", "enum": [ "CLIENT_STATUS_CHANGED" ], "example": "CLIENT_STATUS_CHANGED" }, "eventTypeDetails": { "type": "string", "enum": [ "CLIENT_STATUS_CHANGED" ], "example": "CLIENT_STATUS_CHANGED" }, "clientId": { "type": "string", "format": "uuid" }, "latestLeadSubmissionId": { "type": "string", "format": "uuid", "nullable": true, "description": "Latest lead inquiry/submission associated with the contact, when available." }, "companyId": { "type": "string", "format": "uuid" }, "previousStatus": { "type": "string", "nullable": true, "example": "NEW" }, "newStatus": { "type": "string", "example": "WON" }, "timestamp": { "type": "string", "format": "date-time" }, "changedByUserId": { "type": "string", "nullable": true, "description": "User who changed the contact status in LeadTruffle, when available." }, "changedByUserEmail": { "type": "string", "nullable": true, "description": "Email for the user who changed the contact status, when available." }, "client": { "type": "object", "additionalProperties": true, "description": "Full contact/client payload after the status change." } } }, "LeadConversionPayload": { "type": "object", "description": "Conversion data for a lead. This object is included in webhook payloads\nwhen conversion tracking has been enabled and conversion data exists for the lead.\n\nThis will be null/undefined if no conversion has been recorded for the lead yet.\n", "properties": { "id": { "type": "string", "format": "uuid", "description": "Unique identifier for the conversion record", "example": "conv_123e4567-e89b-12d3-a456-426614174000" }, "conversionStatus": { "type": "string", "enum": [ "WON", "LOST", "QUOTED", "PENDING", "CONTACTED", "NURTURING", "CLOSED" ], "description": "The status of the lead conversion", "example": "WON" }, "revenueAmount": { "type": "string", "nullable": true, "description": "Revenue amount as a decimal string.\nNull if no revenue was recorded.\n", "example": "1500.00" }, "currency": { "type": "string", "description": "Currency code for the revenue", "example": "USD" }, "source": { "type": "string", "nullable": true, "description": "Source of the conversion record.\nExamples: 'HousecallPro', 'Jobber', 'ServiceTitan', 'API', 'Manual Entry'\n", "example": "HousecallPro" }, "sourceData": { "type": "object", "nullable": true, "description": "Additional data from the integration source.\nStructure varies based on the source system.\n", "example": { "crmRecordId": "REC-12345", "invoiceNumber": "INV-2024-001" } }, "notes": { "type": "string", "nullable": true, "description": "Optional notes about the conversion", "example": "Customer signed contract for kitchen remodel" } } } }, "responses": { "UnauthorizedError": { "description": "Authentication error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "InternalError": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "NotFoundError": { "description": "Resource not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "X-API-Key", "description": "Your LeadTruffle API key. Obtain yours from the LeadTruffle dashboard under Settings → API Keys." } } } } ```