- Endpoint admin (
/v1/api/email/endpoints,/v1/api/email/workspace-mailbox) — manage the addresses. Every operation is admin-scoped. - Triage (
/v1/api/email/...) — the read + action surface over inbound mail. Read/list operations requireconversations:read, reply/discard requireconversations:write, and the rest (assign, message/attachment content, quarantine) are admin-scoped.
https://api.hq.zone and a PAT bearer token:
Most of this feature is admin only. The scope is stated per endpoint below; where it says “admin”, a non-admin token is rejected. The triage list/badge endpoints (
list_review, list_activity, get_badge) are scoped so a regular member sees only mail bound to them and the admin-only queues read empty.1. Create an email endpoint (give an agent an address)
Create the endpoint
POST /v1/api/email/endpoints (admin) creates a custom inbox. The required fields are localpart and kind; an agent-handled inbox sets plane to agent and names a handler agent_id (the agent must belong to your workspace). A “light” quick-reply inbox is agentless — do not pass agent_id.EndpointJson) with its id, the stored config, and per-endpoint counters.Read back the full address
The endpoint object does not contain the full address by itself — it carries the The full address is
localpart. To assemble the address, call GET /v1/api/email/endpoints (List endpoints, admin), which returns tenant_email_slug, domain, and outbound_mailbox_localpart alongside the endpoints:<localpart>-<tenant_email_slug>@<domain>. So the inbox above resolves to sales.eu-<tenant_email_slug>@<domain>.Endpoint behavior fields
create_endpoint and update_endpoint share the same config knobs. The validated enum fields:
plane— one oflight,agent.light(the default) is the agentless quick-reply plane;agenthands the thread to the named agent.sender_policy— one ofbound_users_only,anyone,domain_allowlist,addr_allowlist.reply_policy— one ofagent_decides,always_reply,never_reply,human_review.
instructions, allow_escalation, default_reply_lang, deliver_to, category_policy, spam_score_max, max_turns_per_sender_day, max_turns_per_day, enabled, and allow_patterns (the allowlist; supplying it replaces the set wholesale).
Manage existing endpoints
| Operation | Method + path | Scope | Notes |
|---|---|---|---|
| List endpoints | GET /v1/api/email/endpoints | admin | Endpoints + counters + slug/domain. |
| Get endpoint | GET /v1/api/email/endpoints/{id} | admin | One endpoint; unknown id → 404. |
| Update endpoint | PATCH /v1/api/email/endpoints/{id} | admin | Omitted fields keep current values; localpart/kind are fixed. |
| Delete endpoint | DELETE /v1/api/email/endpoints/{id} | admin | The system workspace mailbox can’t be deleted (400). |
The workspace outbound mailbox
Every workspace has one HQ-managed outbound mailbox — the address all agents send from (is_system: true). It is never edited through the endpoint editor (that returns 400) and can’t be deleted. The only knob is its localpart, changed via Update mailbox:
The workspace mailbox never auto-replies, and that behavior is not configurable. An invalid or reserved localpart returns 400.
2. Mail arrives and the agent drafts
When mail lands on an endpoint, HQ mirrors it as anemail_message and routes it. The message moves through a small handling-state machine. The exact state strings are:
unrouted— couldn’t be routed to a handler (admin owns this queue).awaiting_review— the agent drafted a reply; a human must approve, edit & send, or discard it.handled— terminal: replied, discarded, or assigned.
reply_policy keeps a human in the loop, the drafted reply lands in awaiting_review for you to act on. The badge and review queues count exactly the items needing a human.
3. The review queue
List review (GET /v1/api/email/review, scope conversations:read) returns the three actionable queues plus counts:
ReviewQueues) has:
awaiting_review— rows of drafted replies (scoped to the caller; admins see all). Each is capped at 200 items.unrouted— admin-only; empty for a non-admin.quarantined— admin-only; empty for a non-admin.counts—{ awaiting_review, unrouted, quarantined }.
awaiting_review / unrouted row carries email_message_id, direction, handling_state, from_addr, to_addrs, subject, llm_category, llm_summary, agent_id, endpoint_id, endpoint_localpart, endpoint_plane, a conversation_id deep-link, and occurred_at.
4. Read a message
Pick anemail_message_id from the review queue and fetch the full message with Get message (GET /v1/api/email/messages/{id}/content, admin):
GET /v1/api/email/messages/{id}/attachments/{attachment_id}/download, admin). It streams the bytes with the upstream Content-Type and Cache-Control: no-store; an attachment that doesn’t belong to the message returns 404.
5. Approve & send — or discard
Send the reply
Send reply (The reply is sent as the handler agent on behalf of the bound user, and the item settles to
POST /v1/api/email/messages/{id}/reply, scope conversations:write) approves the drafted reply. The body (ReplyReq) is { "text": "...", "html": "..." } — text is required and must be non-empty; html is optional. The UI pre-fills text with the draft so a human can edit before sending.handling_state: "handled".Or discard the draft
Discard draft (
POST /v1/api/email/messages/{id}/discard, scope conversations:write) drops the draft without sending and settles the item to handled:6. Assign unrouted mail
Mail that couldn’t be routed sits in theunrouted queue (admin-only). Assign email (POST /v1/api/email/messages/{id}/assign, admin) attaches a handler and re-drives it:
AssignReq) is just { "agent_id": "..." }. The agent must belong to your workspace. The handler sets the message’s agent, re-emits the routed event so a turn runs, and — if the mail hit a handler-less mailbox endpoint — wires the chosen agent onto that endpoint so future mail routes without a human. The item settles to handled.
7. The quarantine path
Suspicious inbound mail (for example,unknown_sender or a spam-threshold hit) is held in quarantine instead of being routed. This is an admin-only surface; the pending items appear as the quarantined queue in list_review.
View the quarantined content
Get quarantine (Note the id here is the quarantine id, not an
GET /v1/api/email/quarantine/{id}/content, admin) fetches the raw message for inspection before you decide. Attachments aren’t materialized until accept, so the attachments list is empty here.email_message_id. An unknown id returns 404.Accept (optionally binding the sender)
Accept quarantine (When the destination is now reachable (a freshly bound
POST /v1/api/email/quarantine/{id}/accept, admin) marks the item accepted. The body (AcceptReq) optionally carries bind_user_id — binding the sender to a workspace user so their future mail routes. A bind_user_id is required to clear an unknown_sender quarantine.unknown_sender, or a spam_threshold item whose sender is already bound), HQ re-delivers this message so the agent acts on it immediately; otherwise it routes on the sender’s next mail. The response (ActionOk) returns ok: true and an optional explanatory note.Or reject
Reject quarantine (
POST /v1/api/email/quarantine/{id}/reject, admin) marks the item rejected — a terminal decision:Accept and reject act only on a pending quarantine. If no pending item with that id exists, both return 404. Accepting an
unknown_sender without a bind_user_id only records the decision — the response note reminds you to pass bind_user_id to route the sender’s future mail.Where it lands
Once an item ishandled, it drops out of the review queue and the badge. It stays visible in the Activity feed, where you can filter by state or endpoint_id to audit what the agent and your reviewers did.