Tasks
Overview
Tasks let an AI such as ChatGPT or Claude run work on your own server (a worker). Say "Place an order for 30 of A-1042 in our ERP", and the AI sends the job to your worker and shows you the result in the conversation.
- 3Min API only delivers the task and records its status — the worker does the actual work
- A worker is an HTTP server that you run
When to use it
- Having internal systems do something — place an ERP order, check stock, search an internal database
- Work that takes time — generating reports, bulk conversions (up to 24 hours)
- Scripts that only run on your own machine — attach a public URL with a tunnel (see below)
If the goal is to store and collect data, use an endpoint instead.
Not comfortable writing worker code? Ask the AI you already use: "Build me a 3minapi task worker". The AI reads the same contract as this page from the
helptool and writes the code.
How to get here
- Register a worker: Dashboard → Tasks → Workers tab →
Register worker - Check runs: Dashboard → Tasks → Runs tab
How it works
① Register a worker on the web (once)
② AI → task_create task created, status working
③ 3Min API → POST to worker signed request (includes callback_url)
④ Worker → 2xx right away "received" only, not a result
⑤ Worker → callback_url progress messages (optional) → completed or failed
⑥ AI → task_get reads the result
| Where | What happens there |
|---|---|
| Web dashboard | Register, edit and delete workers; view and rotate secrets; view runs |
| AI chat | List workers, send tasks, check status and results, cancel |
| Worker server | Receive requests, do the work, report the result |
The AI cannot see a worker's URL or secrets. Both are handled on the web only.
Registering a worker (web)
| Field | Description | Limit |
|---|---|---|
| Name | Required. The name the AI uses. Unique regardless of case, cannot be changed | 50 characters |
| Description | Required. The AI picks a worker from this description | 500 characters |
| Worker URL | Required. The address that receives tasks by POST | 2,048 bytes |
| Authorization header / Authorization value | Optional. For the worker's own auth. With only a value, the header is Authorization |
128 / 8,192 bytes |
- Write a specific description. The AI sees only the name and description, never the URL. Instead of
erp-1, write something like "Places an order in our ERP and returns the order number" — what it takes and what it returns — so the AI can pick it
Two secrets, opposite directions
Registering issues two secrets automatically. Find them on the worker page (Dashboard → Tasks → Workers tab → select a worker) with Show secret · Copy secret.
| Secret | Format | Direction | Used for |
|---|---|---|---|
| Signing secret | whsec_... |
3Min API → worker | Verifying that a request really came from 3Min API |
| Callback key | tm_task_... |
worker → 3Min API | Sent as Authorization: Bearer when reporting |
- The two values are different and not interchangeable. An endpoint API key (
tm_live_·tm_test_) is not a callback key either
| Change | What happens | What the worker should do |
|---|---|---|
Rotate signing secret |
For 48 hours, new tasks carry both the new and the previous signature. Retries of tasks created before the rotation (up to about 5 hours) carry only the previous one | Keep the previous secret for at least 6 hours, then switch within 48 hours |
Regenerate callback key |
The old key stops working immediately (running tasks included) | Switch right away |
Rotating again within 48 hours invalidates the original secret immediately. The dashboard shows only the current secret, so copy it before rotating if your worker needs to keep the previous one.
Building a worker
Five rules for workers
- Return 2xx within 15 seconds. Do the work after responding
- Accept the request with 2xx even if your work fails. Report the failure instead (5xx is retried, other 4xx fail the task at once — see response codes)
- Deduplicate on
task_id. The same task can arrive twice (at-least-once) - Signature verification is optional but recommended. Without it, anyone who knows the worker URL can send fake tasks (signature verification)
- Report the result to
callback_url. Sendcompletedorfailedonce. With no report, the task becomesfailedafter 24 hours (status report API)
Delivery request
The request 3Min API sends to the worker URL.
POST https://worker.example.com/tasks
Content-Type: application/json
webhook-id: 0199a1b2-...
webhook-timestamp: 1757650867
webhook-signature: v1,K3XkZ1n0...
X-3minapi-Task-Id: 0199a1b2-...
X-3minapi-Delivery-Attempt: 1
Authorization: Bearer xxx
| Header | Content |
|---|---|
webhook-id |
Task ID (same value as task_id) |
webhook-timestamp |
Send time (Unix seconds). New on every attempt |
webhook-signature |
Signature. Two space-separated values during rotation (signature verification) |
X-3minapi-Task-Id |
Task ID (same value as webhook-id) |
X-3minapi-Delivery-Attempt |
Which delivery attempt this is (from 1) |
| Your registered auth header | Only if you entered an authorization header and value when registering |
Body:
{
"type": "task.created",
"timestamp": "2026-09-12T04:21:07.123Z",
"data": {
"task_id": "0199a1b2-...",
"input": { "sku": "A-1042", "qty": 30 },
"callback_url": "https://api.3minapi.com/api/v1/tasks/0199a1b2-.../result"
}
}
| Field | Content |
|---|---|
task_id |
Task ID. Use it for deduplication |
input |
The JSON object the AI sent (same values, key order may differ) |
callback_url |
This task's report URL. Send status and results here |
Worker response codes
Only the status code counts; the response body is ignored.
| Response | Result |
|---|---|
| 2xx | Accepted. The task stays working until a report arrives |
| 5xx (except 501 · 505), 429, 15-second timeout, network error | Retried after 30s · 2m · 10m · 1h · 4h — 6 attempts in total (about 5 hours). A 429 Retry-After is honored only when longer |
| Other 4xx (401 · 404 · 409, …), 501, 505 | failed at once, no retry |
- Return 401 when signature verification fails. If the secret was entered wrong, the very first task fails right away, so you find the setup problem quickly
Signature verification (optional)
The webhook-signature header is the signature. 3Min API computes it with the worker's signing secret, and the worker repeats the calculation to check that it matches.
signed content = "{webhook-id}.{webhook-timestamp}.{raw request body}"
key = the signing secret without whsec_, base64-decoded bytes
signature = "v1," + base64( HMAC-SHA256(key, signed content) )
- With an official Standard Webhooks library it takes a few lines (example code). For packages per language and a manual implementation, see Verifying the webhook signature (same scheme)
- Verify against the raw body bytes. Parsing the JSON and serializing it again always fails
- The library rejects a
webhook-timestampmore than 5 minutes off (blocks replayed requests) - When two signatures arrive, either one matching is enough (rotation rules)
- If you don't verify, at least check that
callback_urlstarts withhttps://api.3minapi.com/— your callback key is sent to that address
Status report API
Send status to the callback_url from the delivery request. Progress messages can be sent many times; completed or failed is sent once.
POST https://api.3minapi.com/api/v1/tasks/{task_id}/result
Authorization: Bearer tm_task_...
Content-Type: application/json
- URL:
callback_urlarrives in this format with the task ID filled in. Don't build it yourself — use it as is - Callback key: Callback key on the worker page →
Copy secret. Keep it in the worker server's environment variables or similar
In progress — the task stays working, and the AI sees the message through task_get
{ "status": "working", "status_message": "1/2 Checking stock" }
Completed
{
"status": "completed",
"result": { "order_id": "PO-20260912-001" },
"status_message": "Order PO-20260912-001 placed"
}
Failed
{
"status": "failed",
"error": { "code": 1001, "message": "Out of stock", "data": { "available": 12 } }
}
| Field | Rule |
|---|---|
status |
One of working · completed · failed |
status_message |
Required for working, optional otherwise. Cut off past 256 characters. For failed without one, error.message is used |
result |
Required for completed. Any JSON except null |
error |
Required for failed. code (integer) + message (string); data is optional |
- The whole body must be 100 KB or less.
resultanderrorare never cut off, so a larger body is rejected with 413 - Each accepted report counts as one API call. Send progress messages only at meaningful steps
Status report API response codes
| Code | Meaning | What the worker should do |
|---|---|---|
| 202 | Accepted. Usually applied within a few seconds | — |
| 400 | Malformed body, invalid task ID, NUL character in a string | Fix the code |
| 401 | Missing or wrong callback key (endpoint API keys included) | Check the key |
| 404 | Not a task of this worker | Check callback_url |
| 409 | The task already finished (completed · failed · cancelled) | Stop reporting |
| 413 | Body over 100 KB. Nothing was recorded | If the task is still working, shrink and resend |
| 415 | Content-Type is not JSON | Fix the header |
| 503 | Temporary outage | Resend shortly |
Example code (Node.js)
Requires Node.js 18 or later and "type": "module" in package.json.
// npm i express standardwebhooks
import express from 'express';
import { Webhook } from 'standardwebhooks';
// The signing secret (whsec_...) and the callback key (tm_task_...) are different values
const wh = new Webhook(process.env.TASK_SIGNING_SECRET.replace('whsec_', ''));
const CALLBACK_KEY = process.env.TASK_CALLBACK_KEY;
// Accepted task_ids — store these in a database or Redis in production
const seen = new Set();
const app = express();
// The signature covers the raw bytes. Parsing the JSON first makes verification fail.
app.post('/tasks', express.raw({ type: 'application/json' }), (req, res) => {
let envelope;
try {
envelope = wh.verify(req.body, {
'webhook-id': req.header('webhook-id'),
'webhook-timestamp': req.header('webhook-timestamp'),
'webhook-signature': req.header('webhook-signature')
});
} catch {
return res.sendStatus(401);
}
const { task_id, input, callback_url } = envelope.data;
// Rule 3 — don't process a task you've already accepted
if (seen.has(task_id)) return res.sendStatus(202);
seen.add(task_id);
// Rule 1 — respond first, work afterwards
res.sendStatus(202);
run(input, callback_url);
});
async function run(input, callbackUrl) {
try {
// Progress message — the task stays working; the AI sees it through task_get
await report(callbackUrl, { status: 'working', status_message: '1/2 Checking stock' });
await checkStock(input); // ← your actual work
await report(callbackUrl, { status: 'working', status_message: '2/2 Placing the order' });
const orderId = await createOrder(input);
// Completed — the result plus a short summary
await report(callbackUrl, {
status: 'completed',
result: { order_id: orderId },
status_message: `Order ${orderId} placed`
});
} catch (err) {
// Rule 2 — if the work or a report fails, report failed
await report(callbackUrl, {
status: 'failed',
error: { code: 1001, message: String(err?.message ?? err) },
status_message: 'Could not place the order'
}).catch(console.error);
}
}
// callbackUrl — the callback_url from the delivery request, used as is
async function report(callbackUrl, body) {
for (let attempt = 1; attempt <= 3; attempt++) {
const res = await fetch(callbackUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${CALLBACK_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
}).catch(() => null);
// 202 accepted, 409 task already finished — nothing more to send
if (res?.status === 202 || res?.status === 409) return;
// 400 · 401 · 404 · 413 give the same result when resent
if (res && res.status !== 503) throw new Error(`report rejected: HTTP ${res.status}`);
// Only 503 and network errors are retried after a pause
if (attempt < 3) await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
throw new Error('report failed: service unavailable');
}
app.listen(8080);
Running a worker on your own machine
A worker needs a public URL reachable from the internet. For a machine behind a home or office router, use a tunnel to attach a public HTTPS address to a local port.
cloudflared tunnel --url http://localhost:8080
ngrok http 8080
- Setup and usage: Cloudflare Quick Tunnels · ngrok getting started
- Add your path to the printed address (e.g.
https://xxxx.trycloudflare.com/tasks) and register it as the Worker URL - Temporary addresses without an account change on every run. Update the Worker URL each time, or use the service's fixed-address option
- If the machine is off or the tunnel is down, the task usually becomes
failedafter retries (about 5 hours). A service that returns 404 for an offline address makes itfailedat once
Sending tasks (AI)
Once a worker is registered, you can use it straight from an AI chat. No need to know the tool names.
- "Place an order for 30 of A-1042 in our ERP"
- "What happened to the order I sent earlier?"
- "Cancel the task I just sent"
| Tool | What it does |
|---|---|
worker_list |
Names and descriptions of registered workers (no URLs or secrets) |
task_create |
Creates a task from a worker name and input (JSON object, up to 100 KB) |
task_get |
One task's status, message, and result or error |
task_list |
Recent tasks (without result bodies) |
task_cancel |
Cancels running tasks (up to 100) |
- The same worker and the same
inputwithin 2 minutes (up to about 4 minutes depending on timing) return the existing task. Completed and cancelled tasks are returned as they are; only a failed task is created again - Cancelling does not notify the worker. Only the remaining delivery attempts stop; later reports from a worker that already received the task get 409
Checking status
| Status | Meaning | Can change? |
|---|---|---|
working |
Being delivered, or the worker is processing it | Yes |
completed |
The worker reported completion | Final |
failed |
The worker reported failure, delivery failed, or 24 hours passed | Final |
cancelled |
Cancelled | Final |
The reason for failed is kept in status_message.
| Cause | status_message example |
|---|---|
| Worker reported failure | The worker's message (or error.message if none) |
| Retries exhausted | The task could not be delivered to the worker after 6 attempts (no response within 15s). |
| Immediate failure status | The task could not be delivered to the worker after 1 attempt (HTTP 401). |
| 24 hours passed | Worker did not respond within 24 hours. |
Where to check:
- AI —
task_get·task_list. The worker'sresultis passed on as is, without summarizing - Web — Runs tab. Filter by period (7 · 30 · 60 days; 7 on Free) and status, and click a row to see input, result, error and delivery. If delivery finally failed, Delivery shows attempts, last response and reason
Limits
| Item | Value |
|---|---|
| Workers | 1 on Free / unlimited on paid plans |
| Tasks · concurrent runs | Unlimited (within your monthly API call quota) |
| Deadline | 24 hours after creation — then failed (checked hourly, so up to about 25 hours) |
| History retention | At least 60 days on paid plans / on Free, workers and their runs are deleted 7 days after the worker was registered |
input size |
100 KB |
| Report body | 100 KB (status_message cut off at 256 characters) |
| Delivery timeout · retries | 15 seconds · 6 attempts over about 5 hours |
| Signing secret rotation grace | 48 hours |
| Callback key regeneration | Old key stops working immediately |
| API call counting | One successful task tool call and one accepted report each count as 1 call. Deliveries and retries count as 0. Reports are accepted even over quota |
| Delivery guarantee | At-least-once (the same task can arrive twice) |
Privacy
- A worker's
resultanderrorreach the AI conversation unmasked, as is - 3Min API does not detect personal data inside free-form JSON, so don't put more personal data in results than needed
Troubleshooting
- Every task fails right away: Check Delivery → Last response in the run details. A 401 usually means the callback key was pasted in place of the signing secret, or the previous secret was removed right after a rotation
- A task stays
working: The worker returns 2xx but never reports. Check the status report API response codes in the worker's logs - Reports get 401: Make sure it's the callback key starting with
tm_task_, and not a key from before a regeneration - Reports get 409: The task already finished or was cancelled. No need to send more
- Reports get 413: The body is over 100 KB. Shrink
resultand resend - Sending the same request doesn't create a new task: The same
inputwithin 2 minutes returns the existing task. Add something like a date toinputto tell them apart