✅ Meta App Review approved — GoSendAPI Cloud is live. Sign up free →
GuidesAI Agents — API overview

AI Agents — API overview

Everything you can do from the portal’s AI Agents tab is also exposed as a public REST API under /v1. Use it to provision agents from your own dashboards, run migrations, wire CI, or build multi-tenant products where each of your customers gets their own agent without a human touching the portal.

This guide is the mental model. For the exhaustive request/response schemas of every endpoint, see the interactive API Reference — it is auto-generated from the production NestJS source and always up to date.

The five resources

ResourceWhat it isImmutable?
AgentThe stable definition: name + LLM provider + model + description. One per “product” you build (e.g. booking assistant, support triage).No
AgentToolAn HTTP contract to one of your endpoints — the primitive the agent uses to talk to your API. Scoped per tenant, referenced by slug. See tools declaration.Yes (edit = new record)
AgentVersionA snapshot: system prompt + selected tools + guardrails config. Immutable — every change creates a new version. Lets you roll v4 to 5% of pilots while the rest of production stays on v3, no infra needed.✅ Yes
AgentBindingThe deploy: assigns an AgentVersion to a scope (tenant / customer / phone_number) with per-client params, rules and encrypted secrets. Precedence when multiple match: phone_number > customer > tenant.No
AgentThread1:1 with Conversation. Holds the message history plus explicit muting state (muted_at + muted_reason) that the guardrails floor writes to. See thread reactivation.No

Lifecycle

┌──────────┐   ┌───────────┐   ┌───────────────┐   ┌───────────────┐
│  Agent   │──▶│ AgentTool │──▶│ AgentVersion  │──▶│ AgentBinding  │
│  (name,  │   │  (HTTP    │   │ (prompt +     │   │ (scope +      │
│  model)  │   │  contract)│   │  tools + gr.) │   │  secrets)     │
└──────────┘   └───────────┘   └───────────────┘   └───────────────┘
                                        │                  │
                                        │                  ▼
                                        │           ┌──────────────┐
                                        │           │ AgentThread  │
                                        │           │ (per conv,   │
                                        │           │  auto-created)│
                                        │           └──────────────┘

                                        └─ Roll a new version = new record,
                                           existing bindings unchanged.

Runtime is transparent: when a message hits a phone_number that has a matching active AgentBinding, the runtime resolves the version, hydrates tools + secrets, runs the LLM loop, calls your endpoints when the model asks for them, and replies as source=AUTO_AGENT_PLATFORM. You never write orchestration code.

Quickstart — provision an agent end-to-end

⚠️

Every request needs X-API-Key: <your key> and (for admin-only endpoints) X-Admin-Key. Never commit either. See rate limits and error handling.

Create an Agent

curl -X POST https://cloud.gosendapi.com/v1/agents \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Booking assistant",
    "provider": "openai",
    "model": "gpt-4o-mini",
    "description": "Handles appointment booking over WhatsApp."
  }'
# → { "id": "agt_...", "name": "Booking assistant", ... }

Declare the tools it needs

One tool per endpoint the agent will call. See the tools declaration guide for the five blocks (description, input_schema, request, response, errors) and the three interpolation namespaces ({{input.*}}, {{binding.*}}, {{context.*}}).

curl -X POST https://cloud.gosendapi.com/v1/agent-tools \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "booking.search",
    "description": "Search available slots.",
    "declaration": { /* five blocks — see agents-tools guide */ }
  }'
# → { "id": "tool_...", "slug": "booking.search", ... }

Create the first AgentVersion

The version snapshots the system prompt + tool slugs + guardrails. Editing later = creating v2.

curl -X POST https://cloud.gosendapi.com/v1/agents/$AGENT_ID/versions \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "system_prompt": "You are the booking assistant of ...",
    "tools": ["booking.search", "booking.create"],
    "guardrails_config": { "capExecucion": 10, "capConversacion": 20 }
  }'
# → { "id": "ver_...", "version": 1, ... }

Bind the version to a phone/customer/tenant

The binding is what actually turns the agent on for a given scope. Same version can be reused across many bindings (multi-tenant SaaS pattern).

curl -X POST https://cloud.gosendapi.com/v1/agent-bindings \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Fitz Roy Clinic — pilot",
    "agent_version_id": "ver_...",
    "scope": "phone_number",
    "phone_number_id": "phn_...",
    "params": { "wizardEnabled": false, "catchAllAction": "escalate" },
    "reglas": { "tono": "Cordial, use vos.", "identidadIA": "Say you are the virtual assistant if asked." }
  }'
# → { "id": "bnd_...", "has_secrets": false, ... }

reglas is free-form JSON rendered into the system prompt at runtime — write instructions, not keywords. params is runtime config the agent engine reads (kill switches, wizard flags). See rules vs params below.

Attach encrypted secrets (admin-only)

Anything the tool needs to authenticate against your API — API keys, tenant-scoped tokens, JWT credentials. Never travel in cleartext except during the initial POST.

curl -X POST https://cloud.gosendapi.com/v1/admin/agent-bindings/$BINDING_ID/secrets \
  -H "X-Admin-Key: $GOSENDAPI_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "secrets": {
      "erp_api_key": "sk_live_...",
      "erp_tenant_slug": "fitzroy"
    }
  }'

Now every {{binding.secret.erp_api_key}} in your tool declarations resolves at runtime. Secrets are AES-256-GCM encrypted at rest and never returned by any GET endpoint.

That’s it — the next inbound message on that phone number goes through the agent loop.

Rules vs Params vs Secrets

Three separate JSON objects on AgentBinding, each with a different consumer:

FieldRead byFormatExample
reglasThe LLM (rendered into system prompt)Free-form JSON, keys are titles for prompt sections{"tono": "Formal, use usted, no emojis."}
paramsThe agent engine (runtime code)Documented keys only{"wizardEnabled": true, "catchAllAction": "escalate", "capConversacion": 30}
secrets_encryptedThe tool executor (via {{binding.secret.*}})Key/value strings, admin-only{"api_key": "sk_...", "webhook_secret": "..."}

The most common mistake: putting catchAllAction in reglas and expecting the runtime to change behavior — it won’t. reglas only affects what the model says.

Documented params keys

Only these keys have runtime effect (anything else is silently ignored):

KeyTypeDefaultWhat it does
wizardEnabledbooleanfalseEnables the deterministic booking wizard for the vertical when the router classifies the message as pedir_turno.
wizardReserveToolSlugstringnoneSlug of the reservation tool the wizard should call (e.g. gs.crear_turno). Required if wizardEnabled: true.
catchAllAction"escalate" | "respond""escalate"What the runtime does when the router lands on catch_all — escalate to a human or let the model respond freely.
capConversacioninteger20 (platform-managed: 30)Hard cap on the number of message pairs kept in the loop context before escalating. Prevents runaway threads.
inactiveResetDaysintegerenv AGENT_INACTIVE_RESET_DAYS (default 3)Days of inactivity (since AgentThread.updated_at) after which the runtime clears messages=[] automatically. 0 disables the reset for this binding — the history grows forever. See Housekeeping — inactive thread reset.

Versioning strategy

AgentVersion is immutable so you get safe rollouts without infrastructure:

  • Pilot a new prompt — create v2, point one binding at it. If the pilot succeeds, PATCH the other bindings’ agent_version_id.
  • A/B testing — same version across bindings; different reglas per binding for tone/copy variants.
  • Emergency rollback — one PATCH per binding back to the previous version. No code deploy.
  • Clone & edit — duplicate a version (portal has a shortcut; via API, GET the version, POST a new one with tweaks).

You cannot mutate a version’s system_prompt, tools, guardrails_config, or routing. Attempting a PATCH will fail with 405. Delete only works if no bindings still reference it.

Guardrails floor

Every run passes through non-negotiable checks before, during and after the LLM loop:

  1. Urgency detector — Spanish medical patterns (respiratory, cardiac, neuro, bleeding, suicide, death-imminent, child+symptom, explicit urgente/emergencia/SOS). Match → thread muted as handoff.
  2. Human intervention scan — if a human replied in the last 24h via HUMAN_COEXISTENCE (mobile) or HUMAN_PORTAL → thread muted as human_detected.
  3. Conversation cap — default 20 messages (30 for platform-managed). Prevents runaway loops.
  4. Cost circuit breaker — per-thread ($1 default), per-tenant per-day ($50 default), per-tenant per-month ($500 default). Overridable per tenant.
  5. Output safety — if the LLM claims a factual action (“your appointment is booked”) without a successful matching tool call, the reply is blocked before send.

Every muting event is durable — see the next section for reactivation.

Reactivating a muted thread

Every mute has an explicit muted_reason so the runtime knows what kind of unmute (if any) is safe. These are the exact string values you get back in the muted_reason field of GET /v1/agent-threads:

muted_reason valueAuto-unmuteRationale
handoffNo — human onlyRouter escalated for judgement (clinical urgency, catch-all policy). Only a human decides when it’s safe
human_detectedYes — 24h since muted_atHuman took over the conversation; after silence, safe to hand back
cost_cap_threadNo — human onlyA single thread burned $1+ → signal of a bug in tools or a loop. Auto-unmute would burn the same $1 again
cost_cap_tenant_dayYes — 24h since muted_atRolling window matches the daily cap
cost_cap_tenant_monthYes — on the 1st of the next calendar monthCalendar window matches the monthly cap

Manual reactivation via API works for any reason (overrides the “no auto-unmute” categories):

Reactivation clears the muted state. It does not clear AgentThread.messages. If you’re iterating on the system prompt and the accumulated history is making the model replay old patterns (see Milestone tools & auto-reset), reset the history separately:

curl -X POST https://cloud.gosendapi.com/v1/agent-threads/$THREAD_ID/reset-messages \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "iterating booking flow prompt to v23"}'

This clears messages to [] and emits AgentEvent thread.messages_reset for audit. It does NOT unmute — use /reactivate for that.

# List every muted thread for your tenant
curl https://cloud.gosendapi.com/v1/agent-threads?muted=true \
  -H "X-API-Key: $GOSENDAPI_KEY"
 
# Reactivate one
curl -X POST https://cloud.gosendapi.com/v1/agent-threads/$THREAD_ID/reactivate \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason": "False positive from urgency detector — patient just asked if it was a bot."}'

The reactivation is atomic (mutedAt=NULL, mutedReason=NULL) and emits an AgentEvent thread.reactivated for audit — previous_muted_reason, reactivated_by_user_id, reason. Idempotent: reactivating an already-active thread is a no-op, no event emitted.

Detecting silent human intervention

Beyond the explicit handoff mechanism, the runtime scans the last 24h of each conversation for any message that came from a human channel and mutes the thread with human_detected if it finds one. The three channels the scan recognises:

Message.sourceMeaning
HUMAN_COEXISTENCEHuman replying from the WhatsApp Business App on the mobile phone.
HUMAN_PORTALHuman with a gsacloud account replying from the portal or from the inbox iframe with requireAgentLogin=true.
HUMAN_EMBED_ANONYMOUSHuman replying from the inbox iframe without login (embed with requireAgentLogin=false or scope tenant/phone_number). There’s no sentByUserId because the operator has no gsacloud account, but the runtime knows it’s human because the message flowed through POST /v1/inbox-embed/conversations/:id/messages.

If any of the three appear within the window, the agent stays silent. The reason is auto-reset later (see the inactivity reset matrix).

Housekeeping — inactive thread reset

AgentThread.messages grows every turn — the assistant reply plus each tool result gets appended. Without housekeeping the array grows forever, and every subsequent run pays the token cost of the whole history plus risks pattern replay from stale content (see milestone tools & auto-reset).

Three mechanisms clear it:

MechanismTriggerConfigured by
Post-milestone auto-resetA tool with terminates_thread: true returns ok=trueThe tool declaration
Inactivity auto-resetThread’s updated_at is older than N daysbinding.params.inactiveResetDays (falls back to env AGENT_INACTIVE_RESET_DAYS, default 3)
Manual resetPOST /v1/agent-threads/:id/reset-messages or the portal buttonAd-hoc — useful while iterating on the system prompt

Tuning inactiveResetDays per binding

Different bindings on the same tenant can pick different windows depending on the vertical:

  • Booking / appointments — 2 to 3 days. A patient that comes back a week later is almost always starting a new topic.
  • Support / troubleshooting — 5 to 7 days. Longer troubleshooting arcs where multi-day context is useful.
  • Notifications-only — set 0 to disable (rare — you probably don’t want a growing history at all, in which case make sure your tools have terminates_thread: true).

Set it in the binding’s params:

curl -X PATCH https://cloud.gosendapi.com/v1/agent-bindings/$BINDING_ID \
  -H "X-API-Key: $GOSENDAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "params": {
      "wizardEnabled": false,
      "catchAllAction": "escalate",
      "capConversacion": 20,
      "inactiveResetDays": 2
    }
  }'

Or fill it in the portal’s Params textarea when creating/editing the binding.

Every automatic reset emits an AgentEvent:

  • thread.messages_auto_reset — post-milestone (properties: milestone_tool, previous_message_count).
  • thread.messages_inactivity_reset — inactivity (properties: previous_message_count, inactive_days, threshold_days, threshold_source).
  • thread.messages_reset — manual (properties: previous_message_count, reset_by_user_id, reason).

Use these events for audit and to reason about token consumption on a tenant.

Ownership and errors

Every resource is scoped to the tenant behind the X-API-Key. Cross-tenant reads return 404 (not 403 — we don’t leak existence). Common conflicts:

SituationStatusFix
Delete an Agent with active bindings409Delete or reassign the bindings first
Delete a Tool referenced by any AgentVersion409Roll a new version without that tool, then delete
PATCH an AgentVersion (any field)405Versions are immutable — create a new one
Binding with scope=phone_number but the phone belongs to another customer409Fix the scope or the phone ownership

Full error handling patterns: error handling guide.