Skip to content
← Virtual Assistants for AI Businesses

Agent API — Virtual Assistant Delegation

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.

Base URL: 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 →

Quickstart

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": "awaiting_customer_start",
  "message": "NOT started until you click Start. https://taskbullet.com/dashboard?tab=work&startTask=PENDING_TASK_ID"
}

Remote MCP (Grok Bot / Cursor / OpenClaw)

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 until completed, failed, or cancelled, or pass webhookUrl.
Customer cancel is cancelled and does not send the completion webhook.
Never put passwords, OTP, recovery keys, cookies, or API keys in the payload.
Never invent VA results.

Grok Bot

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.

OpenClaw

Other lobsters install the skill with openclaw skills install @griffmurder/taskbullet. The lobster cannot sign up or mint a key. It tells the human:

  1. No account → start free (10 hours) /get-started?source=openclaw
  2. Issue tb_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.
  3. Then 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.

Grok Build / Cursor config

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

Authentication

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.

One-time reveal: Your full API key and webhook secret are shown only once at creation time. Store them in a secrets manager (e.g. process.env) immediately.
// All v1 endpoints
x-api-key: tb_live_<64 hex chars>
POST

/api/v1/delegate

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.

Request body

FieldTypeDescription
taskrequiredstringWhat you want done. Max 5,000 chars. Used as the Basecamp todo title (first 240 chars).
contextstringAdditional 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".
dueDatestringFree-form due date hint (e.g. "EOD Friday"). Appended to task description.
webhookUrlstringPublic HTTPS URL. TaskBullet POSTs when you close the job (client_complete). A VA checkbox does not send this webhook. A customer cancel does not send it. Localhost and private IPs are rejected.
sourceAgentstringName of the calling agent for audit trails (e.g. "claude-3-opus").
maxHoursnumberSpend 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.

Response 200 OK

{
  "success": true,
  "taskId": "atask_a3f9c2d1e4b5a6f7",   // stable ID — use to poll status
  "status": "awaiting_customer_start",
  "message": "NOT started until you click Start. https://taskbullet.com/dashboard?tab=work&startTask=PENDING_TASK_ID"
}

"awaiting_customer_start" means the task is NOT started until the customer clicks Start on Work. It is only returned by this endpoint and will not appear in subsequent polls. Poll status stays "pending" until the task is on the board.

402 is only a live Ops read that is not above zero. It includes hoursRemaining and addHoursUrl so the agent can send the client to add hours without guessing. A cap that does not fit open caps is 422 with hoursRemaining and capRoom. Hours remain. Lower maxHours to capRoom or wait. That response has no addHoursUrl.

GET

/api/v1/balance

Remaining hours and cap room for this API key. Call before delegate_task. Returns only hour fields — no other account data. canDelegate false while hoursRemaining is above zero means lower maxHours to capRoom or wait. It is not a buy-hours signal. capRoom is omitted when Ops did not return a live figure.

{
  "hoursRemaining": 8.5,
  "canDelegate": true,
  "addHoursUrl": "https://taskbullet.com/packages-pricing",
  "hoursUnavailable": false,
  "capRoom": 8.5
}
GET

/api/v1/tasks

List delegated tasks for your account. Optionally filter by status.

Query params

ParamValuesDescription
status"pending" | "in_progress" | "completed" | "failed" | "cancelled" | "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."
    }
  ]
}
GET

/api/v1/tasks/:ref

Poll the status of a specific task by its taskId returned from /delegate. Stop when status is completed, failed, or cancelled. A customer cancel is cancelled. It does not send the completion webhook.

// GET /api/v1/tasks/atask_a3f9c2d1e4b5a6f7
{
  "taskId": "atask_a3f9c2d1e4b5a6f7",
  "status": "completed",        // pending | in_progress | completed | failed | cancelled
  "priority": "normal",
  "createdAt": "2026-05-28T14:00:00.000Z",
  "result": "Completed the research. List attached in Basecamp.",
  "completedAt": "2026-05-29T09:15:00.000Z"
}
StatusMeaning
pendingFiled, not on the board
in_progressOn the board. The VA has not delivered, and you have not closed it.
completedYou closed it. Only client_complete.
failedThe existing failed status only. A missing board card is a hold, not a failure.
cancelledYou cancelled it. Stop polling. Not a failure, and not a hold. No completion webhook.

Calls

Human phone calls are pay as you go from the calls wallet. Only the US and Canada can be placed today. A priced country that is not live returns 422 country_not_enabled with country and tier.

Required fields

phone, business_name, purpose (info, scheduling, account_servicing, vendor_ops), goal (at most 4000 characters), questions, on_behalf_of, deadline (ISO-8601), and consent_attestation true.

A question is { id, ask, type? }. ask is at least 8 characters and at most 1000. At most 20 questions. An over-long goal or question is 422 field_too_long with field and limit. It is not truncated.

Optional fields

language (default en), record_call false or omitted, script_mode, script, instructions, extension (digits, passed into the VA instructions, not billed), time_window as 24-hour HH:MM-HH:MM with the end after the start, recipient_timezone, allow_outside_business_hours, max_attempts (default 3, max 5), max_minutes (default 5, max 60), and callback_url.

US and Canada numbers such as 801-414-4664, (801) 414-4664, and +1 801 414 4664 are stored as E.164. If recipient_timezone is omitted, TaskBullet infers it from the area code and says so with recipient_timezone_inferred. An uninferable number stays needs_info. There is no UTC fill-in.

Endpoints

  • POST /api/v1/calls/validate — no call and no hold. Returns hold_cents and whether the time zone was inferred.
  • POST /api/v1/calls — Idempotency-Key required. Replaying the key after a needs_info update returns the current call.
  • GET /api/v1/calls/{id}
  • PATCH /api/v1/calls/{id} — needs_info only.
  • POST /api/v1/calls/{id}/cancel — releases the hold before the first dial.
  • GET /api/v1/calls
  • GET /api/v1/calls/{id}/transcript.txt — empty is transcript_not_ready. A cancelled call is call_cancelled.
  • GET /api/v1/balance — balance_cents, wallet_cents, holds_cents, addFundsUrl.
  • POST /api/v1/billing/add-funds — body topup_cents. GET does not create a Stripe session.
  • POST /api/v1/accounts and PATCH /api/v1/accounts/me — the webhook secret is shown once, and again only on rotate.

Statuses, outcomes, and webhooks

Statuses: needs_info, queued, assigned, in_progress, completed, failed, blocked, cancelled. Finished outcomes include success, partial, no_answer, voicemail, voicemail_left, busy, wrong_number, refused, ended_insufficient_funds, and expired. A call that is still in progress does not report no_answer.

Webhook events: call.needs_info, call.queued, call.assigned, call.in_progress, call.attempted, call.redial, call.completed, call.failed, call.blocked, call.cancelled, call.auto_topup_failed. call.redial includes next_attempt_at and attempt. The signature is x-taskbullet-timestamp (unix seconds) and x-taskbullet-signature, hex HMAC-SHA256 of {unixSeconds}.{rawBody}, with no sha256= prefix.

Hold, no-answer charge, and tiers

The hold is the full call: the tier minimum once, or max_minutes times the per-minute rate, whichever is higher. Default max_minutes is 5. Tier 1 is $1 per minute, so the default hold is $5. Raise max_minutes on that call to hold more. A call that is never answered costs the tier minimum once. Redials after no-answer, voicemail, or busy are free. Ring time is not billed.

TierMinimumPer minuteCountries
1$2.50$1.00US, Canada, Puerto Rico
2$3.00$1.25US Virgin Islands, Singapore, Bermuda, Guam, India, Australia, New Zealand
3$3.50$1.50Northern Mariana Islands, Ireland, Nigeria, Philippines, Trinidad and Tobago, Gibraltar, Kenya
4$4.50$2.00Cayman Islands, American Samoa, Bahamas, Jamaica, British Virgin Islands, South Africa, United Kingdom, Belize, Ghana, Barbados, Malta, Turks and Caicos

Only US and Canada are live. Premium and toll-free numbers return number_blocked.

Calls errors

413 payload_too_large names the 32 KiB limit. 422 country_not_enabled, country_quote_only, number_blocked, field_too_long, and do_not_call are top-level codes. needs_lead_time includes a message when the deadline is under 60 minutes from now. A huge body is 413, not a generic failure.

Webhooks

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. A customer cancel does not send it. Verify the signature before processing.

Payload

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"
}

Signature verification (Node.js)

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.

Errors

StatusCause
400Missing or invalid body, oversize fields, or secrets in title/description
401Missing, invalid, or revoked API key
402Live Ops hours are not above zero — addHoursUrl is /packages-pricing (not trial)
422Requested cap does not fit open caps. Hours remain. Lower maxHours to capRoom or wait. No addHoursUrl.
413Request body larger than 32 KiB
429Rate limit exceeded (60 req / 60s on this instance; it does not hold across instances) — respect Retry-After
500Internal server error

Error responses include a JSON body with error. 402 also includes hoursRemaining and addHoursUrl. 422 also includes hoursRemaining and capRoom.

Ready to integrate?

Issue your API key from the dashboard settings, then connect your AI agent or automation workflow.

Get your API key