Introduction
TextConvo provides an API to ingest leads and webhooks to receive conversation events. Ingested leads are engaged by AI across your configured journeys.
Base URL
https://api.textconvo.aiScope: The public API currently covers lead ingestion and event webhooks. Voice, RCS, WhatsApp, and email are orchestrated by the platform internally — API access to those channels is on our roadmap.
Quick Start
Try your first API call right away. Replace YOUR_API_KEY with your actual key.
curl -X POST https://api.textconvo.ai/functions/v1/ingest-lead \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Source-Key: YOUR_SOURCE_KEY" \
-H "X-Request-Id: $(uuidgen)" \
-d '{
"phone": "+15035551234",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com"
}'Authentication
Every lead-ingestion request must include all three of the following headers. Missing any one returns 401 or 400.
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key |
X-Source-Key | Yes | Identifies the lead source |
X-Request-Id | Yes | Unique per request (idempotency key) |
POST /functions/v1/ingest-lead
Content-Type: application/json
X-API-Key: YOUR_API_KEY
X-Source-Key: YOUR_SOURCE_KEY
X-Request-Id: 3f1a9c8e-2b4d-4a1e-9c6f-0d2e8b7a1c33HMAC Authentication
HMAC request signing is optional and enabled per source. When enabled, send two additional headers alongside the required auth headers so TextConvo can verify the request was not tampered with in transit.
Additional Headers (when HMAC is enabled)
| Header | Description |
|---|---|
X-TC-Timestamp | Unix timestamp — must be within ±300 seconds of server time |
X-TC-Signature | Hex HMAC-SHA256 signature (case-insensitive compare) |
Signature Formula
Sign the string `${timestamp}.${rawBody}` — the timestamp, a literal dot, then the exact raw request body — keyed with your HMAC secret, and hex-encode the result.
X-TC-Signature = HEX(
HMAC_SHA256(
key = HMAC_SECRET,
data = timestamp + "." + rawBody
)
)Example Request
curl -X POST https://api.textconvo.ai/functions/v1/ingest-lead \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Source-Key: YOUR_SOURCE_KEY" \
-H "X-Request-Id: 3f1a9c8e-2b4d-4a1e-9c6f-0d2e8b7a1c33" \
-H "X-TC-Timestamp: 1710000000" \
-H "X-TC-Signature: 9f86d081884c7d659a2feaa0c55ad015..." \
-d '{ "phone": "+15035551234" }'Note: The signature is compared case-insensitively against the hex you send. Timestamps outside the ±300-second window are rejected.
Lead Ingestion API
/functions/v1/ingest-leadCreate a new lead inside TextConvo. The lead will be matched to an existing contact by phone number or created as a new contact. A journey will be queued automatically based on your account configuration.
Request Headers
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Your API key |
X-Source-Key | Yes | Identifies the lead source |
X-Request-Id | Yes | Unique per request (idempotency key) |
Content-Type | Yes | application/json |
X-TC-Timestamp | If HMAC enabled | Unix timestamp (±300s skew) |
X-TC-Signature | If HMAC enabled | Hex HMAC-SHA256 signature |
Request Body
{
"phone": "string — REQUIRED, E.164 format e.g. +15035551234",
"email": "string (optional)",
"external_id": "string (optional) — your CRM's ID for this lead",
"lead_external_id": "string (optional) — alias of external_id",
"first_name": "string (optional)",
"last_name": "string (optional)",
"address": "string (optional)",
"city": "string (optional)",
"state": "string (optional) — auto-normalized to 2-letter code",
"zip": "string (optional)",
"affiliate_id": "string (optional)",
"ip_address": "string (optional)",
"user_agent": "string (optional)",
"landing_page": "string (optional)",
"metadata": { "any_key": "any_value" },
"custom_fields": {
"any_non_standard_field": "must be nested here, not top-level"
}
}Required field: phone is the only required field. Every other field is optional but validated against a whitelist — any non-standard field must be nested inside custom_fields, never sent top-level. metadata and custom_fields are each capped at 2KB, and state is auto-normalized to a 2-letter code.
Success Response (202 Accepted)
{
"success": true,
"ingestion_request_id": "<uuid>"
}Idempotency
The X-Request-Id header is an idempotency key. If you retry a request with the same X-Request-Id, TextConvo does not ingest the lead again — it returns the original result with "duplicate": true and the same ingestion_request_id. This makes safe retries possible after a network timeout.
{
"success": true,
"duplicate": true,
"ingestion_request_id": "<uuid>"
}Error Response
{
"success": false,
"error": "phone is required",
"code": "MISSING_REQUIRED_FIELD"
}Webhooks
TextConvo sends webhook events as conversations progress — lead acceptance, message delivery and replies, opt-outs, CRM sync, and scheduled-call activity. Each event body includes an event field naming the event.
Emitted Events
lead.accepted— Lead accepted and matched or created as a contactlead.rejected— Lead rejected (validation or dedupe)lead.delivered— Message delivered to the carrierlead.failed— Message failed to deliverlead.reply— Contact replied to a messagelead.click— Contact clicked a tracked linklead.opt_out— Contact opted out of messaginglead.sent— Email message sentlead.crm_updated— Inbound CRM change reflected back into TextConvojourney.crm_pushed— Journey outcome pushed to your connected CRMscheduled_call.confirmed— A scheduled call was confirmedscheduled_call.cancelled— A scheduled call was cancelledsupport.call_me_now— Contact requested an immediate callbacksupport.call_back_later— Contact requested a callback later
Payload Structures
lead.accepted
{
"event": "lead.accepted",
"lead_id": "lead_abc123",
"contact_id": "cnt_xyz789",
"phone": "+15035551234",
"lead_external_id": "crm_00042"
}lead.delivered
{
"event": "lead.delivered",
"message_id": "msg_abc123",
"contact_id": "cnt_xyz789",
"phone": "+15035551234",
"delivered_at": "2026-07-20T10:12:00Z"
}lead.failed
{
"event": "lead.failed",
"message_id": "msg_abc123",
"contact_id": "cnt_xyz789",
"phone": "+15035551234",
"failed_at": "2026-07-20T10:12:00Z",
"error_code": "carrier_rejected"
}lead.reply
{
"event": "lead.reply",
"message_id": "msg_reply123",
"contact_id": "cnt_xyz789",
"phone": "+15035551234",
"message_text": "Yes, send the quote",
"received_at": "2026-07-20T10:18:00Z"
}lead.opt_out
{
"event": "lead.opt_out",
"contact_id": "cnt_xyz789",
"phone": "+15035551234",
"channel": "sms",
"opted_out_at": "2026-07-20T10:25:00Z"
}journey.crm_pushed
{
"event": "journey.crm_pushed",
"contact_id": "cnt_xyz789",
"crm_key": "hubspot",
"outcome": "completed",
"pushed_at": "2026-07-20T11:30:00Z",
"lead_id": "lead_abc123",
"crm_contact_id": "473031564003",
"fields_synced": ["textconvo_journey_status", "textconvo_outcome"]
}Webhook Security
Every webhook TextConvo sends includes the following headers. Verify the signature before trusting the payload.
| Header | Description |
|---|---|
X-TextConvo-Signature | Signature in the form sha256=<hex> |
X-TextConvo-Timestamp | Unix timestamp used in the signed string |
X-TextConvo-Source-Key | Identifies which source the webhook is for |
Recompute HMAC-SHA256 over `${X-TextConvo-Timestamp}.${rawBody}` with your webhook secret, hex-encode it, and compare it against the signature header after stripping the sha256= prefix.
const crypto = require('crypto');
const header = req.headers['x-textconvo-signature'] || '';
const timestamp = req.headers['x-textconvo-timestamp'];
// rawBody is the exact, unparsed request body string.
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const received = header.replace(/^sha256=/, '');
const isValid = crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex')
);Rate Limits
Rate limits are configured per API key. Defaults are 60 requests/minute and 10 requests/second. Exceeding limits returns HTTP 429 — contact us to adjust limits or enable overflow queueing for burst traffic.
Limit Exceeded
When a limit is exceeded, TextConvo returns HTTP 429.
{
"error": "Rate limit exceeded",
"retry_after": 30
}Error Codes
| Code | Description |
|---|---|
400 | Bad Request — Invalid parameters |
401 | Unauthorized — Invalid API key |
403 | Forbidden — Insufficient permissions |
429 | Rate Limit — Too many requests |
500 | Server Error — Internal error |
CRM Integrations
Overview
TextConvo supports bi-directional CRM integration. Your CRM sends leads via webhook, TextConvo runs the journey, and pushes results back automatically.
CRM → Webhook → TextConvo → Journey → Push results back → CRMHubSpot Integration Guide
Prerequisites
- HubSpot account (any paid plan)
- TextConvo account with API access
- Super Admin access in HubSpot
Step 1 — Create a Legacy App in HubSpot
- Go to app-na2.hubspot.com
- Click Settings (gear icon) → Integrations → Legacy Apps
- Click "Create legacy app" → Select "Private"
- Name it "TextConvo"
- Go to Scopes tab and add crm.objects.contacts.read and crm.objects.contacts.write
- Click "Create app"
- Go to Auth tab → click "Show token"
- Copy your Access Token (starts with pat-)
Step 2 — Create custom contact properties
In HubSpot → Settings → Data Management → Properties → Contact properties → Create property:
| Property label | Internal name | Field type |
|---|---|---|
| TextConvo Journey Status | textconvo_journey_status | Single-line text |
| TextConvo Journey Date | textconvo_journey_date | Date picker |
| TextConvo Outcome | textconvo_outcome | Single-line text |
| TextConvo Notes | textconvo_notes | Multi-line text |
Step 3 — Connect in TextConvo
- Go to Integrations → CRM Connectors → HubSpot
- Click "Connect"
- Paste your Access Token (pat-na2-...)
- Paste your Webhook Client Secret (found in HubSpot → Settings → Integrations → Private Apps → your app → Auth tab → Client Secret)
- Review field mappings (defaults are recommended)
- Select outbound events to sync back
- Click "Save & Activate"
Step 4 — Configure HubSpot webhook
- In your HubSpot app → Webhooks tab
- Set Target URL to your TextConvo webhook URL:
- In TextConvo → Integrations → CRM Connectors → HubSpot → Manage
- Copy the Webhook URL shown at the top
- Paste it into HubSpot as the Target URL
- Subscribe to Contact → Created and Contact → Property changed events
- Include properties: firstname, lastname, phone, email, company
- Click Subscribe
Step 5 — Test the integration
- Create a test contact in HubSpot with a phone number
- Check TextConvo → Integrations → HubSpot → Webhook logs
- Verify contact appears in TextConvo contacts
- Check HubSpot contact for TextConvo properties after journey completes
Field Mapping Reference
| HubSpot field | TextConvo field |
|---|---|
firstname | first_name |
lastname | last_name |
phone | phone (required) |
email | |
company | company |
hs_lead_source | lead_source |
Troubleshooting
| Issue | Solution |
|---|---|
| Webhook logs show “Invalid signature” | Check Client Secret is correct in TextConvo |
| Contact created but no journey | Phone number missing on HubSpot contact |
| Push fails with 400 | Custom HubSpot properties not created (Step 2) |
| Push fails with 401 | Access Token expired — reconnect in TextConvo |
HubSpot Integration FAQ
GoHighLevel Integration Guide
Prerequisites
- GoHighLevel account (any Agency plan)
- TextConvo account with API access
- Super Admin access in TextConvo
Step 1 — Create a Private Integration Token in GHL
- Log into GHL and switch to your Sub-Account
- Go to Settings → Private Integrations → Create New Integration
- Name it "TextConvo"
- Select these scopes:
contacts.readonlycontacts.writelocations.readonly
- Click Create and copy the token immediately (starts with
pit-)
You cannot view this token again after leaving the page.
Step 2 — Find your Location ID
- In GHL, switch to your Sub-Account
- Look at the URL in your browser:
app.gohighlevel.com/location/{YOUR_LOCATION_ID}/dashboardCopy the Location ID from the URL.
Step 3 — Create custom contact fields in GHL
In GHL → Settings → Custom Fields → Add Field (type: Text) for each:
| Field Key | Purpose |
|---|---|
textconvo_journey_status | Journey completion status |
textconvo_journey_outcome | Journey outcome (qualified, not interested etc) |
textconvo_reentry_token | Triggers re-entry into a new journey |
Step 4 — Connect in TextConvo
- Go to Integrations → CRM Connectors → GoHighLevel
- Click "Connect"
- Select your Account
- Paste your Private Integration Token (pit-xxx)
- Paste your Sub-Account Location ID
- Review field mappings (defaults recommended)
- Select outbound events to sync back
- Click "Save & Activate"
Step 5 — Set up TWO GHL Workflows
GHL uses Workflows to send webhooks. You need two separate workflows.
Get your Webhook URL first: TextConvo → Integrations → CRM Connectors → GoHighLevel → Manage → copy the Webhook URL.
Workflow 1 — New Contacts
- GHL → Automation → Workflows → Create Workflow → Start from Scratch
- Add Trigger: Contact Created (no filters needed)
- Add Action: Webhook
- Method: POST
- URL: paste your TextConvo Webhook URL copied above
- Save and click Publish
Workflow 2 — Contact Updates
- GHL → Automation → Workflows → Create Workflow → Start from Scratch
- Add Trigger: Contact Changed (no filters needed)
- Add Action: Webhook
- Method: POST
- URL: paste the same TextConvo Webhook URL
- Save and click Publish
Step 6 — Test the integration
- Create a test contact in GHL with a phone number
- Check TextConvo → Inquiries — contact should appear within seconds with SOURCE = GoHighLevel
- Update a field on the GHL contact
- Check the Activity Timeline in TextConvo — should show "GoHighLevel contact updated" with the changed field
Field Mapping Reference
| GoHighLevel field | TextConvo field |
|---|---|
firstName | first_name |
lastName | last_name |
phone | phone (required) |
email | |
source | lead_source |
tags | tags |
Troubleshooting
| Issue | Solution |
|---|---|
| No contacts appearing after creation | Confirm Workflow 1 (Contact Created) is Published |
| No timeline events on field changes | Confirm Workflow 2 (Contact Changed) is Published |
| Test connection fails with 401 | Token invalid — recreate Private Integration in GHL |
| Test connection fails with 404 | Location ID incorrect — check URL in GHL sub-account |
| SOURCE shows as “gohighlevel” not “GoHighLevel” | Reconnect the integration — display name fix is live |
GoHighLevel Integration FAQ
Zoho CRM Integration Guide
Prerequisites
- Zoho CRM account (any paid plan)
- TextConvo account with API access
- Super Admin access in TextConvo
Step 1 — Generate an OAuth Access Token
For testing, use the Self Client method:
- Go to api-console.zoho.com
- Click "Self Client"
- Click "Generate Code"
- In the Scope field paste:text
ZohoCRM.modules.contacts.ALL,ZohoCRM.org.READ,ZohoCRM.modules.notes.CREATE - Set duration to 10 minutes
- Click Create — copy the token immediately (starts with
1000.)
For production use, create a Server-based OAuth app to get a refresh token that never expires. Self Client tokens expire in 10 minutes.
Step 2 — Connect in TextConvo
- Go to Integrations → CRM Connectors → Zoho CRM
- Click Connect
- Select your Account
- Paste your OAuth Access Token (starts with
1000.) - Enter any value for Webhook Secret (e.g.
test123) — optional for testing - Review field mappings (defaults recommended)
- Select outbound events to sync back
- Click Save & Activate
Get your Webhook URL from: TextConvo → Integrations → CRM Connectors → Zoho CRM → Manage → Webhook URL.
Step 3 — Create TWO Workflow Rules in Zoho CRM
Zoho sends webhooks via Workflow Rules. You need two separate rules.
Rule 1 — New Contacts
- Zoho CRM → Setup → Automation → Workflow Rules → Create Rule
- Module: Contacts
- Rule Name: TextConvo — Contact Created
- Execute On: Record Action → Create
- Condition: All Contacts → Done
- Instant Action: Webhook
- URL: paste your TextConvo Webhook URL
- Method: POST
- Body Type: Raw → Format: JSON
- Body:
{
"contact_id": "${!Contacts.id}",
"First_Name": "${!Contacts.First_Name}",
"Last_Name": "${!Contacts.Last_Name}",
"Phone": "${!Contacts.Phone}",
"Mobile": "${!Contacts.Mobile}",
"Email": "${!Contacts.Email}",
"operation": "insert"
}- Save and Associate → Activate the rule
Rule 2 — Contact Updates
- Create another Workflow Rule
- Module: Contacts
- Rule Name: TextConvo — Contact Updated
- Execute On: Record Action → Edit
- Condition: All Contacts → Done
- Instant Action: Webhook
- URL: same TextConvo Webhook URL
- Method: POST
- Body Type: Raw → Format: JSON
- Body:
{
"contact_id": "${!Contacts.id}",
"First_Name": "${!Contacts.First_Name}",
"Last_Name": "${!Contacts.Last_Name}",
"Phone": "${!Contacts.Phone}",
"Mobile": "${!Contacts.Mobile}",
"Email": "${!Contacts.Email}",
"operation": "update"
}- Save and Associate → Activate the rule
Important: The merge field syntax requires ${!Contacts.field_name} with the exclamation mark and underscores. Spaces in field names are not supported.
Step 4 — Create custom fields in Zoho for outbound sync
In Zoho CRM → Setup → Customization → Modules → Contacts → Fields → Add Custom Field (type: Single Line Text):
- Field name:
TextConvo_Journey_Status - Field name:
TextConvo_Journey_Outcome
Step 5 — Test the integration
- Create a new contact in Zoho CRM with a phone number (include country code e.g. +1)
- Check TextConvo → Inquiries — contact should appear within seconds with SOURCE = Zoho
- Edit a field on that same contact (e.g. change first name)
- Check the Activity Timeline in TextConvo — should show "Zoho contact updated" with the changed field
Field Mapping Reference
| Zoho CRM field | TextConvo field |
|---|---|
First_Name | first_name |
Last_Name | last_name |
Phone | phone (required) |
Email | |
Lead_Source | lead_source |
Important Notes
- OAuth tokens from Self Client expire in 10 minutes (testing only)
- For production, use a Server-based OAuth app to get a non-expiring refresh token
- Two workflow rules are required — one for Create, one for Edit
- Merge fields must use
${!Contacts.field_name}syntax with ! and underscores - Zoho limits webhooks to 10 fields maximum per notification
Troubleshooting
| Issue | Solution |
|---|---|
| No contacts appearing | Confirm Rule 1 (Create) is Active in Zoho |
| No timeline events on edits | Confirm Rule 2 (Edit) is Active and body has operation=update |
| Connection invalid after 10 min | OAuth token expired — regenerate from api-console.zoho.com |
| Duplicate inquiries on edit | Check Rule 2 body — operation must be "update" not "insert" |
| Merge field error on save | Use ${!Contacts.First_Name} syntax — exclamation mark required |
| Test connection fails 401 | Token expired — generate a new one from Self Client |
Zoho CRM Integration FAQ
Pipedrive
Coming soonDocumentation coming soon. Contact us to join the early access list.
Contact usZendesk
Coming soonDocumentation coming soon. Contact us to join the early access list.
Contact usSalesforce
Coming soonDocumentation coming soon. Contact us to join the early access list.
Contact usPush Journey Results
Pushing journey results back to your CRM is not a separate endpoint you call. When a journey outcome is ready, TextConvo automatically writes it back to any connected CRM and emits the journey.crm_pushed webhook event (documented under Webhooks). Listen for that event to know when a result has synced.
CRM → Webhook → TextConvo → Journey → Auto write-back + journey.crm_pushed → CRMjourney.crm_pushed payload
{
"event": "journey.crm_pushed",
"contact_id": "cnt_xyz789",
"crm_key": "hubspot",
"outcome": "completed",
"pushed_at": "2026-07-20T11:30:00Z",
"lead_id": "lead_abc123",
"crm_contact_id": "473031564003",
"fields_synced": ["textconvo_journey_status", "textconvo_outcome"]
}