Agent API reference
The JavaScript agent client In testing
An AI agent 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, cases, replies with
ifLatest, notes, handoffs, typing, resolution and the event log. - 1.1 includes case frames, evidence, citations, Ask AI drafts and follow-ups. It also includes transaction checks, 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 ishttps://usetempl.com. - Send
Origin: https://usetempl.comandAccept: application/jsonon every request, andContent-Type: application/jsonon every POST. - Never follow a redirect. Treat a 3xx as an error, so the session cookie never leaves Templ.
- Keep only
__Host-templ_nonceand__Host-templ_sessioncookies. RequireSecure,HttpOnlyandPath=/, with noDomain. Each value must hold 64 lowercase hex characters. A session lasts at most 30 days. - Every POST to a workspace route includes
expectedPrincipalId, theprincipalIdof your session. Without it the service answers 401. - Errors are JSON with
errorand sometimesreason. Check the status andreason. Never showerrorto a user. Responses with 429 or some 503 statuses carryRetry-Afterin seconds. - Each account sends at most 1 message or note every 5 seconds. Reads share account limits. Follow
Retry-Afterwhen the service asks you to wait.
Sign in
The agent signs in with its own wallet. An EVM wallet needs no funds.
-
POST /auth/noncewith{ "address": "<0x wallet>", "chainId": <agents.api.signInChainId> }. The answer is{ message, expiresAt }and the nonce cookie. -
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.comVersion: 1Chain 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> -
Sign the message with EIP-191
personal_signover its UTF-8 bytes. -
POST /auth/verifywith{ message, signature }and the nonce cookie. The answer is the session and the session cookie. Check that itsaddressis 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 path | Body or query | Returns |
|---|---|---|
GET /bots | { bots }. Check that your agent is listed. | |
GET /conversations/status | { available, team, canAsk, canStart, limits } | |
GET /conversations | filter (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>/messages | limit 1 to 50, before | { conversation, messages, nextBefore, typing } |
GET /conversations/<tc>/workflow | notesBefore | { 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 /events | after, 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 path | Body or query | Returns |
|---|---|---|
POST /conversations/<tc>/messages | { text, clientId, provenance?, ifLatest?, replyTo? } | 201 { message, conversation } |
GET /conversations/<tc>/cases | before | { cases, nextBefore }, newest first |
GET /conversations/<tc>/cases | seq | { 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-requests | after, 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-ups | case | { followUps, cases } |
POST /conversations/<tc>/follow-ups | { clientId, type, title, ... } | { followUps } |
GET /conversations/<tc>/diagnosis | id | { diagnoses } or { diagnosis } |
GET /conversations/<tc>/review | seq | { cases } with reply labels |
GET /conversations/evals/export | set, format=json, after | { lines, next, withheld } |
POST /conversations/evals/runs | a templ-eval/1 run | the run |
GET /conversations/pulse | counts 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 reason | What to do |
|---|---|
409 stale | Read the case again. |
409 handed-off | A person owns the request. Stop until they reply. |
409 ai-drafts-only, ai-off | A person limited the AI on this request. Write a draft or a note, or nothing. |
403 person-only | Only a person on the team can do this. |
422 secret-refused, secret-request-refused, unsafe-ask | Nothing was stored. Hand the request to a person. |
422 reply-held | Your reply became a draft for the team. |
403 support-paused | The workspace waits for its team to add balance. Stop until it does. |
409 needs-reply-preparing | Wait Retry-After seconds, then list again. |
| 429 | Wait 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
walletin the case details carries wallet sign-in proof. - A widget session grants only the
memberrole. It never grants team access or reaches these routes.
In testing means the feature is built and not yet open to every workspace.