CRM & Webhooks
Connect your agents to customer data and external systems. The Get User tool hydrates the agent with caller identity at the start of every conversation and on demand mid-call. Webhooks push data out to any external service in real time.
Get User pulls caller / visitor information from one or more sources, normalises it into a set of {{variable}} slots that substitute into your greeting + system prompt, and exposes the same data to the agent’s get_user() function for mid-conversation lookups.
Four sources, multi-select
You can enable any combination — they run in priority order and later sources can use earlier sources’ values.
| Source | What it is | Outbound? |
|---|
| URL (Web Embed) | Reads variables from the embed URL’s query string or dispatch metadata (phone calls, outbound campaigns). | No |
| Webhook | POSTs the caller’s identifier to your CRM endpoint; flattens the JSON response into variables. | Yes (HTTP) |
| API (generic REST) | Like Webhook, but you control the method, URL template, headers, and body. JSONPath response map projects nested fields. | Yes (HTTP) |
| Database (your own Postgres) | Runs a pre-approved SELECT against your own Postgres with bound parameters. Read-only, write keywords blocked, statement timeout enforced. Agents never write SQL. | Yes (DB) |
The Database source connects to your own Postgres only. ThinnestAI’s platform tables are not queryable from your agents — your data stays in your control.
Search By
You decide which fields the agent is allowed to use as the lookup identifier:
- Name — agent passes the caller’s name to
get_user("Sarah Johnson").
- Contact Number — agent passes the caller’s phone.
- Other — add as many labelled fields as you need (Customer ID, Email, Account Number, Loyalty Number, …). Each becomes its own searchable key.
These fields double as {{variable}} slots in your greeting and system prompt. Even if you don’t enable any DB/API/Webhook lookup, just declaring “Customer ID” in Search By + capturing it from the embed URL means you can write Hello {{customer_id}}! in your greeting.
- Navigate to your agent → Tools → Add Tool → Get User.
- Search By: tick Name and/or Contact Number, and add Other rows for any custom identifiers your CRM uses.
- Get User Details From: enable one or more sources. Click each source in the sidebar to configure it.
- Save.
The Get User modal opens as a wide sidebar+content view — pick Search By, the timing options, or any of the four sources in the left rail; the editor for the selected item fills the right pane.
When the lookup runs
The first item in the sidebar — Get Details / User Data — controls when enabled sources execute. Two independent toggles:
| Toggle | Effect |
|---|
| Before agent speaks | Prefetch runs during connection setup. Resolved fields substitute into {{variables}} in your greeting + system prompt before the first word is spoken. Recommended for the “Hi Priya” greeting use case. |
| In call (by the agent) | Exposes get_user() to the LLM so the agent can look users up mid-conversation. The agent decides when to call it based on your system prompt (e.g. “If the caller mentions a different account, call get_user with that customer ID.”). |
Enable either, both, or neither — most setups want both on.
Source 1 — URL (Web Embed)
For the chat widget and try-voice-call panel, the embed URL’s query params travel with the conversation. For phone calls, the same field carries outbound-campaign custom variables.
Config:
| Setting | Description | Example |
|---|
| Allowed keys | Comma-separated whitelist of param names to harvest. Leave empty to allow all. | customer_id, email, plan |
| Default values | JSON object of fallback values when a key wasn’t supplied. | {"name": "there"} |
Embed URL example:
https://yoursite.com/chat?customer_id=C-987&email=jane@x.com&plan=Pro
The agent now sees {{customer_id}} = C-987, {{email}} = jane@x.com, {{plan}} = Pro.
Outbound campaign example: when you launch a campaign, each contact row’s custom variables (e.g. {"customer_id": "C-987", "plan": "Pro"}) ride along with the dispatched call and land in the same URL-source slot.
Source 2 — Webhook (CRM lookup)
Fires POST <your-url> with {"identifier": "<lookup>"} at conversation start. Response JSON’s top-level fields become {{variables}}.
Config:
| Setting | Description | Example |
|---|
| URL | Your CRM lookup endpoint | https://api.yourcrm.com/lookup |
| Auth header / value | Sent with every request. Stored encrypted. | Authorization: Bearer sk-... |
| Identifier param | JSON body key carrying the lookup | phone, email, customer_id |
| Response map | Optional: project nested JSON into flat vars. | {"name": "$.user.name", "plan": "$.user.plan"} |
Request:
POST https://api.yourcrm.com/lookup
Authorization: Bearer sk-...
Content-Type: application/json
{ "identifier": "+15551234567" }
Response:
{
"name": "Sarah Johnson",
"email": "sarah@company.com",
"account_type": "Enterprise",
"account_status": "Active",
"last_order": "2026-02-28"
}
Variables now available: {{name}}, {{email}}, {{account_type}}, {{account_status}}, {{last_order}}.
Source 3 — API (generic REST)
The full power of HTTP. Use this when your CRM expects a GET with the identifier in the URL, or a custom JSON body, or you need to call the same endpoint with different params per session.
Config:
| Setting | Description | Example |
|---|
| URL template | Supports {placeholders} from earlier sources or {lookup} | https://api.example.com/users/{contact_no} |
| Method | GET, POST, PUT, PATCH, DELETE | GET |
| Headers | JSON map; values support {placeholders} | {"X-Api-Key": "abc123"} |
| Body template | JSON template (POST/PUT/PATCH); supports {placeholders} | {"phone": "{lookup}"} |
| Response map | JSONPath-style projection into {{vars}} | {"name": "$.firstName", "tier": "$.subscription.tier"} |
Placeholders available inside templates:
{lookup} — what the agent passed to get_user() (or the caller’s phone at conversation start)
{contact_no}, {email}, {customer_id}, {name}, … — anything supplied by earlier sources (URL, Webhook) or that the embed/dispatch provided
- Missing placeholders collapse to empty string — your endpoint should tolerate it
Source 4 — Database (your own Postgres)
Connect the agent to your own Postgres database for direct lookups. The agent never writes SQL — you define query templates with named parameters, and the agent only supplies values.
Security model:
- Connection is opened read-only for every query.
- Write keywords (
INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE, GRANT, REVOKE, COPY ... FROM, …) are rejected before the query reaches your DB.
- Only a single SQL statement per template — stacked statements are blocked.
- Statement timeout caps every query at 5 seconds.
- Results are capped to 50 rows per call.
- Values are bound as parameters — there is no string interpolation into SQL.
- Credentials are stored encrypted in the platform.
Connection:
| Setting | Description |
|---|
| Host, Port, Database, User | Your Postgres connection details |
| Password | Stored encrypted; leave blank on later edits to keep the saved value |
| SSL Mode | disable, prefer, or require (recommended for cloud DBs) |
Click Test Connection to verify the agent can reach your DB.
Query templates:
Each template binds the agent’s lookup arg to one of your Search By keys.
| Field | Description | Example |
|---|
| ID | Identifier for the template (any slug) | by_email |
| Search By | Which Search By field this template binds | email, contact_no, customer_id, … or any |
| Mode | Visual (point-and-click) or SQL (write it yourself) | |
| SQL | SELECT only. Named bind syntax: %(field_name)s | SELECT id, name, plan FROM users WHERE email = %(email)s LIMIT 1 |
| Response map | Project columns onto {{variable}} names | {"user_id": "id", "name": "name", "plan": "plan"} |
Visual builder: if you’d rather not write SQL, switch to Visual mode. Pick the table from a searchable dropdown (populated from your DB’s information_schema — type to filter when you have hundreds of tables), multi-select columns with search + Select all, add WHERE conditions with operator + bound parameter, set ORDER BY + LIMIT. The generated SQL preview updates live and is validated against your DB on every change.
Bulk-add tables
If your schema has many tables and you want one query per table — for example, an agent that should be able to read from users, orders, wallets, subscriptions, and a dozen more — use the Bulk button next to + Add in the Queries list.
- Click Bulk → the right pane swaps to the bulk-add view.
- Search for tables and/or click Select all to grab everything visible (the toggle respects your current filter — type
audit_ then click Select all to grab every audit_* table at once).
- (Optional) Configure Apply this WHERE to every query:
- Column — autocompletes from the union of columns across every selected table (so picking 87 tables surfaces every column name you might want).
- Op —
=, !=, LIKE, ILIKE, IN. Defaults to =.
- Bind to — pick which Search By key to bind the value to (e.g.
:email_id, :contact_no, :customer_id).
- A live coverage badge tells you how many of the picked tables actually have the column you typed (green = all match, amber = partial, red = none).
- Click Add N queries — one query template gets created per selected table.
How the WHERE is applied:
| Table has the column? | Generated WHERE |
|---|
| Yes | WHERE "column" = %(bind)s is added, ready to run |
| No | Empty WHERE — you fill it in per-query from the right pane |
Each generated query starts on the visual builder, with the table set, columns empty (SELECT *), and the WHERE row populated where applicable. The first newly-added query is selected automatically so you land on something useful instead of staring at the sidebar.
SQL example — search by email:
SELECT id, name, email, plan, last_login
FROM users
WHERE email = %(email)s
LIMIT 1
SQL example — search by user_id:
SELECT u.id, u.name, u.plan, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE u.id = %(user_id)s
GROUP BY u.id
LIMIT 1
When the agent calls get_user("jane@x.com") and the active template’s Search By is email, the value is bound to %(email)s automatically. You don’t need to plumb the agent’s input anywhere — it’s wired by the Search By config.
How the identifier reaches the lookup
| Where the identifier comes from | What happens |
|---|
Web embed — URL has ?email=jane@x.com | URL source captures it. Available as {{email}} and as %(email)s bind. Zero outbound HTTP. |
| Phone — inbound, no campaign | Only the caller’s phone is known. Bind it to a query with Search By = contact_no. The agent can also ask the caller for more info mid-call. |
| Phone — outbound campaign | The campaign’s per-contact custom variables (any fields you stored) ride with the dispatch and land in URL-source slots. |
| Agent collects mid-conversation | Agent asks for the info, then calls get_user("the_value"). Bound to whichever Search By matches the active query template. |
| Chained: URL gives partial data → DB enriches | URL source returns {customer_id: "C-987"}. Database source’s query WHERE customer_id = %(customer_id)s uses it. |
Variables in greeting + system prompt
Every Search By field and every key in a source’s response map shows up in the {{ }} autocomplete inside your greeting and system prompt editors.
Greeting:
Hello {{name}}! I see you're on our {{plan}} plan.
System prompt:
You are speaking with {{name}}, a {{plan}} customer (account: {{account_status}}).
Their customer ID is {{customer_id}}. Reference their {{last_order}} purchase
if relevant.
If a variable wasn’t supplied for this session, the placeholder stays as literal text (so configure Default values on the URL source for graceful fallbacks like {"name": "there"}).
Source chaining — concrete example
A bank wants the agent to greet the caller by name AND check their account balance.
Setup:
- Search By: Customer ID (key=
customer_id), Contact Number
- URL source: enabled,
allowed_keys: customer_id
- Database source: enabled, query template:
- Search By:
customer_id
- SQL:
SELECT name, account_status, current_balance FROM customers WHERE customer_id = %(customer_id)s LIMIT 1
- Response map:
name → name, status → account_status, balance → current_balance
- Greeting:
Hi {{name}}! Your current balance is ₹{{balance}} and your account is {{status}}.
Flow:
- Caller dials in. The phone number is on file in
campaign_contacts, so the dispatcher attaches their customer_id to the call.
- URL source captures
customer_id = "C-987".
- Database source runs
WHERE customer_id = %(customer_id)s → returns name, status, balance.
- Greeting interpolates → “Hi Jane! Your current balance is ₹45,200 and your account is Active.”
Using lookup data in the conversation
Once Get User has populated the variables, the agent can reference them naturally in its instructions:
At the start of every conversation, the user's profile has already been
loaded.
If the customer is on the Enterprise plan, prioritise their request and
offer to escalate to their dedicated account manager.
If their account status is "Past Due", gently mention the outstanding
balance and offer to help resolve it.
Never read out the customer's full email address — just use their name.
Sample voice call:
[Incoming call from +1-555-123-4567]
[Get User: URL → customer_id="C-987"; Database → name="Sarah", plan="Enterprise"]
Agent: "Hello Sarah! Thanks for calling. I see you're on our Enterprise
plan. How can I help you today?"
Sarah: "I need to upgrade our seat count."
Agent: "Of course! You currently have 50 seats on your Enterprise plan.
How many additional seats would you like to add?"
Custom Webhook Integration
Beyond CRM lookups, you can use webhooks to send data from your agent to any external system.
Sending Data to External Systems
Create a custom webhook tool that pushes data to your backend:
Common webhook use cases:
| Use Case | Trigger | Data Sent |
|---|
| Lead capture | User provides contact info | Name, email, phone, interest |
| Support ticket | User reports an issue | Issue description, severity, user ID |
| Order placement | User confirms an order | Product, quantity, shipping info |
| Appointment booking | Agent books a slot | Date, time, attendee details |
| Call summary | Call ends | Transcript summary, action items, sentiment |
Setting Up an Outbound Webhook
- Go to Tools > Add Tool > Custom Tool (API).
- Configure your webhook endpoint:
# Example: Create a support ticket
Endpoint: https://api.yourapp.com/tickets
Method: POST
Headers:
Authorization: Bearer your-api-key
Content-Type: application/json
- Define the parameters the agent will fill in:
| Parameter | Type | Description |
|---|
subject | string | Ticket subject/title |
description | string | Detailed issue description |
priority | string | low, medium, high, urgent |
customer_email | string | Customer’s email address |
- The agent will call this webhook when it determines a ticket should be created.
Webhook Authentication
Secure your webhooks with one of these methods:
# Bearer token
Authorization: Bearer sk-your-webhook-secret
# API key header
X-API-Key: your-api-key
# HMAC signature (verify on your server)
X-Webhook-Signature: sha256=...
Webhook Response Handling
Your webhook should return a JSON response that the agent can use:
{
"success": true,
"ticket_id": "TKT-4521",
"message": "Ticket created successfully"
}
The agent will use this response to confirm the action to the user:
Agent: "I've created a support ticket for you. Your ticket number is
TKT-4521. Our team will get back to you within 2 hours."
Receiving Webhook Events
You can also trigger agent actions from external events by sending webhooks to the thinnestAI API.
Use Cases
- New lead in CRM — Trigger the agent to send a welcome email.
- Payment received — Notify the agent to update the customer’s account status.
- Support ticket updated — Have the agent follow up with the customer.
Sending Events to thinnestAI
curl -X POST https://api.thinnest.ai/agents/{agent_id}/trigger \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "new_lead",
"data": {
"name": "Alex Rivera",
"email": "alex@startup.com",
"source": "website",
"interest": "Enterprise plan"
}
}'
Example: Full CRM-Connected Voice Agent
Build a voice agent that looks up callers, handles requests, and logs everything to your CRM.
Agent Setup
| Field | Value |
|---|
| Name | Customer Service Agent |
| Model | GPT-4o |
| Tools | Get User (CRM), Custom Webhook (Create Ticket), SMS |
Agent Instructions
You are a customer service agent for Acme SaaS.
ON EVERY CALL:
1. Look up the caller using the Get User tool
2. Greet them by name and reference their account
HANDLING REQUESTS:
- For billing questions: look up their account and answer directly
- For technical issues: create a support ticket with full details
- For upgrade requests: explain the options and offer to process it
- For cancellations: understand why, offer alternatives, escalate if needed
AFTER RESOLUTION:
1. Create a webhook ticket summarizing the interaction
2. Offer to text them a summary or confirmation number via SMS
3. Thank them and ask if there's anything else
TONE: Professional, helpful, empathetic. Never argue with the customer.
Best Practices
CRM Integration
- Cache responses — If your CRM lookup is slow, consider caching frequent lookups.
- Handle missing users — Tell the agent what to do if the user isn’t found in the CRM (e.g., collect their info and create a new record).
- Keep data fresh — CRM data can change. The agent fetches it live on each interaction.
Webhook Reliability
- Return quickly — Your webhook endpoint should respond within 5 seconds. For longer operations, accept the webhook and process asynchronously.
- Return meaningful errors — If something goes wrong, return an error message the agent can relay to the user.
- Idempotency — Design your webhooks to handle duplicate calls gracefully.
Security
- Validate webhook signatures on your server to ensure requests come from thinnestAI.
- Use HTTPS for all webhook endpoints.
- Limit data exposure — Only return the CRM fields your agent actually needs.
Send data from your agent to any automation platform. Three tiles cover the common platforms:
| Tool | Use when |
|---|
| Webhook (generic) | Custom endpoint or Zapier. See below. |
| Make.com | Triggering a Make.com scenario. Dedicated tile with Make-specific setup docs. |
| n8n | Triggering an n8n workflow (self-hosted or Cloud). Dedicated tile with n8n-specific setup docs including Header Auth. |
All three share the same runtime behaviour (the agent POSTs a JSON payload with {event, source, data}). The separate tiles exist for discoverability and platform-specific config hints. You can use multiple simultaneously in one agent (e.g. one Make scenario + one n8n workflow).
Setting Up (generic Webhook)
- Go to Tools > Add Tool > Webhook.
- Paste your webhook URL from Zapier or any custom endpoint.
- Optionally name it (e.g., “Zapier CRM”) and add a webhook secret.
| Setting | Description | Example |
|---|
| Webhook URL | Your automation endpoint | https://hooks.zapier.com/hooks/catch/123/abc/ |
| Name | Friendly name (optional) | Zapier Lead Capture |
| Secret | Signature verification (optional) | whsec_... |
What the Agent Can Do
The agent has two functions:
send_webhook(data, event_type) — Send any JSON data with an event type
send_event(event_name, customer_name, email, phone, notes) — Send structured CRM-style events
Example: Zapier Lead Capture
User: "My name is Rahul, email is rahul@company.com, I want a demo"
Agent: [Calls send_event]
event_name: "new_lead"
customer_name: "Rahul"
customer_email: "rahul@company.com"
notes: "Wants a demo of voice agents"
→ Zapier receives JSON, creates HubSpot contact + sends Slack notification
Every webhook receives this JSON structure:
{
"event": "new_lead",
"source": "thinnestai",
"data": {
"customer_name": "Rahul",
"customer_email": "rahul@company.com",
"notes": "Wants a demo of voice agents"
}
}
| Platform | Get Webhook URL | Dedicated tile? |
|---|
| Zapier | Create a Zap → Trigger: “Webhooks by Zapier” → “Catch Hook” → Copy URL | Use the generic Webhook tile. |
| Make.com | Create a Scenario → Add “Webhooks” module → “Custom webhook” → Copy URL | Yes — use the Make.com tile. |
| n8n | Add “Webhook” node → Copy Production URL (not the Test URL) | Yes — use the n8n tile. |
Composio (500+ App Integrations)
Connect your agent to 500+ SaaS apps using Composio — including CRMs (Zoho, Pipedrive, Freshsales), productivity tools, and more. Composio handles authentication and API complexity automatically.
Setting Up
- Sign up at composio.dev and get your API key.
- Go to Tools > Add Tool > Composio.
- Paste your Composio API key.
- The agent can now access any app you’ve connected in your Composio dashboard.
Supported Apps
Composio connects to 500+ apps including:
| Category | Apps |
|---|
| CRM | Zoho CRM, Pipedrive, Freshsales, Salesforce, HubSpot |
| Productivity | Google Workspace, Notion, Slack, Asana, Linear |
| Marketing | Mailchimp, SendGrid, ActiveCampaign |
| Support | Zendesk, Intercom, Freshdesk |
| Finance | Stripe, QuickBooks, Xero |
| Scenario | Use |
|---|
| You need HubSpot/Salesforce with deep customization | Direct tool (built-in) |
| You need Zoho/Pipedrive/Freshsales | Composio (no custom code) |
| You need 5+ different SaaS integrations | Composio (one API key for all) |
| You need simple webhook automation | Webhook tool (Zapier/Make) |
thinnestAI includes direct integrations for popular CRMs. These are deeper integrations with full CRUD operations.
| CRM | Operations | Setup |
|---|
| HubSpot | Contacts, Deals, Companies, Tickets — create, update, search | HubSpot Access Token |
| Salesforce | Leads, Accounts, Contacts, Opportunities — full CRUD + SOQL | Username + Password + Security Token |
| ActiveCampaign | Contacts, Deals, Lists — create, update, manage | API URL + API Key |
| Apollo | People search, Company search, Email finder, Sequences | Apollo API Key |
| LinkedIn | Profile lookup, Company search, Share posts | LinkedIn Access Token |
To add a CRM tool: Tools > Add Tool > select the CRM > enter your API credentials.
Next Steps