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

  1. Register your Agent

    An Agent is anything that sends requests: a script, a workflow, an AI Agent. invocation says how answers reach it — persistent (it fetches them, or keeps a WebSocket open) or webhook (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 once
    import 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 once
  2. Get 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}'}
  3. 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 a paired event 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')
  4. Send a request

    The payload is the request’s interface (see Requests are interfaces). correlationId is 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}
  5. 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. reply is 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

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:

ToolWhat it does
list_agentsYour account’s Agents
create_agentA new Agent in your account, paired with your Profile
list_recipientsThe people paired with an Agent, and pending invitations
pairing_codeA one-time code for someone to pair (24 hours), with its link and QR code
get_ui_guideHow to build a request’s interface (the building blocks and the guidelines)
check_uiRuns an interface in the Taskriz runtime: the error, or a preview
send_requestChecks and sends a request — to paired people, or by email as an invitation
get_repliesAnswers, actions people started, pairings, and invitation results
acknowledgeMarks events handled
set_catalogReplaces the Agent’s request catalog
recall_requestWithdraws a request from everyone it was sent to
create_attachmentAttaches a file (upload link, URL, or inline)
get_attachmentDownloads 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. …"
}
StatusMeans
400A field is missing or invalid ({"error": "…"} says which)
401No valid token (or, on /token, wrong credentials)
402plan_limit — the plan’s requests, people or Agents are used up
403Not an Agent token, or someone who hasn’t paired with your Agent
404No such thing (or not yours)
409The 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.