Developers
Give your Agent a way to ask people — and get structured answers back. Use the plugins for Claude Code and ChatGPT, or the Taskriz API for any Agent or workflow. Designing requests people answer in seconds? See the For Agents guide.
Two ways in
The plugins
For Claude Code and ChatGPT. Install, sign in when your Agent first asks, and it can create Agents, design requests and send them — no code.
/plugin marketplace add appeggio-com/taskriz-claude-plugin
/plugin install taskriz@taskriz
codex plugin marketplace add appeggio-com/taskriz-chatgpt-plugin
Both use the Taskriz MCP server at https://www.taskriz.com/mcp (OAuth sign-in) — any MCP client can.
The API
For any Agent, script or workflow that can make HTTPS calls. JSON in, JSON out.
https://api.taskriz.com/v1/
Agents you create with the plugins belong to your Taskriz account and its plan. Linking Agents registered with the API to an account is coming.
Quickstart: the API
Register your Agent
An Agent is anything that sends requests: a script, a workflow, an AI Agent.
invocationsays how answers reach it —persistent(it fetches them, or keeps a WebSocket open) orwebhook(Taskriz posts them to your HTTPS endpoint). The client secret is shown once; keep it safe.curl -X POST https://api.taskriz.com/v1/register \ -H 'content-type: application/json' \ -d '{"name": "Invoice Approver", "invocation": {"mode": "persistent"}}' # → {"agentId": "agent_…", "clientId": "client_…", "clientSecret": "…"}const res = await fetch('https://api.taskriz.com/v1/register', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'Invoice Approver', invocation: { mode: 'persistent' } }), }); const { agentId, clientId, clientSecret } = await res.json(); // the secret is shown onceimport requests r = requests.post('https://api.taskriz.com/v1/register', json={'name': 'Invoice Approver', 'invocation': {'mode': 'persistent'}}) agent = r.json() # agentId, clientId, clientSecret — the secret is shown onceGet a token
Exchange the credentials for a bearer token (it lasts an hour), and send it as
Authorization: Bearer ….curl -X POST https://api.taskriz.com/v1/token \ -H 'content-type: application/json' \ -d '{"grant_type": "client_credentials", "client_id": "<clientId>", "client_secret": "<clientSecret>"}' # → {"access_token": "…", "token_type": "Bearer", "expires_in": 3600}const { access_token } = await (await fetch('https://api.taskriz.com/v1/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret }), })).json(); const auth = { authorization: `Bearer ${access_token}`, 'content-type': 'application/json' };token = requests.post('https://api.taskriz.com/v1/token', json={ 'grant_type': 'client_credentials', 'client_id': agent['clientId'], 'client_secret': agent['clientSecret'], }).json()['access_token'] auth = {'authorization': f'Bearer {token}'}Pair with a person
Ask for a one-time code (good for 24 hours) and give it to the person: they scan its QR code (
qrCode, a PNG) with their phone’s camera or open its link (pairingUrl) — Taskriz opens at Pair an Agent with the code filled in — or they enter the code in Taskriz (+ → Pair an Agent). Only people who have paired with your Agent can receive its requests — and they can unpair at any time. When someone pairs, your Agent gets apairedevent with their email and connection.curl -X POST https://api.taskriz.com/v1/pairing -H 'authorization: Bearer <access_token>' # → {"connectionId": "…", "manualCode": "K7Q2-9XW4-M3PA", "pairingUrl": "https://www.taskriz.com/pair/K7Q2-9XW4-M3PA", "qrCode": "data:image/png;base64,…", "expiresIn": 86400, …}const { connectionId, manualCode } = await (await fetch('https://api.taskriz.com/v1/pairing', { method: 'POST', headers: auth })).json(); console.log(`Enter ${manualCode} in Taskriz → + → Pair an Agent`);offer = requests.post('https://api.taskriz.com/v1/pairing', headers=auth).json() print('Enter', offer['manualCode'], 'in Taskriz → + → Pair an Agent')Send a request
The
payloadis the request’s interface (see Requests are interfaces).correlationIdis your thread: the answer comes back with it.curl -X POST https://api.taskriz.com/v1/send \ -H 'authorization: Bearer <access_token>' -H 'content-type: application/json' \ -d @- <<'JSON' { "to": [ "<connectionId>" ], "kind": "ui", "correlationId": "invoice-4567", "importance": "high", "subject": "Approve invoice #4567", "summary": "Acme Ltd · $1,240 · due Friday", "payload": "ScreenMorph new title: 'Invoice #4567'; addMorph: (LabelMorph new text: 'Acme Ltd sent invoice #4567 for $1,240.'); addMorph: (ButtonMorph new label: 'Approve'; onClick: [Taskriz current reply: #{ #decision -> 'approve' }]); addMorph: (ButtonMorph new label: 'Query'; style: #plain; onClick: [Taskriz current reply: #{ #decision -> 'query' }]); yourself" } JSON # → 202 {"messageId": "01K…", "recipients": 1}const ui = `ScreenMorph new title: 'Invoice #4567';\n addMorph: (LabelMorph new text: 'Acme Ltd sent invoice #4567 for $1,240.');\n addMorph: (ButtonMorph new label: 'Approve'; onClick: [Taskriz current reply: #{ #decision -> 'approve' }]);\n addMorph: (ButtonMorph new label: 'Query'; style: #plain; onClick: [Taskriz current reply: #{ #decision -> 'query' }]);\n yourself`; const sent = await (await fetch('https://api.taskriz.com/v1/send', { method: 'POST', headers: auth, body: JSON.stringify({ to: [connectionId], kind: 'ui', correlationId: 'invoice-4567', importance: 'high', subject: 'Approve invoice #4567', summary: 'Acme Ltd · $1,240 · due Friday', payload: ui, }), })).json(); // { messageId, recipients }ui = ("ScreenMorph new title: 'Invoice #4567';" " addMorph: (LabelMorph new text: 'Acme Ltd sent invoice #4567 for $1,240.');" " addMorph: (ButtonMorph new label: 'Approve'; onClick: [Taskriz current reply: #{ #decision -> 'approve' }]);" " addMorph: (ButtonMorph new label: 'Query'; style: #plain; onClick: [Taskriz current reply: #{ #decision -> 'query' }]);" " yourself") sent = requests.post('https://api.taskriz.com/v1/send', headers=auth, json={ 'to': [offer['connectionId']], 'kind': 'ui', 'correlationId': 'invoice-4567', 'importance': 'high', 'subject': 'Approve invoice #4567', 'summary': 'Acme Ltd · $1,240 · due Friday', 'payload': ui, }).json() # {'messageId': …, 'recipients': 1}Get the answer
Answers arrive as events. Fetch them (or receive them by webhook or WebSocket — see Getting answers), then acknowledge the ones you’ve handled.
replyis the structured answer.curl https://api.taskriz.com/v1/events -H 'authorization: Bearer <access_token>' curl -X POST https://api.taskriz.com/v1/events/ack \ -H 'authorization: Bearer <access_token>' -H 'content-type: application/json' \ -d '{"ids": ["01K…"]}'const { events } = await (await fetch('https://api.taskriz.com/v1/events', { headers: auth })).json(); for (const e of events) { if (e.kind === 'reply' && e.correlationId === 'invoice-4567') console.log(e.reply); // { decision: 'approve' } } await fetch('https://api.taskriz.com/v1/events/ack', { method: 'POST', headers: auth, body: JSON.stringify({ ids: events.map((e) => e.id) }) });events = requests.get('https://api.taskriz.com/v1/events', headers=auth).json()['events'] for e in events: if e['kind'] == 'reply' and e['correlationId'] == 'invoice-4567': print(e['reply']) # {'decision': 'approve'} requests.post('https://api.taskriz.com/v1/events/ack', headers=auth, json={'ids': [e['id'] for e in events]})
An event, as it arrives:
{
"v": 1,
"id": "01K…",
"kind": "reply",
"from": "prof_…",
"connectionId": "…",
"correlationId": "invoice-4567",
"inReplyTo": "01K…",
"timestamp": "2026-10-11T09:41:00Z",
"contentType": "application/json",
"payload": "#{#decision->'approve'}",
"reply": {
"decision": "approve"
},
"attempts": 1
}
Requests are interfaces
A request’s payload is its interface: a short program, in Smalltalk, built from familiar iOS-style building blocks — screens, lists, text fields, switches, pickers, date pickers, photo and document pickers, buttons. Taskriz runs it in a sandbox on the person’s own device, following world-leading human interface guidelines, so it looks and feels native wherever they open it. The last statement is a ScreenMorph … yourself; a button’s Taskriz current reply: #{ … } sends the answer back as JSON in the event’s reply.
| colour note |
colour := SegmentedControlMorph new label: 'Colour'; segments: #('Red' 'Green' 'Blue'); selected: 'Green'.
note := TextFieldMorph new label: 'Why?'; placeholder: 'Optional'.
ScreenMorph new
title: 'Favourite Colour';
addMorph: (LabelMorph new text: 'Which colour do you like best?'; textStyle: #headline);
addMorph: (ListMorph new style: #insetGrouped; addMorph: colour; addMorph: note; yourself);
addMorph: (ButtonMorph new label: 'Send';
onClick: [Taskriz current reply: #{ #colour -> colour selected. #why -> note value }]);
yourself
The plugins write these for you: get_ui_guide has the building blocks and the guidelines, and check_ui runs an interface first and shows you a preview. A photo or document picker’s files come back in the reply as {id, name, type, size}.
Getting answers
- Fetch them —
GET /v1/events(oldest first, up to 100 at a time), thenPOST /v1/events/ack. Unacknowledged events are kept for 30 days. - Webhook — register with
"invocation": {"mode": "webhook", "endpoint": "https://…"}and Taskriz posts each event to your HTTPS endpoint, signed with yourwebhookSecretin aTaskriz-Signatureheader (andTaskriz-Event-Id). A 2xx means delivered (still acknowledge it); otherwise Taskriz retries for 24 hours. - WebSocket — a
persistentAgent can keepwss://www.taskriz.com/prod?token=<access_token>open: events arrive as they happen; send{"action": "sync"}to replay the unacknowledged ones.
Events are answers (reply), actions people start (initiate), what became of email invitations (invitation), and read receipts (receipt): delivered when a request reaches the person’s device, then read when they open it — one per person, each once, with inReplyTo naming the request. Leave receipts out with GET /v1/events?exclude=receipt.
Delivery is at least once — de-duplicate by the event’s id. Checking a webhook’s signature:
import { createHmac, timingSafeEqual } from 'node:crypto';
// Taskriz-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>"> — keyed by your webhookSecret
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // not a replay
const want = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return parts.v1?.length === want.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(want));
}
The request catalog
Not every conversation starts with your Agent. A request catalog lists actions people can start themselves — “Report a problem”, “Request a statement”. Set it when you register, or later with POST /v1/update (it replaces the whole list; up to 50 entries).
{
"catalog": [
{
"id": "report-problem",
"label": "Report a problem",
"description": "Tell us what went wrong",
"importance": "high"
},
{
"id": "request-statement",
"label": "Request a statement"
}
]
}
When someone picks one in Taskriz, your Agent gets an initiate event with its actionId and a new correlationId. Answer with POST /v1/send on that correlationId — your interface collects whatever you need.
Files
POST /v1/attachments makes an attachment three ways: {name, type, size} for an upload link (15 minutes), {url} for Taskriz to fetch, or {content} inline (base64, small files). Put the ids in a request’s attachments. Files people send back are in the event; GET /v1/attachments/{id} gives a download link (5 minutes). Up to 100 MB a file, 20 per request.
Threads and recall
Your correlationId is the thread: requests and answers that share it are grouped for the person, and each answer names the request it answers (inReplyTo). Changed your mind? POST /v1/recall with the messageId withdraws it from everyone it was sent to.
Every request is sealed to the person’s own devices — see Security.
MCP tools
The Taskriz MCP server (https://www.taskriz.com/mcp, OAuth sign-in) gives an AI Agent these tools:
| Tool | What it does |
|---|---|
list_agents | Your account’s Agents |
create_agent | A new Agent in your account, paired with your Profile |
list_recipients | The people paired with an Agent, and pending invitations |
pairing_code | A one-time code for someone to pair (24 hours), with its link and QR code |
get_ui_guide | How to build a request’s interface (the building blocks and the guidelines) |
check_ui | Runs an interface in the Taskriz runtime: the error, or a preview |
send_request | Checks and sends a request — to paired people, or by email as an invitation |
get_replies | Answers, actions people started, pairings, and invitation results |
acknowledge | Marks events handled |
set_catalog | Replaces the Agent’s request catalog |
recall_request | Withdraws a request from everyone it was sent to |
create_attachment | Attaches a file (upload link, URL, or inline) |
get_attachment | Downloads a file someone sent back |
Limits and errors
Each person a request reaches counts as one request, against your plan’s monthly limit — see Pricing. Over the limit, a request is refused whole, before anything is delivered:
{
"error": "plan_limit",
"plan": "free",
"limit": 500,
"used": 500,
"upgradeUrl": "https://www.taskriz.com/pricing/",
"message": "Not sent: this account’s Free plan includes 500 requests a month (each person reached counts as one), and 500 are used. …"
}
| Status | Means |
|---|---|
400 | A field is missing or invalid ({"error": "…"} says which) |
401 | No valid token (or, on /token, wrong credentials) |
402 | plan_limit — the plan’s requests, people or Agents are used up |
403 | Not an Agent token, or someone who hasn’t paired with your Agent |
404 | No such thing (or not yours) |
409 | The person hasn’t set up Taskriz on a device yet |
Also: subjects up to 200 characters and summaries up to 500; groups of up to 50 people; up to 100 events a fetch.
Versioning
The version is in the path: /v1. Within v1 changes are additive — new fields and new endpoints, never removals — so ignore what you don’t recognise. A breaking change would come as /v2, beside v1. Each request also carries the runtime version its interface was written for, so a request keeps working after the runtime moves on.