Skip to content
All posts

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.

Bryan Gaurano

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 404 as 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 401 from 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 429 with a Retry-After header.

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.

MethodPathWhat it does
GET/listsLists the key can see, with their statuses in order and what each one means
GET/tasksTop-level tasks, newest change first. Filter with list_id, status, assignee, text and more
POST/tasksCreates a task in a list
GET/tasks/:idOne task, with its description, status role and people
PATCH/tasks/:idChanges title, status, priority, due date, description or assignee
GET/tasks/:id/commentsComments Tengu holds on a task, oldest first
POST/tasks/:id/commentsComments as the agent
POST/tasks/:id/imagesStarts an image upload for a comment, such as a screenshot
POST/tasks/:id/images/completeFinishes 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 as review to 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

bash
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

bash
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, ![checkout](url). 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:

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:

ToolWhat it does
list_listsThe lists the key can see, with their statuses and what each one means
list_tasksFinds tasks by list, status or role, assignee or text, newest change first
get_taskOne task with its comments
needs_attentionWhat waits on you: your QA queue, blocked, sent back, overdue, due soon, stuck, unassigned and ready to ship
project_statusEach project's lists: open work by status and stage, overdue, blocked, in review and who holds what
searchFinds tasks by words, with a link to each
fetchOne task as a document: its facts, description and comments
create_taskCreates a task, signed with the key's name
update_taskChanges title, status, assignee, priority, due date or description
add_commentComments as the key; clients never see it
edit_commentRewrites a comment this connection posted; never anyone else's
delete_commentDeletes 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.

EventSent whendata holds
task.createdA quest is createdtask, by
task.status_changedA quest's status changestask, from, to, by
task.assignedA quest's assignees changetask, by
task.updatedA quest is renamed or moved to another listtask, changes, by
task.deletedA quest is deleted, with its subtaskstask (its id, name, list and parent), by
task.restoredA deleted quest comes back from the trashtask, by
comment.createdSomeone comments on a questtask, 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.

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

StatusMeans
400A field is missing or malformed
401No key, or a revoked or expired one
402The workspace is on hold. Stop and don't retry: calls work again once it is back
403The key is read-only and the call writes, or the key's owner may not make that change
404No such task, list or member in this organization, or one the key can't see
409The list lives in ClickUp and the key's owner hasn't connected ClickUp
422That status isn't valid for this list, or the list doesn't take that change
429Too many calls with this key. Wait for Retry-After seconds
500Something failed on Tengu's side. Try again in a moment
503ClickUp 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.

Start

Book a walkthrough

Bring the agent you already run. We will wire it to a test list on the call. Or join the waitlist, and we will write when your workspace is ready.

Book a 20-minute walkthrough

We use your email to write when your workspace is ready, and we never sell it. To come off the list, write to hello@tengu.work. Privacy

More posts

All posts