---
title: "REST API"
description: "A documented HTTP API with issuable, scopable, revocable keys."
url: "https://openemail.uk/docs/knowledge/agents/api"
area: "Knowledge base"
category: "Agents, API & MCP"
status: "live"
---

# REST API

A documented HTTP API with issuable, scopable, revocable keys.

## Details

- On, everywhere. The API serves 104 documented operations across 68 paths (emails, threads, drafts, labels, contacts, audiences, domains, templates, rules, roles, members, settings, calendar, tracking, webhooks and the account) behind a committed OpenAPI 3.1 document you can read without a key at GET /openapi.json. Access is decided by the workspace key you issue in Settings.
- The durability this used to be waiting on is done. A send writes a row before anything is dispatched, with a public id of the form msg_ followed by 24 hex, and GET /emails/{id} resolves it, along with /events for the per-recipient trail and /tracking for opens and clicks. An Idempotency-Key of 1–255 characters is claimed against a unique index on the key and your API key together, so a retry after a timeout returns the first result with Idempotency-Replayed: true rather than sending twice. A keyed send answers 200 once it has settled and 202 while it is still queued or scheduled.
- Keys are minted, scoped, rotated and revoked in Settings → API keys. Every key the console issues is an oe_live_ one. The oe_test_ prefix is understood by the verifier and by the send path, where a test-mode send is recorded and answered as sent without ever reaching a transport, but nothing can mint one yet, and offering the option before the no-op transport sits above the Durable Object would hand you a test key that delivers for real. A key carries a send scope of up to 25 whole domains and 50 single addresses, where a whole domain also covers addresses added to it later, an optional expiry between 1 and 3650 days, and optionally a role. The role is a ceiling rather than a second grant: GET /ping returns both the scopes on the key and the scopes the role left it, so a 403 for a scope your key plainly names has a visible cause. Revoking is an update rather than a delete, so a later call is told revoked_api_key instead of simply failing to authenticate. Rotating keeps everything about the key except the secret: the id, the scopes, the send scope and the request history carry on, the old secret dies the instant the new one is minted, and a key holding keys:write can rotate itself over the API. The same list, rotate, revoke and enable actions are on the MCP server for anyone whose role may manage keys.
- What is genuinely absent: the API has no upload endpoint of its own. Inline attachments go as base64 under a 5 MB total cap, and a larger file is sent by naming a file already in the workspace by its id, which travels as a download link. Bounces are handled in the mailbox rather than in the send log: a delivery report is parsed, matched to the original by Message-ID, labelled on the thread and pushed as an email.bounced webhook, but nothing writes back to the send row, whose status has no bounced state, so through GET /emails a bounced message still reads as sent. Mail sent from the app composer does not appear in GET /emails either, because the composer does not write through the same send path.
