Guides
AI agent
The heysteff AI agent answers your customers on WhatsApp, Instagram, Messenger, SMS and email with your instructions, your context files and your custom tools, and hands the conversation to your team when it should. It comes with all three developer plans: you pay your provider for tokens with your own key, or use the plan’s trial quota.
Leave the agent off and follow Bring your own AI. A developer workspace starts with the agent off, so it never answers on top of your model. Both approaches use the same inbox, the same channels and the same webhooks.
How it works#
- A customer writes on any connected channel.
- If the agent is on for that channel and that conversation, it reads the history, your instructions and your context files. When it needs to, it calls your custom tools.
- It replies on the same channel and notifies you with
agent.replied. When it should not continue, it hands the conversation to your team, pauses the bot and notifies you withagent.handoff. - Your team picks it up from the inbox, on the web or the mobile app, with the full history.
Turning it on and off#
There are four levels. The workspace one is the main switch: when it is off, the agent does not reply on any channel.
| Level | How | Details |
|---|---|---|
| Workspace | PATCH /v1/agent with "enabled": true, or the console → Agent. | A new workspace starts with it off. |
| Channel | "channels": [{ "id": "<channel_connection_id>", "enabled": false }] | true turns it on, false turns it off and null goes back to inheriting. |
| Customer of your SaaS | "customers": [{ "id": "<customer_id>", "enabled": false }] | Applies to every channel that customer connected. A channel with its own value wins over its customer’s. |
| Conversation | POST /v1/conversations/{id}/bot with { "paused": true } or { "paused": false }. | Pauses or resumes the bot in that conversation only. /escalate also pauses it and /resolve turns it back on. |
curl -X PATCH https://app.heysteff.com/api/v1/agent \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"provider": "anthropic",
"model": "claude-haiku-5-5",
"temperature": 0.3,
"channels": [{ "id": "8a1f0c2e-5d3b-4c6a-9e7f-0a1b2c3d4e5f", "enabled": false }],
"customers": [{ "id": "6f1c2a8e-3b4d-4e5f-9a0b-1c2d3e4f5a6b", "enabled": null }]
}'GET /v1/agent returns the full state: on or off, instructions, provider, model, temperature, saved keys (last 4 only), this month’s trial quota, each channel and customer with its agent_enabled, and the suggested models with their price. Every PATCH field is optional, but send at least one.
Instructions and knowledge base#
instructionsis the agent’s prompt, up to 10,000 characters: who it is, its tone, what it may promise and when it should hand the conversation to a person.temperaturegoes from 0 (precise) to 1 (creative).- Context files: what the agent looks up to answer, such as prices, policies, opening hours or FAQs. For now you manage them from the MCP server (
list_context_files,upsert_context_file,patch_context_file,delete_context_file) or by asking the console assistant; they have no endpoint in API v1.
Models and prices#
provider can be anthropic, openai, deepseek or openrouter, and model is the model id at that provider. The list below is a suggestion, not a closed catalog: with OpenRouter you can use any vendor/model. If you pick no provider or model, the agent uses the platform’s default model.
| Provider | Model | Input, per million tokens | Output, per million tokens |
|---|---|---|---|
| Anthropic | claude-haiku-5-5 | USD 0.10 | USD 0.50 |
| Anthropic | claude-sonnet-5-5 | USD 2.00 | USD 10.00 |
| Anthropic | claude-opus-5-5 | USD 4.00 | USD 20.00 |
| OpenAI | gpt-4o-mini | USD 0.15 | USD 0.60 |
| OpenAI | gpt-4o | USD 2.50 | USD 10.00 |
| DeepSeek | deepseek-v4-flash | USD 0.30 | USD 1.20 |
| DeepSeek | deepseek-v4-pro | USD 1.32 | USD 3.96 |
| OpenRouter | anthropic/claude-haiku-5-5 | USD 0.10 | USD 0.50 |
| OpenRouter | openai/gpt-4o-mini | USD 0.15 | USD 0.60 |
These are reference list prices, the same ones GET /v1/agent returns in models. With your key, what counts is your provider’s rate, billed to you directly.
Who pays: your key or the trial quota#
With your key (BYOK). Save your provider key with PUT /v1/agent/keys or in the console → Agent. It is stored encrypted and never comes back through the API: GET /v1/agent shows only the provider and its last 4. There is one key per provider; the agent uses the key for the provider you picked in provider. DELETE /v1/agent/keys/{provider} removes it. Your provider bills you for tokens and heysteff adds nothing on top.
curl -X PUT https://app.heysteff.com/api/v1/agent/keys \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "provider": "anthropic", "api_key": "sk-ant-..." }'Without a key. Each reply comes out of the trial quota for the calendar month (UTC), using the heysteff key. The console assistant uses the same quota. See it in GET /v1/agent → quota (period, used, limit, remaining).
| Plan | Trial replies a month | Custom tools | Insights |
|---|---|---|---|
| Developer Free | 200 | 2 | Not included |
| Developer | 2,000 | 10 | Included |
| Developer Platform | 20,000 | Unlimited | Included |
Without a key and without quota, the agent does not reply. It never switches to another provider on its own. Your webhook gets agent.failed with code: "ai_quota_exhausted" and the customer’s message stays in the inbox for your team. It replies again once you save your key, the next month starts or you upgrade. See Pricing.
Errors#
| Where | Code | What happened |
|---|---|---|
agent.failed webhook | ai_quota_exhausted | No key of your own and no trial quota left this month. Includes quota. |
agent.failed webhook | provider_error | Your provider failed: invalid key, an outage or a limit on its side. Includes provider, model, status_code and credential. |
| Console assistant | 402 ai_quota_exhausted | Same case: no key and no quota. Comes with fix and details. |
/v1/agent | 403 developer_workspace_only | The key belongs to a heysteff workspace that is not a developer one. |
PATCH /v1/agent | 404 channel_not_found, customer_not_found | A channel or customer in the list does not exist in your workspace. |
POST /v1/agent/tools | 402 plan_limit | You reached your plan’s custom tool cap (details.resource = "custom_tools"). |
{
"error": {
"code": "ai_quota_exhausted",
"message": "...",
"fix": "...",
"details": { "period": "2026-11", "used": 200, "limit": 200 }
}
}Custom tools#
A custom tool is an HTTP call to your backend that the agent makes mid-conversation: checking an order, looking up stock or booking a slot. The agent decides when to use it by reading its description, and fills in the arguments you declare in spec.parameters_schema. Every {{args.x}} in url_template, query_template, headers or body_template must be declared there.
curl -X POST https://app.heysteff.com/api/v1/agent/tools \
-H "Authorization: Bearer $HEYSTEFF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "order_status",
"name": "Order status",
"description": "Looks up an order by its number. Use it when the customer asks where their purchase is.",
"spec": {
"method": "GET",
"url_template": "https://api.your-company.com/orders/{{args.number}}",
"parameters_schema": {
"type": "object",
"properties": { "number": { "type": "string", "description": "Order number" } },
"required": ["number"]
},
"timeout_ms": 8000
}
}'GET /v1/agent/toolsandGET /v1/agent/tools/{id}list and show them (without secrets).PATCH /v1/agent/tools/{id}edits one;specis replaced as a whole, so send the full spec with your change."is_active": falseturns it off without deleting it.DELETE /v1/agent/tools/{id}deletes it.- The credential for your backend (a bearer token, for example) is set in the console: no third-party secrets travel through the API.
Agent webhooks#
They only reach developer workspaces, with the same envelope and signature as the rest of the webhooks. Subscribe to them when you create the webhook.
| Event | When | data fields |
|---|---|---|
agent.replied | The agent replied. In copilot mode, mode is suggestion and the reply waits for approval in the inbox. | conversation_id, trigger_message_id, message_ids, mode, text, channel_type, channel_connection_id, provider, model, credential (byok or platform) |
agent.handoff | The agent handed the conversation to a person. | conversation_id, escalation_id, reason, by, bot_paused |
agent.failed | The agent could not reply. | conversation_id, trigger_message_id, code, message, provider, and depending on the case quota or model, status_code and credential |
insight.detected | The weekly analysis of your conversations found something to review. | report_id, funnel_id, week_start, week_end, unresolved, knowledge_gaps, top_gaps |
{
"event": "agent.replied",
"data": {
"conversation_id": "c0a8012e-...",
"trigger_message_id": "5b7e...",
"message_ids": ["9f2c..."],
"mode": "sent",
"text": "...",
"channel_type": "whatsapp_cloud",
"channel_connection_id": "8a1f0c2e-...",
"provider": "anthropic",
"model": "claude-haiku-5-5",
"credential": "byok"
}
}{
"event": "agent.failed",
"data": {
"conversation_id": "c0a8012e-...",
"trigger_message_id": "5b7e...",
"code": "ai_quota_exhausted",
"message": "...",
"provider": "anthropic",
"quota": { "period": "2026-11", "used": 200, "limit": 200, "remaining": 0 }
}
}Insights#
On Developer and Developer Platform, heysteff analyzes your conversations and shows you three lists: frustrated customers (frustrated), what the agent could not answer or answered wrong (agent_gaps) and unresolved conversations (unresolved). Each list has total and items.
- In the console, under Insights.
- With
GET /v1/insights?days=30&limit=10(scopeinsights:read):daysgoes from 1 to 90 andlimitfrom 1 to 50. On Developer Free it returnsincluded: false. - With the
insight.detectedwebhook, once a week when the analysis finds something.
Use insights to improve the instructions and context files: what shows up in agent_gaps is usually an answer missing from the knowledge base.
Console assistant#
The console has a side assistant you can ask about your workspace in plain language: why a webhook failed, what happened in a conversation, how much quota you have left. It also takes actions with the same tools as the MCP server, and before any change or message to a person it shows you what it is about to do and waits for your confirmation.
It uses the agent’s provider key if you saved one; otherwise, the same trial quota. Without either it returns 402 ai_quota_exhausted.
From your AI (MCP) and the SDK#
get_developer_agentandupdate_developer_agent: see and configure the agent, with the same fields asPATCH /v1/agent.delete_developer_ai_key: removes a provider key. The key is not saved through MCP because it is a secret: save it in the console or withPUT /v1/agent/keys.get_developer_insights: the insights.list_custom_tools,create_custom_tool,update_custom_toolanddelete_custom_tool: custom tools.set_conversation_bot: pauses or resumes the bot in a conversation.
With the TypeScript SDK:
import { Heysteff } from '@heysteff/sdk';
const heysteff = new Heysteff(); // reads HEYSTEFF_API_KEY
await heysteff.agent.setKey({ provider: 'anthropic', api_key: process.env.ANTHROPIC_API_KEY! });
const agent = await heysteff.agent.update({ enabled: true, model: 'claude-haiku-5-5' });
console.log(agent.quota, agent.keys); // keys: provider and last 4 only
const tools = await heysteff.agent.tools.list();
const insights = await heysteff.insights.get({ days: 7 });
console.log(insights.agent_gaps.total);Every endpoint with its fields is in the API reference, under AI agent.