developers
ES Sign up

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.

Already have your own model?

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#

  1. A customer writes on any connected channel.
  2. 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.
  3. 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 with agent.handoff.
  4. 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.

LevelHowDetails
WorkspacePATCH /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.
ConversationPOST /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
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#

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.

ProviderModelInput, per million tokensOutput, per million tokens
Anthropicclaude-haiku-5-5USD 0.10USD 0.50
Anthropicclaude-sonnet-5-5USD 2.00USD 10.00
Anthropicclaude-opus-5-5USD 4.00USD 20.00
OpenAIgpt-4o-miniUSD 0.15USD 0.60
OpenAIgpt-4oUSD 2.50USD 10.00
DeepSeekdeepseek-v4-flashUSD 0.30USD 1.20
DeepSeekdeepseek-v4-proUSD 1.32USD 3.96
OpenRouteranthropic/claude-haiku-5-5USD 0.10USD 0.50
OpenRouteropenai/gpt-4o-miniUSD 0.15USD 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
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).

PlanTrial replies a monthCustom toolsInsights
Developer Free2002Not included
Developer2,00010Included
Developer Platform20,000UnlimitedIncluded

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#

WhereCodeWhat happened
agent.failed webhookai_quota_exhaustedNo key of your own and no trial quota left this month. Includes quota.
agent.failed webhookprovider_errorYour provider failed: invalid key, an outage or a limit on its side. Includes provider, model, status_code and credential.
Console assistant402 ai_quota_exhaustedSame case: no key and no quota. Comes with fix and details.
/v1/agent403 developer_workspace_onlyThe key belongs to a heysteff workspace that is not a developer one.
PATCH /v1/agent404 channel_not_found, customer_not_foundA channel or customer in the list does not exist in your workspace.
POST /v1/agent/tools402 plan_limitYou reached your plan’s custom tool cap (details.resource = "custom_tools").
402 Payment Required
{
  "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
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
    }
  }'

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.

EventWhendata fields
agent.repliedThe 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.handoffThe agent handed the conversation to a person.conversation_id, escalation_id, reason, by, bot_paused
agent.failedThe 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.detectedThe weekly analysis of your conversations found something to review.report_id, funnel_id, week_start, week_end, unresolved, knowledge_gaps, top_gaps
agent.replied
{
  "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"
  }
}
agent.failed
{
  "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.

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#

With the TypeScript SDK:

TypeScript
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.