Blog
The Agent API: give your agents a place to work
Scoped API keys, a small REST API, an MCP server and signed webhooks. How agents create, read, move and comment on quests in Tengu.
10 min read
- agents
- api
- mcp
- webhooks
- guides
Your team may already have a few agents doing real work: a support bot that files bugs, a coding agent that reports progress, a script that opens tickets from monitoring alerts. Each one needs somewhere to put that work where people will see it. Often that means an API key for the project tool, a different shape for every tool, and comments signed by whoever owns the key.
Tengu's Agent API gives agents a place on the board. They create quests, read them, move them, assign them and comment on them, and they hear what happens through signed webhooks. Claude Code and other MCP clients get the same through Tengu's MCP server. Their comments carry the agent's own name. They never take a seat, and they never earn XP.
The Agent API and the MCP server are on every plan, Free included.
API keys
An admin makes a key in Admin > Settings > Agents & webhooks. Give it the agent's name, for example "Support bot". That name is what people see on the agent's comments. Then choose how far it reaches:
- What it may do. Reads only, or reads and writes. A read-only key can list, find and read tasks and comments. Anything that creates, changes or comments gets
403. - Which lists. Every list you see, or only the lists you pick. A key kept to some lists sees nothing else: other lists, and tasks on them, answer
404as if they weren't there. - When it expires. Never, or in 7, 30 or 90 days, or a year. An expired key gets
401.
Settings shows each key's reach, when it expires and when it was last used.
- A key looks like
koba_…(from the product's earlier name). It is shown once, when it is made. Tengu stores only a hash of it. - A key belongs to one organization. Every call is scoped to that organization's data and never sees another's.
- A key acts as the person who made it: their list access and their permissions, never their XP. If they cannot see a list, neither can the agent, whatever the key says.
- Revoking a key is immediate: anything still using it gets
401from then on. A key also stops when the person who made it leaves the organization. - Each key makes up to 120 calls a minute, the REST API and the MCP server together. Past that it gets
429with aRetry-Afterheader.
Keys made before scopes existed keep reading and writing every list their maker sees, with no end.
Send the key on every call:
Authorization: Bearer koba_YOUR_KEY
Endpoints
All endpoints live under https://tengu.work/api/v1 and speak JSON.
| Method | Path | What it does |
|---|---|---|
GET | /lists | Lists the key can see, with their statuses in order and what each one means |
GET | /tasks | Top-level tasks, newest change first. Filter with list_id, status, assignee, text and more |
POST | /tasks | Creates a task in a list |
GET | /tasks/:id | One task, with its description, status role and people |
PATCH | /tasks/:id | Changes title, status, priority, due date, description or assignee |
GET | /tasks/:id/comments | Comments Tengu holds on a task, oldest first |
POST | /tasks/:id/comments | Comments as the agent |
POST | /tasks/:id/images | Starts an image upload for a comment, such as a screenshot |
POST | /tasks/:id/images/complete | Finishes it and returns a URL to put in a comment |
Statuses mean something
GET /lists returns each status with its role: available, started, review, sentback, done, shipping, live, closed and a few more. Your agent can look for "the review status" without knowing that one list calls it "QA Ready" and another "In Review".
Find tasks
GET /tasks takes these query parameters, all optional:
list_id: one list.status: a status name in any case, or a role such asreviewto match that status in every list.assignee: a member's ID or sign-in email.text: words to look for in the title, key and description. Results come best match first.updated_since: an ISO date-time.limit: up to 500, 100 by default.
Create a task
curl -X POST https://tengu.work/api/v1/tasks \
-H "Authorization: Bearer koba_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"list_id": "YOUR_LIST_ID",
"name": "Checkout fails for saved cards",
"description": "Reported in support conversation 4821. Steps to reproduce: ...",
"assignee": "dev@youragency.com",
"priority": 2,
"due_date": "2026-10-09"
}'
assignee is a member's ID or sign-in email. Leave it out and the quest waits in the Available pool. priority runs from 1 (urgent) to 4. status may name one of the list's unfinished statuses to start in; any other gets 422. The response is the new task, in the same shape GET /tasks/:id returns.
Move it and comment
curl -X PATCH https://tengu.work/api/v1/tasks/TASK_ID \
-H "Authorization: Bearer koba_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "in review" }'
curl -X POST https://tengu.work/api/v1/tasks/TASK_ID/comments \
-H "Authorization: Bearer koba_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "body": "PR #214 is up and checks pass." }'
A PATCH applies its changes in order: name (the title, up to 200 characters, on lists that live in Tengu), then status, then priority and due date, then description, then assignee. The first refusal stops the rest and says why. Set assignee to null to put a quest back in the pool.
Screenshots in comments
To attach an image, POST /tasks/:id/images with { "name": "checkout.png", "size": 48213 }. Tengu returns an uploadUrl and a path. PUT the file to uploadUrl with its type as Content-Type (image/png, say), then POST /tasks/:id/images/complete with the path. Storage refuses a file sent as a page, a script, XML or an SVG, and the complete call then says the image isn't in storage; one sent with no type, or as curl's default, is kept as plain bytes. You get back a url. Put it in the comment as Markdown, . PNG, JPEG, GIF and WebP, up to 200 MB each; the size is the file's exact size in bytes, since Tengu keeps it at the size it lands as and refuses one bigger than it said.
Where tasks live matters
Imported quests keep their ClickUp IDs, so an ID your agent already has keeps working. On lists that live in Tengu, agents can rename quests, rewrite descriptions and read the full comment thread.
The MCP server
Claude, ChatGPT, Claude Code and any other client that speaks the Model Context Protocol can work in Tengu without any glue code. The server lives at https://tengu.work/api/mcp.
Connect Claude or ChatGPT
Anyone on the team, not only admins, can connect their own assistant: Settings > MCP has the address and the steps for each assistant. Paste https://tengu.work/api/mcp into the assistant as a custom connector (in Claude, Settings > Connectors > Add custom connector; in ChatGPT, a connector made in developer mode with OAuth). The assistant sends you to Tengu to sign in, and you choose:
- The workspace, if you're in more than one.
- What it may do. Read only (the default), or read and write.
- Which lists. Every list you see, or only the ones you pick.
The assistant then works as you. It sees only the tickets on lists you're on, with your permissions: never another client's list or another organization's, even a ticket there you review or were mentioned on, and never time, pay, rates or billing. What it writes is signed "Dana via Claude". Its access token lasts an hour and refreshes on its own; the connection ends when you disconnect it in Settings > MCP, when an admin disconnects it in Admin > Settings, or when you leave the organization. Admins see every connection on the team.
Then ask it things like "What needs my attention today?", "Which tickets are in QA or blocked across my projects?" or "What has been stuck for more than three days? Draft a follow-up for each."
Under the hood this is standard OAuth 2.1 for MCP: protected resource metadata, dynamic client registration, PKCE with S256 and rotating refresh tokens. Any MCP client that supports OAuth connects the same way. In Claude Code, run claude mcp add --transport http tengu https://tengu.work/api/mcp, then /mcp to sign in.
With an API key
An agent that can't sign in (a shared cloud environment, CI) uses an API key instead. Add the server to a repo's .mcp.json:
{
"mcpServers": {
"tengu": {
"type": "http",
"url": "https://tengu.work/api/mcp",
"headers": { "Authorization": "Bearer ${TENGU_API_KEY}" }
}
}
}
Put the key in TENGU_API_KEY where the agent runs: your shell, or the cloud environment's settings. Never commit it.
The server gives the agent ten tools:
| Tool | What it does |
|---|---|
list_lists | The lists the key can see, with their statuses and what each one means |
list_tasks | Finds tasks by list, status or role, assignee or text, newest change first |
get_task | One task with its comments |
needs_attention | What waits on you: your QA queue, blocked, sent back, overdue, due soon, stuck, unassigned and ready to ship |
project_status | Each project's lists: open work by status and stage, overdue, blocked, in review and who holds what |
search | Finds tasks by words, with a link to each |
fetch | One task as a document: its facts, description and comments |
create_task | Creates a task, signed with the key's name |
update_task | Changes title, status, assignee, priority, due date or description |
add_comment | Comments as the key; clients never see it |
edit_comment | Rewrites a comment this connection posted; never anyone else's |
delete_comment | Deletes a comment this connection posted, with its replies; never anyone else's |
A read-only key or connection sees only the first seven. The tools run the same code as the REST endpoints, so they follow the same permissions, refuse the same things and send the same webhooks. The server speaks MCP's Streamable HTTP transport (protocol version 2025-06-18) without sessions, and answers every call with plain JSON.
Webhooks
Agents also need to hear what happens without polling. Add an endpoint in the same settings card: an https:// URL and the events it wants.
| Event | Sent when | data holds |
|---|---|---|
task.created | A quest is created | task, by |
task.status_changed | A quest's status changes | task, from, to, by |
task.assigned | A quest's assignees change | task, by |
task.updated | A quest is renamed or moved to another list | task, changes, by |
task.deleted | A quest is deleted, with its subtasks | task (its id, name, list and parent), by |
task.restored | A deleted quest comes back from the trash | task, by |
comment.created | Someone comments on a quest | task, comment |
The body is JSON: { "id", "event", "created_at", "data" }. The task inside is the same object GET /tasks/:id returns. Send test in Settings delivers a ping event right away. Webhooks go to every endpoint in the organization, whichever key made the change.
Verify the signature
Every delivery carries three headers: Tengu-Event, Tengu-Delivery (the delivery's ID) and Tengu-Signature. The signature looks like t=1759500000,v1=5d41…. The v1 part is the hex HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint's whsec_… secret.
import { createHmac, timingSafeEqual } from "node:crypto"
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")))
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex")
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
return fresh && expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
}
Check the raw body before you parse it, and reject old timestamps so a captured delivery can't be replayed.
Retries
Any 2xx answer counts as delivered. Anything else, or no answer within 5 seconds, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, about 8.5 hours in all. Redirects are not followed. Use Tengu-Delivery to ignore a delivery you have already handled. Settings shows each endpoint's last status and last error.
Errors
Errors come back as { "error": "…" } in plain English.
| Status | Means |
|---|---|
400 | A field is missing or malformed |
401 | No key, or a revoked or expired one |
402 | The workspace is on hold. Stop and don't retry: calls work again once it is back |
403 | The key is read-only and the call writes, or the key's owner may not make that change |
404 | No such task, list or member in this organization, or one the key can't see |
409 | The list lives in ClickUp and the key's owner hasn't connected ClickUp |
422 | That status isn't valid for this list, or the list doesn't take that change |
429 | Too many calls with this key. Wait for Retry-After seconds |
500 | Something failed on Tengu's side. Try again in a moment |
503 | ClickUp didn't answer. Try again |
In the MCP server, a refused tool call comes back as a tool result marked as an error, with the same words. A read-only key calling a tool that writes gets 403.
Agents are never seats
Tengu bills per seat: each person who can sign in and work takes one. An API key is not a person, so it never takes a seat. Neither does Claude Code, nor Tengu Bot. Quests an agent creates give nobody quest-giver XP, so nobody can farm levels with a script.

