API reference
Everything here takes and returns JSON, except the agent link's instructions, which are markdown. The examples assume:
export M=https://YOUR-MIRTEL-HOST
export ADMIN=mrt_admin_...
export AGENT=mrt_agent_...Keys
| Key | Prefix | Can |
|---|---|---|
| Admin | mrt_admin_ | Everything under "Owner endpoints" |
| Agent | mrt_agent_ | Read and post to its own link, POST /api/report and GET /api/me, for itself only |
| Registration token | mrt_reg_ | One POST /api/register |
Keys are random and shown once. Mirtel stores only a hash of each one.
---
The agent link
Every agent key is also a link: $M/r/ followed by the key. The link is the credential, so no Authorization header is needed. Anyone holding it can report as that agent.
GET /r/<key>
Returns instructions written for that agent: its contract (goal, schedule in plain words, grace period, success description, minimum items), how to report, an example curl with its own link, and what each response means. The contract is read fresh on every request, so edits show up next time the agent looks.
- Always
text/markdown; charset=utf-8, whatever theAcceptheader says. - Sent with
Cache-Control: no-store,Referrer-Policy: no-referrerandX-Robots-Tag: noindex. - A pending (self-registered, not yet approved) agent gets the same instructions with a note at the top that its reports are refused until the owner approves it.
- A revoked, rotated-away or unknown key gets
401with a short plain-text message, which doesn't say which of those it was.
curl $M/r/$AGENTPOST /r/<key>
Files a report. Same body, rules and response as POST /api/report below, without the header.
curl -X POST $M/r/$AGENT -H 'Content-Type: application/json' \
-d '{"status":"done","items":5,"output":"5 items, digest.md"}'Returns 201 with the report. Errors are JSON: 401 if the key is revoked, rotated or unknown, 403 if the agent is still waiting for approval, 400 for a bad body, 413 for a body over 100 KB.
Where links come from
POST /api/agents, POST /api/register and POST /api/agents/:id/rotate-key return a link next to the key. It's shown once, like the key. Rotating issues a new key, so a new link, and the old one stops working. Revoking stops it too.
Links are absolute. If MIRTEL_PUBLIC_URL is set, it is the base. Otherwise Mirtel builds the base from the request's Host header and X-Forwarded-Proto (its first value if the proxy sends a list, and http unless that is https). A Host that isn't a plain host name or address is ignored, and links fall back to localhost. In production, set MIRTEL_PUBLIC_URL (for example https://mirtel.example.com) so links always point at the right host. Behind a proxy that doesn't send X-Forwarded-Proto, the guessed links would start with http://.
Checking a handover: first_report and key_issued_at
Each agent has key_issued_at, set whenever a key is issued (create, register, rotate). Agents created before this field existed fall back to created_at.
GET /api/agents includes first_report on each agent: the earliest report received since the current key was issued, as { "received_at": "…", "status": "done" }, or null if none has come in. It uses the time Mirtel received the report and the status the agent sent, not the ledger verdict. Rotating resets it to null, so it confirms the new handover.
---
Agent endpoints with a header
These are the same thing as the link, for agents that would rather send the key in a header: Authorization: Bearer mrt_agent_....
POST /api/report
Report one run. Send it when the run finishes, whatever the outcome.
| Field | Type | Notes |
|---|---|---|
status | text | Required. One of done, failed, empty, needs_you |
items | whole number, 0 or more | How many things it produced. Checked against success.min_items |
output | text, up to 2000 characters | A short summary or a link to the result |
error | text, up to 2000 characters | What went wrong, for failed |
ran_at | ISO 8601 time | When the run started. Defaults to now. Anything in the future or more than 24 h back is replaced with now |
Longer output and error text is cut to 2000 characters.
curl -X POST $M/api/report -H "Authorization: Bearer $AGENT" -H 'Content-Type: application/json' \
-d '{"status":"done","items":5,"output":"5 items, digest.md"}'Returns 201 { "report": { "id": "rp_…", "status": "done", "items": 5, "ran_at": "…", "received_at": "…", … } }.
GET /api/me
The agent's own contract and status. Useful for checking that a key works: 200 means good, 403 means waiting for approval, 401 means wrong or revoked.
curl $M/api/me -H "Authorization: Bearer $AGENT"The MCP connector
For agents whose app can use tools through an MCP connector but can't send web requests. It's the same key in another shape: rotate or revoke the agent and the connector stops working too.
| Address | Key |
|---|---|
POST /mcp/<key> | In the address, for apps that only take a URL |
POST /mcp | Authorization: Bearer mrt_agent_... |
It speaks MCP over Streamable HTTP, with JSON replies only: no event streams and no sessions. Both protocol eras work. Older clients open with initialize (versions 2025-06-18 and 2025-11-25). Newer ones send 2026-07-28 metadata on every request, and the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers must match it. GET and DELETE answer 405. A request from a browser Origin other than Mirtel's own answers 403.
Two tools:
| Tool | What it does |
|---|---|
report_run | Files a report. Takes the same fields as POST /api/report (status, items, output, error, ran_at) under the same rules |
get_contract | Returns the agent's instructions, the same as its link but written for tool calls |
Tool and server descriptions include the agent's name, so an owner can add several connectors to one app and the model can tell them apart. A bad field or a pending agent comes back as a tool result with isError: true and a sentence saying what to fix, so the model can try again. Nothing is stored in that case.
curl -X POST $M/mcp/$AGENT -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"report_run","arguments":{"status":"done","items":5}}}'Agents can't register themselves through MCP yet. Use POST /api/register for that.
POST /api/register
Self-registration with a one-time token from the owner (Authorization: Bearer mrt_reg_...). Takes the same body as POST /api/agents. The token is used up on the first try that gets a valid contract. The key and link it returns are refused with 403 until the owner approves the agent.
curl -X POST $M/api/register -H "Authorization: Bearer mrt_reg_..." -H 'Content-Type: application/json' \
-d '{"name":"Paper alerts","platform":"Claude","schedule":{"tz":"Europe/London","days":"weekdays","times":["10:15"]},"success":{"description":"New papers with DOIs","min_items":1}}'Returns 201 { "agent": { "status": "pending", … }, "key": "mrt_agent_…", "link": "…/r/mrt_agent_…", "note": "…" }.
---
Owner endpoints
All of these need the admin key: Authorization: Bearer $ADMIN.
The contract body
Used by POST /api/agents, PATCH /api/agents/:id and POST /api/register.
{
"name": "Competitor digest",
"platform": "ChatGPT",
"goal": "Summarise competitor news each morning",
"schedule": { "tz": "Europe/London", "days": "weekdays", "times": ["08:00"] },
"success": { "description": "3+ new items, with links", "min_items": 3 },
"grace_minutes": 30
}nameis required. Everything else is optional.schedule.daystakes"daily","weekdays"or"weekends", or a list such as["mon","wed","fri"].schedule.timesis a list of 24-hourHH:MMtimes.schedule.tzis an IANA zone and defaults toUTC.- Set
scheduletonullfor an ad-hoc agent. Its reports appear as off-schedule runs, and it can never be Missed. grace_minutes(0 to 1440, default 30) is how long after each scheduled time before a run with no report counts as Missed.
POST /api/agents
Create an active agent. Returns its key and link, shown once.
curl -X POST $M/api/agents -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
-d '{"name":"Competitor digest","platform":"ChatGPT","schedule":{"tz":"Europe/London","days":"weekdays","times":["08:00"]},"success":{"min_items":3}}'Returns 201 { "agent": {…}, "key": "mrt_agent_…", "link": "https://YOUR-MIRTEL-HOST/r/mrt_agent_…" }.
GET /api/agents
curl $M/api/agents -H "Authorization: Bearer $ADMIN"Returns { "link_base": "https://YOUR-MIRTEL-HOST", "agents": [ … ] }. link_base is where agent links start (MIRTEL_PUBLIC_URL if set), so link_base + "/r/" + key is an agent's link. Each agent has id, workspace_id, name, platform, goal, schedule, success, grace_minutes, status (active, pending or revoked), created_at, active_since, revoked_at, key_issued_at and first_report. key_issued_at may be missing on agents created before it existed.
PATCH /api/agents/:id
Change any contract fields. The ledger re-judges history with the new contract, and the agent's link shows the new contract next time it's read.
curl -X PATCH $M/api/agents/ag_123 -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
-d '{"grace_minutes":60}'POST /api/agents/:id/approve
Activate a pending, self-registered agent. Expected runs start from the moment of approval. Returns 409 if the agent isn't pending.
POST /api/agents/:id/revoke
Disable the agent's key and link immediately. History is kept, no new runs are expected, and other agents are unaffected.
POST /api/agents/:id/rotate-key
Issue a new key and link, and stop the old ones working. Resets first_report. Returns { "agent": {…}, "key": "mrt_agent_…", "link": "…" }, or 409 if the agent is revoked.
DELETE /api/agents/:id
Delete the agent and all its reports.
POST /api/registration-tokens
curl -X POST $M/api/registration-tokens -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' -d '{"ttl_minutes":15}'Returns 201 { "token": "mrt_reg_…", "expires_at": "…" }. The lifetime is 1 to 1440 minutes, default 15.
GET /api/ledger?days=7
Every expected run over the last days (1 to 90), each agent's next upcoming run, and any off-schedule runs. Newest first.
{
"now": "2026-11-04T09:00:00.000Z",
"days": 7,
"entries": [
{
"agent_id": "ag_…", "agent_name": "Competitor digest", "platform": "ChatGPT",
"slot": "2026-11-04T08:00:00.000Z",
"state": "missed",
"late": false,
"report": null
}
]
}state is one of done, missed, failed, empty, needs_you, due. Off-schedule entries have "slot": null and "off_schedule": true. How it works explains how reports are matched to runs.
GET /api/attention
Unacknowledged Missed, Failed, Empty and Needs-you entries from the last 7 days, plus agents waiting for approval.
Returns { "items": [ { …ledger entry, "key": "ag_…@2026-11-04T08:00:00.000Z" } ], "pending_agents": [ … ] }.
POST /api/attention/ack
Mark an attention item as seen. It stays in the ledger.
curl -X POST $M/api/attention/ack -H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
-d '{"key":"ag_123@2026-11-04T08:00:00.000Z"}'---
Public
POST /api/waitlist
{ "email": "you@work.com" } returns { "ok": true }.
Docs
/docs and each page under it are HTML. Add .md to any page path (for example /docs/api.md, or /docs/index.md for the start page) to get its markdown. /llms.txt lists every page with a one-line description and its .md address.
---
Errors
Errors from /api/* and from POST /r/<key> are JSON, { "error": "message" }, with one of these statuses:
| Status | Means |
|---|---|
400 | Bad input. The message says what |
401 | Missing or invalid key, or a revoked or rotated link |
403 | The agent is waiting for owner approval |
404 | Not found |
409 | Wrong state, such as approving an agent that isn't pending |
413 | Body over 100 KB |
The MCP connector answers in JSON-RPC instead, except for a bad key (401), a foreign Origin (403), 405 and 413, which use the usual { "error": "..." }.
Two answers are plain text instead: GET /r/<key> with a bad key answers 401 in a sentence, so an agent reading it as a page isn't handed JSON, and a docs page that doesn't exist answers 404 Not found.