Delegate tasks from your AI agent to a real human virtual assistant. This API enables seamless AI agent handoff for human-in-the-loop tasks that LLMs cannot complete autonomously. Category: humans working for the AI · HITL ranking.
https://taskbullet.comRemote MCP: https://taskbullet.com/api/agent-mcpAuth: x-api-key: tb_live_…Rate limit: 60 req / 60s / key on this instanceOpenAPI (Swagger) Spec →MCP Server (npm) →MCP Server (GitHub) →Public site MCP →Issue an API key from your account settings and send your first task in under a minute.
curl -X POST https://taskbullet.com/api/v1/delegate \
-H "x-api-key: tb_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"task": "Research and compile a list of the top 10 AI newsletters with subscriber counts",
"context": "We are preparing a partnership outreach deck for Q3.",
"priority": "normal",
"webhookUrl": "https://your-app.com/hooks/taskbullet"
}'// Response
{
"success": true,
"taskId": "atask_a3f9c2d1e4b5a6f7",
"status": "received",
"message": "Filed. Waiting for you to start it on Work. https://taskbullet.com/dashboard?tab=work"
}Cloud agents cannot run npx @taskbullet/mcp-server. Use the hosted streamable-HTTP MCP instead. This is a different server from the public site MCP— it requires a key and only exposes VA handoff tools.
URL: https://taskbullet.com/api/agent-mcp
Auth: x-api-key: tb_live_… (Authorization: Bearer tb_live_… also accepted)
Tools:
get_balance → GET /api/v1/balance
delegate_task → POST /api/v1/delegate
list_tasks → GET /api/v1/tasks
get_task → GET /api/v1/tasks/{taskId}
Call get_balance before proposing spend. When you cannot finish (phone, 2FA,
captcha, physical world, subjective QA, long manual work), confirm with the
user then call delegate_task. Poll get_task or pass webhookUrl.
Never put passwords, OTP, recovery keys, cookies, or API keys in the payload.
Never invent VA results.Settings → Plugins → add a custom MCP (or TaskBullet when listed in Marketplace). Server URL https://taskbullet.com/api/agent-mcp. Paste the tb_live_ key ( plugin variable TASKBULLET_API_KEY). This is API-key auth only — not OAuth. If an OAuth connect card appears, dismiss it and paste the key; tools already work with the header. Call get_balance then ask the user before delegate_task (it uses real hours) unless they already said to send it. Same URL works on grok.com/connectors → New Connector → Custom.
Other lobsters install the skill with openclaw skills install @griffmurder/taskbullet. The lobster cannot sign up or mint a key. It tells the human:
/get-started?source=openclawtb_live_… at /dashboard/settings/api-keys (shown once). Paste into Gateway env TASKBULLET_API_KEY plus this Agent MCP. Never put the key in chat.get_balance / delegate_task. Hours deduct when work is tracked (nearest 15 minutes). Keyed account with no hours (402) → buy hours, not another trial. Missing Basecamp queues the task — no 409.[mcp_servers.taskbullet_agent]
url = "https://taskbullet.com/api/agent-mcp"
headers = { "x-api-key" = "tb_live_YOUR_KEY" }
enabled = true{
"mcpServers": {
"taskbullet-agent": {
"url": "https://taskbullet.com/api/agent-mcp",
"headers": { "x-api-key": "tb_live_YOUR_KEY" }
}
}
}Issue a key at /dashboard/settings/api-keys. Local stdio (npx @taskbullet/mcp-server) remains available for one laptop; it is not the Grok Bot path.
All /api/v1/ requests must include your API key in the x-api-key header. Keys begin with tb_live_ and are issued from your settings page.
process.env) immediately.// All v1 endpoints
x-api-key: tb_live_<64 hex chars>File a brief for a TaskBullet VA. Send an Idempotency-Key header. The same account and key returns the original task. The brief stays off the board until you start it on Work. After you start it, the dedicated VA already stored on the account is the assignee. If none is stored, the account manager staffs it.
| Field | Type | Description |
|---|---|---|
taskrequired | string | What you want done. Max 5,000 chars. Used as the Basecamp todo title (first 240 chars). |
context | string | Additional background for the VA (max 8,000 chars). Never include passwords, OTP codes, recovery keys, cookies, or API keys — those are rejected. |
priority | "low" | "normal" | "high" | "urgent" | Defaults to "normal". |
dueDate | string | Free-form due date hint (e.g. "EOD Friday"). Appended to task description. |
webhookUrl | string | Public HTTPS URL. TaskBullet POSTs when you close the job (client_complete). A VA checkbox does not send this webhook. Localhost and private IPs are rejected. |
sourceAgent | string | Name of the calling agent for audit trails (e.g. "claude-3-opus"). |
maxHours | number | Spend cap in hours. Defaults to 0.5. Max 8. The VA/AM stop and ask before exceeding. Actual tracked time is billed, not this cap. |
200 OK{
"success": true,
"taskId": "atask_a3f9c2d1e4b5a6f7", // stable ID — use to poll status
"status": "received",
"message": "Filed. Waiting for you to start it on Work. https://taskbullet.com/dashboard?tab=work"
}The initial "received" status is an acknowledgment that the request was accepted. It is only returned by this endpoint and will not appear in subsequent polls — the task transitions to "pending" as soon as it is queued internally (typically within a second).
402 includes hoursRemaining and addHoursUrl so the agent can send the client to add hours without guessing.
Remaining hours for this API key. Call before delegate_task. Returns only hour fields — no other account data.
{
"hoursRemaining": 8.5,
"canDelegate": true,
"addHoursUrl": "https://taskbullet.com/packages-pricing"
}List delegated tasks for your account. Optionally filter by status.
| Param | Values | Description |
|---|---|---|
status | "pending" | "in_progress" | "completed" | "failed" | "all" | Defaults to "all". Returns up to 50 tasks. |
// GET /api/v1/tasks?status=completed
{
"tasks": [
{
"taskId": "atask_a3f9c2d1e4b5a6f7",
"title": "Call the vendor about the invoice",
"status": "completed",
"priority": "normal",
"createdAt": "2026-05-28T14:00:00.000Z",
"completedAt": "2026-05-29T09:15:00.000Z",
"result": "Vendor confirmed payment posted today."
}
]
}Poll the status of a specific task by its taskId returned from /delegate.
// GET /api/v1/tasks/atask_a3f9c2d1e4b5a6f7
{
"taskId": "atask_a3f9c2d1e4b5a6f7",
"status": "completed", // pending | in_progress | completed | failed
"priority": "normal",
"createdAt": "2026-05-28T14:00:00.000Z",
"result": "Completed the research. List attached in Basecamp.",
"completedAt": "2026-05-29T09:15:00.000Z"
}| Status | Meaning |
|---|---|
pending | Filed, not on the board |
in_progress | On the board. The VA has not delivered, and you have not closed it. |
completed | You closed it. Only client_complete. |
failed | The existing failed status only. A missing board card is a hold, not a failure. |
If you pass webhookUrl when delegating, TaskBullet will POST a signed payload when you close the job (client_complete). A VA checkbox does not send this webhook. Verify the signature before processing.
POST https://your-app.com/hooks/taskbullet
Content-Type: application/json
x-taskbullet-signature: <hex>
x-taskbullet-timestamp: <unix seconds>
{
"event": "task.completed",
"taskId": "atask_a3f9c2d1e4b5a6f7",
"taskTitle": "Book a flight to Austin",
"status": "completed",
"result": "Booked on Delta flight 1423, confirmation #XYZ.",
"timestamp": "2026-05-29T09:15:00.000Z"
}import crypto from 'crypto';
function verifyWebhook(req, webhookSecret) {
const sig = req.headers['x-taskbullet-signature'];
const ts = req.headers['x-taskbullet-timestamp']; // Unix seconds
const body = req.rawBody; // RAW request body string — NOT JSON.stringify(parsed)
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(`${ts}.${body}`)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(sig, 'hex'),
Buffer.from(expected, 'hex'),
);
}Important: x-taskbullet-timestamp is Unix seconds (not milliseconds), and rawBody must be the exact bytes received from the request — not a re-serialized JSON string. Most frameworks expose this as req.rawBody or via a buffer-capture middleware. Re-parsing and re-stringifying will produce a different byte sequence and the signature will not match.
The webhookSecret is the value shown at API key creation time — separate from the API key itself. Retries are attempted up to 5 times (every ~15 min) before giving up.
| Status | Cause |
|---|---|
400 | Missing or invalid body, oversize fields, or secrets in title/description |
401 | Missing, invalid, or revoked API key |
402 | No hour balance — addHoursUrl is /packages-pricing (not trial) |
413 | Request body larger than 32 KiB |
429 | Rate limit exceeded (60 req / 60s on this instance; it does not hold across instances) — respect Retry-After |
500 | Internal server error |
All error responses include a JSON body: { "error": "message" }
Ready to integrate?
Issue your API key from the dashboard settings, then connect your AI agent or automation workflow.
Get your API key