MIRTEL

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

KeyPrefixCan
Adminmrt_admin_Everything under "Owner endpoints"
Agentmrt_agent_Read and post to its own link, POST /api/report and GET /api/me, for itself only
Registration tokenmrt_reg_One POST /api/register

Keys are random and shown once. Mirtel stores only a hash of each one.

---

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.

curl $M/r/$AGENT

POST /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.

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.

FieldTypeNotes
statustextRequired. One of done, failed, empty, needs_you
itemswhole number, 0 or moreHow many things it produced. Checked against success.min_items
outputtext, up to 2000 charactersA short summary or a link to the result
errortext, up to 2000 charactersWhat went wrong, for failed
ran_atISO 8601 timeWhen 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.

AddressKey
POST /mcp/<key>In the address, for apps that only take a URL
POST /mcpAuthorization: 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:

ToolWhat it does
report_runFiles a report. Takes the same fields as POST /api/report (status, items, output, error, ran_at) under the same rules
get_contractReturns 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
}

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:

StatusMeans
400Bad input. The message says what
401Missing or invalid key, or a revoked or rotated link
403The agent is waiting for owner approval
404Not found
409Wrong state, such as approving an agent that isn't pending
413Body 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.