Skip to main content

Agent API reference

The JavaScript agent client In testing

An answers users through the team inbox routes. It uses the permissions your manager grants its account. For a Node example, start with Connect a support agent.

Check agents.api.version in app status before starting an agent. A null value means the API checks are pending. A verified API reports 1.0 or 1.1. Read agents.api.features to see which features it serves.

Versions​

  • 1.0 covers sign-in, , replies with ifLatest, notes, , typing, resolution and the event log.
  • 1.1 includes case frames, evidence, citations, Ask and follow-ups. It also includes , reply labels, eval runs and the pulse.

Read only fields your client knows. Treat an unknown reason as an unknown error.

Transport​

  • The API origin is https://community.usetempl.com. The app origin is https://usetempl.com.
  • Send Origin: https://usetempl.com and Accept: application/json on every request, and Content-Type: application/json on every POST.
  • Never follow a redirect. Treat a 3xx as an error, so the session cookie never leaves Templ.
  • Keep only __Host-templ_nonce and __Host-templ_session cookies. Require Secure, HttpOnly and Path=/, with no Domain. Each value must hold 64 lowercase hex characters. A session lasts at most 30 days.
  • Every POST to a route includes expectedPrincipalId, the principalId of your session. Without it the service answers 401.
  • Errors are JSON with error and sometimes reason. Check the status and reason. Never show error to a user. Responses with 429 or some 503 statuses carry Retry-After in seconds.
  • Each account sends at most 1 message or note every 5 seconds. Reads share account limits. Follow Retry-After when the service asks you to wait.

Sign in​

The agent signs in with its own wallet. An EVM wallet needs no funds.

  1. POST /auth/nonce with { "address": "<0x wallet>", "chainId": <agents.api.signInChainId> }. The answer is { message, expiresAt } and the nonce cookie.

  2. Check the message before you sign it. It is exactly these 11 lines, with nothing before or after:

    https://usetempl.com wants you to sign in with your Ethereum account:
    <your address>

    Sign in to your Templ chat.

    URI: https://usetempl.com
    Version: 1
    Chain ID: <agents.api.signInChainId>
    Nonce: <the nonce cookie's value>
    Issued At: <within the last 5 minutes>
    Expiration Time: <expiresAt, at most 6 minutes ahead>
  3. Sign the message with EIP-191 personal_sign over its UTF-8 bytes.

  4. POST /auth/verify with { message, signature } and the nonce cookie. The answer is the session and the session cookie. Check that its address is your wallet.

Solana account sign-in In testing

GET /auth/session returns the current session or 401. POST /auth/logout with {} ends the session everywhere.

Routes in 1.0​

Workspace routes start with /rooms/<workspace UUID>. The manager and accounts with handleConversations have team access. A role grants that permission.

Method and pathBody or queryReturns
GET /bots{ bots }. Check that your agent is listed.
GET /conversations/status{ available, team, canAsk, canStart, limits }
GET /conversationsfilter (needs-reply, needs-you, ai-handling, open, waiting, waiting-on-member, unassigned, mine, needs-person, resolved, all), cursor{ filter, conversations, nextCursor }
GET /conversations/<tc>{ conversation }
GET /conversations/<tc>/messageslimit 1 to 50, before{ conversation, messages, nextBefore, typing }
GET /conversations/<tc>/workflownotesBefore{ workflow } with context, the proven wallet, access.claimedWallet, notes and activity
POST /conversations/<tc>/messages{ text, clientId, ifLatest?, replyTo? }201 { message, conversation }
POST /conversations/<tc>/workflow{ action: "note", text, clientId, mentions? }{ workflow }
POST /conversations/<tc>/workflow{ action: "handoff", reason, ifLatest? }{ workflow }
POST /conversations/<tc>/typing{}204
POST /conversations/<tc>/resolve{ resolved, timeSpent? }{ conversation }
GET /eventsafter, limit 1 to 200{ events, nextAfter, oldestRetained, resync }

Save the text and its UUIDv7 clientId before sending. A retry with the same clientId returns the saved message. Set ifLatest to { id, revision } from the latest message you read.

Handoff reasons: quote, privacy, account, billing, legal, safety, transaction, bug, person, unsure, other, strategy, access, recovery, decision, internal.

Routes in 1.1​

Method and pathBody or queryReturns
POST /conversations/<tc>/messages{ text, clientId, provenance?, ifLatest?, replyTo? }201 { message, conversation }
GET /conversations/<tc>/casesbefore{ cases, nextBefore }, newest first
GET /conversations/<tc>/casesseq{ case, frame, evidence, drafts, requests, unreadable }
POST /conversations/<tc>/workflow{ action: "frame", case, set?, clear?, ifRevision? }{ frame }
POST /conversations/<tc>/workflow{ action: "evidence", case, rows, clientId }201 { evidence }
POST /conversations/<tc>/workflow{ action: "withdraw-evidence", case, n }{ evidence }
GET /conversations/draft-requestsafter, limit 1 to 50{ requests, nextCursor }, oldest first
POST /conversations/<tc>/workflow{ action: "draft", text, clientId, request? or forMessage?, provenance? }201 { draft }
POST /conversations/<tc>/workflow{ action: "draft-decline", request, reason }{ request }
GET /conversations/<tc>/follow-upscase{ followUps, cases }
POST /conversations/<tc>/follow-ups{ clientId, type, title, ... }{ followUps }
GET /conversations/<tc>/diagnosisid{ diagnoses } or { diagnosis }
GET /conversations/<tc>/reviewseq{ cases } with reply labels
GET /conversations/evals/exportset, format=json, after{ lines, next, withheld }
POST /conversations/evals/runsa templ-eval/1 runthe run
GET /conversations/pulsecounts and times, schema 1 or 2

A frame sets product, network, metric, window, reported and question (text). An evidence row records a value and its source. Its fields are source, sourceLabel, product, metric, window, observed and value. observed holds a block, a time or both. Stored rows cannot be edited. Another row can replace one.

Put an evidence marker such as [ev:3] after each figure it supports. Cite a transaction check with [dx:<check id>]. The service removes markers before storing the reply.

Your workspace can hold AI replies that fail citation checks. A held reply becomes a team draft. The API returns 422 with reason reply-held and held: { draft, case, problems }. Fix the citations or hand off.

provenance tells your team why the agent wrote a reply or draft. Its reason is answer, clarifying-question, quote, safety-warning or draft. It includes sources and can include summary, confidence, model and tools. Each source has id, kind, title and an optional HTTPS url. Only your team sees these details.

Use reason: "answer" only for a real answer the user receives. Templ records it only after accepting and storing the registered agent's reply. The answer must arrive before the case is marked solved. Replies without this reason do not count as an AI answer for billing.

Questions, quote summaries, safety notices, drafts and handoffs do not count as AI answers. A handoff action sends no reply. A draft decline with reason handoff sends no reply either. A later handoff keeps an earlier real answer's recorded contribution. Read the billing rule for the other required checks.

The agent sees what happened, findings, a user draft and claims from each transaction check. claims lists numbers, tokens, networks and short references a reply may use. Use only values in claims. Hand off with reason transaction when the check needs a person. Keep team-only next steps out of user replies.

The reference runner waits up to 20 seconds for a queued or running check. It cites the check in its answer or draft with [dx:<check id>]. With checks off, it answers without check facts.

Solana transaction checks In testing

People on your team send Ask AI requests. Answer with a draft linked through request. To decline, use handoff, unsure, no-access, unavailable, refused or other. A draft never reaches the user.

Reasons to handle​

Status and reasonWhat to do
409 staleRead the case again.
409 handed-offA person owns the request. Stop until they reply.
409 ai-drafts-only, ai-offA person limited the AI on this request. Write a draft or a note, or nothing.
403 person-onlyOnly a person on the team can do this.
422 secret-refused, secret-request-refused, unsafe-askNothing was stored. Hand the request to a person.
422 reply-heldYour reply became a draft for the team.
403 support-pausedThe workspace waits for its team to add . Stop until it does.
409 needs-reply-preparingWait Retry-After seconds, then list again.
429Wait Retry-After seconds.

Use it from Python​

The Python client In testing

templ_agent.py uses Python 3.10 or newer and only its standard library. Download it from https://usetempl.com/templ_agent.py. Check its SHA-256 against agents.python.sha256 in app status before importing it. Require agents.python.verified to be true.

The client never holds your key. You pass a function that signs one message. The client checks the message before calling that function. Keep the key in a secret manager or a mode-600 file. Never put it in an argument, prompt or log. This example uses eth-account:

import json
import os
import time
import urllib.request

from eth_account import Account
from eth_account.messages import encode_defunct

from templ_agent import TemplAgent, TemplError, new_message_id

with urllib.request.urlopen("https://usetempl.com/api/status") as response:
status = json.load(response)
account = Account.from_key(open(os.environ["TEMPL_AGENT_KEY_FILE"]).read().strip())
agent = TemplAgent(
address=account.address,
sign=lambda message: account.sign_message(encode_defunct(text=message)).signature.hex(),
room=os.environ["TEMPL_WORKSPACE"],
app_origin="https://usetempl.com",
api_origin="https://community.usetempl.com",
sign_in_chain_id=status["agents"]["api"]["signInChainId"],
)
agent.sign_in()
agent.status()

A loop that answers the needs-reply queue:

for caseItem in agent.iter_conversations("needs-reply"):
if caseItem.get("handoff") or caseItem.get("replyBlocked") or not caseItem.get("canPost"):
continue
page = agent.messages(caseItem["id"])
latest = TemplAgent.latest(page)
details = agent.workflow(caseItem["id"])
text = your_model(page["messages"], details.get("context"), details.get("wallet"))
try:
agent.reply(
caseItem["id"],
text,
client_id=new_message_id(),
if_latest=latest,
provenance={"reason": "answer", "sources": []},
)
except TemplError as error:
if error.reason == "stale":
continue
if error.status == 429:
time.sleep(error.retry_after or 5)
elif error.reason in ("secret-refused", "secret-request-refused", "unsafe-ask"):
agent.hand_off(caseItem["id"], "safety")
else:
raise

Save each reply's text and client_id before sending. Retry an uncertain send with the same client_id. Label details["access"]["claimedWallet"] as unverified when passing it to your model. Run check_citations(text, rows) before sending a cited reply.

What the API never does​

  • It never signs or sends a transaction, and chat text can't make it.
  • It never returns a key or asks for one.
  • A wallet the app reports stays unverified. Only wallet in the case details carries wallet sign-in proof.
  • A widget session grants only the member role. It never grants team access or reaches these routes.

In testing means the feature is built and not yet open to every workspace.