HTTP API
Every MCP tool can also be called over HTTP: POST https://api.subagent.fr/v1/{operation}, with the same arguments in JSON and the same object in return. Under /v1, every route is a tool.
In short
| Property | Value |
|---|---|
| Address | https://api.subagent.fr/v1/{operation}, where {operation} is the name of a tool: send_email, list_threads… |
| Method | POST, for every operation, reads included |
| Body | A JSON object: the tool’s arguments. An empty body counts as {}. 1 MiB at most. |
| Authentication | Authorization: Bearer sak_… header. Only sign_up is called without a key. |
| Response | Status 200 and the tool’s structured object |
| Refusal | An HTTP status and {"error": {"code", "message", "hint"}} |
| Schemas | https://api.subagent.fr/openapi.json (OpenAPI 3.1), readable without a key |
A call
curl -X POST https://api.subagent.fr/v1/inbox_status \
-H "Authorization: Bearer $SUBAGENT_KEY" \
-H "Content-Type: application/json" \
-d '{}'The response is the object the tool returns over MCP in structuredContent. The readable text only exists over MCP.
Signing up and verifying over HTTP
curl -X POST https://api.subagent.fr/v1/sign_up \
-H "Content-Type: application/json" \
-d '{"username": "atelier-exemple", "human_email": "[email protected]"}'The response carries api_key: the key, shown once only. The following calls present it.
curl -X POST https://api.subagent.fr/v1/verify \
-H "Authorization: Bearer $SUBAGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"otp_code": "482913"}'Refusals
{
"error": {
"code": "space_unverified",
"message": "…",
"hint": "…"
}
}code: stable. It is what a program tests.message: a sentence in French.hint: the lead, when there is one. It says what to do.
| Status | Meaning |
|---|---|
| 400 | An argument is missing, unknown or malformed: invalid_argument. |
| 401 | The key is missing, unknown or revoked: unauthorized. |
| 402 | The space has no active plan: payment_required. |
| 403 | The key does not carry the permission (forbidden), the space is not verified (space_unverified), or the call comes from a web page (origin_refused). |
| 404 | The object or the operation does not exist: not_found, unknown_operation. |
| 409 | The state does not allow the action: duplicate_recipient, not_draft, inbox_suspended… |
| 413 | The body exceeds 1 MiB: payload_too_large. |
| 422 | The request is understood but refused: recipient_suppressed, own_domain_required, unresolved_placeholder… |
| 429 | A limit is reached: rate_limited, key_daily_cap, queue_full… |
| 500, 502, 503 | The service, or a service it depends on, did not answer: internal, provisioning_failed, dns_unavailable, billing_unavailable… |
Every code, with what to do: Errors.
A message held back is not a refusal. Daily quota reached, send outside working hours, month’s volume reached: the response is a success (200), and the message is queued (queued). estimated_send_at gives its earliest departure, based on the sending window and the daily quota.
Going through a list
Lists return items and next_cursor.
limitgoes from 1 to 100. 20 by default.- As long as
next_cursoris notnull, pass it back as it is incursorfor the next page. - A cursor is never made up: an unreadable cursor is refused with the code
invalid_argument.
curl -X POST https://api.subagent.fr/v1/list_threads \
-H "Authorization: Bearer $SUBAGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"unanswered": true, "limit": 50}'get_thread follows the same rule for a thread’s messages: 50 per call, then next_cursor. list_domains returns all the space’s domains at once.
Retrying without duplicates
send_email and reply_to_thread accept an idempotency_key, 1 to 255 characters. Over HTTP, the Idempotency-Key header is equivalent to the parameter.
- Same key, same content: the server returns the message already created, with
replayed=true. - Same key, different content: refusal
idempotency_conflict. Nothing is sent.
After a 500 error or a network cut, retry with the same idempotency_key.
The shapes
- Identifiers:
inb_…(inbox),thr_…(thread),msg_…(message),dom_…(domain), followed by 32 hexadecimal characters. Pass them exactly as the tools return them. - Dates: ISO 8601 with an offset, in Paris time:
2026-10-13T09:00:00+02:00. As input, a Paris time without an offset is accepted:2026-10-13T09:00. - Arguments: an unknown argument is refused, with the list of accepted arguments in the lead.
Rate
60 requests per minute without a key, 600 with a key, per client address. Beyond that: status 429, code rate_limited, and the Retry-After header gives the number of seconds to wait. See Limits and quotas.
From a browser
The API cannot be called from another site’s web page: a request carrying a foreign Origin header is refused with the code origin_refused. Call it from your agent or your server.
The server’s reference
https://api.subagent.fr/openapi.json: every operation, its input and output schemas, its permissions (x-subagent-permissions).https://api.subagent.fr/llms.txt: the rules and the list of tools, as text.- The service’s pages that are not tools: an assistant’s sign-in (
/oauth/…, see Connector), the payment page and the invoices (see Pricing and payment), the unsubscribe page. - Tool reference: one page per tool, produced from this registry.