Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: fabricatedemail HTTP Messages API
version: 1.0.0
description: '## General contract
- Every error response has body `{"error": ""}`. The message is
human-facing and may change; branch on the status code, never on the
string.
- Checks run in the order authentication (`401`) → account state (`403`) →
rate limit (`429`) → request validation (`400`) → existence and
ownership (`404`) → cross-key conflict (`409`) → address cap (`429`).
A request that is wrong in two ways gets the earlier status.
- Unknown routes and methods are `404 {"error": "Not found"}`, including
after successful authentication.
- Every timestamp is exactly the format of JavaScript
`Date.prototype.toISOString()`: millisecond precision, trailing `Z`.
- Request bodies must be JSON objects. A malformed body, a JSON array or a
bare JSON string is `400`. Unknown fields are ignored.
- Register first, then trigger the mail. A `201` from `POST /subscriptions`
means the mailbox is committed and visible to the mail handler, and a
stored message is visible to your very next poll.'
servers:
- url: https://api.fabricatedemail.com
description: Production.
security:
- apiKey: []
tags:
- name: Messages
description: Reading stored mail, with a long poll that returns as soon as it lands.
paths:
/subscriptions/{sid}/messages:
parameters:
- $ref: '#/components/parameters/sid'
get:
operationId: listMessages
tags:
- Messages
summary: Read the subscription's messages, oldest first, with an optional long poll
description: 'At most 50 messages per response. A response holding exactly 50 may
have more behind it: advance `after` to the last `id` you received and
request again.
With `wait`, the service re-checks about once per second and returns as
soon as at least one message matches. On timeout it returns
`200 {"messages": []}` — an empty list is a normal result, not an
error.
The key and the subscription are validated once, at request start, so a
wait can outlive the subscription''s expiry, deletion or replacement:
the call then times out with an empty list, and the *next* request is
`404`.
Parameter validation happens before existence, so a bad `after` on a
subscription that does not exist is `400`, not `404`.'
x-mcp-tool:
name: wait_for_message
summary: Wait for mail to arrive in a mailbox and return what is there.
arguments:
- name: subscriptionId
required: true
description: Sent as the `sid` path parameter.
schema:
type: string
format: uuid
- name: after
required: false
description: Sent as the `after` query parameter.
schema:
type: integer
default: 0
- name: waitSeconds
required: false
description: Sent as the `wait` query parameter. At most 30.
schema:
type: integer
minimum: 0
maximum: 30
default: 30
parameters:
- name: after
in: query
required: false
description: 'Return only messages whose `id` is greater than this. This is the
integer `id` field of a message, never the RFC 5322 `messageId`.
Ids start at 1, so an omitted `after` returns everything currently
stored. A non-integer value — including `1.5`, `1e3`, `+1` and an
empty value — is `400`.
'
schema:
type: integer
default: 0
- name: wait
in: query
required: false
description: 'Long-poll seconds. `0` returns immediately. A value above 30 is
clamped to 30, not rejected. A negative or non-integer value is
`400`.
'
schema:
type: integer
minimum: 0
default: 0
responses:
'200':
description: The matching messages, oldest first, at most 50.
content:
application/json:
schema:
type: object
required:
- messages
properties:
messages:
type: array
maxItems: 50
items:
$ref: '#/components/schemas/Message'
examples:
oneMessage:
$ref: '#/components/examples/messageList'
timedOut:
summary: A long poll that found nothing.
value:
messages: []
'400':
description: '`after` is not an integer, or `wait` is negative or not an integer.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
badAfter:
value:
error: after must be an integer message id
badWait:
value:
error: wait must be a non-negative integer number of seconds
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/AccountSuspended'
'404':
$ref: '#/components/responses/SubscriptionNotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
/subscriptions/{sid}/messages/{id}:
parameters:
- $ref: '#/components/parameters/sid'
- $ref: '#/components/parameters/messageId'
get:
operationId: getMessage
tags:
- Messages
summary: Read one message
description: 'Returns one message object, not wrapped in a list.
The parent subscription''s liveness governs what is readable, not the
message''s own retention, so a message can disappear from a live mailbox
once its 24-hour retention has passed and the hourly cleanup has
collected it.'
x-mcp-tool:
name: get_message
summary: Read one stored message.
arguments:
- name: subscriptionId
required: true
description: Sent as the `sid` path parameter.
schema:
type: string
format: uuid
- name: messageId
required: true
description: Sent as the `id` path parameter — the integer `id` of the message.
schema:
type: integer
minimum: 1
responses:
'200':
description: The message.
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
examples:
message:
$ref: '#/components/examples/message'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/AccountSuspended'
'404':
description: 'The message does not exist, belongs to a different subscription, or
the path segment is not an integer; or the parent subscription is
unknown, expired, deleted, replaced or owned by another key.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
notFound:
$ref: '#/components/examples/errorNotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
/subscriptions/{sid}/messages/{id}/extract:
parameters:
- $ref: '#/components/parameters/sid'
- $ref: '#/components/parameters/messageId'
get:
operationId: extractFromMessage
tags:
- Messages
summary: Pull the one-time codes and links out of a message
description: 'The same authentication and `404` rules as reading the message itself.
The rules are deliberately simple, and heuristic:
- `codes` are runs of 4 to 8 digits in the text body — or in the HTML
body stripped of tags, when there is no text body — that appear
within 40 characters of one of the words `code`, `OTP`, `PIN`,
`verification`, `verify` or `passcode`, matched case-insensitively.
In order of appearance, deduplicated.
- `links` are every `http://` or `https://` URL in the text and HTML
bodies. In order of appearance, deduplicated.
A message with neither answers empty arrays, not `404`. A headers-only
message has no bodies to read, so it always answers empty arrays.'
x-mcp-tool:
name: extract
summary: Extract one-time codes and links from a stored message.
arguments:
- name: subscriptionId
required: true
description: Sent as the `sid` path parameter.
schema:
type: string
format: uuid
- name: messageId
required: true
description: Sent as the `id` path parameter — the integer `id` of the message.
schema:
type: integer
minimum: 1
responses:
'200':
description: What the heuristics found. Either array may be empty.
content:
application/json:
schema:
$ref: '#/components/schemas/Extraction'
examples:
codeAndLink:
value:
codes:
- '482913'
links:
- https://example.com/verify?t=8f14e45fceea167a
nothingFound:
summary: A message with neither a code nor a link.
value:
codes: []
links: []
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/AccountSuspended'
'404':
description: Same as reading the message itself.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
notFound:
$ref: '#/components/examples/errorNotFound'
'429':
$ref: '#/components/responses/RateLimited'
'500':
$ref: '#/components/responses/ServerError'
components:
responses:
Unauthorized:
description: 'The key or token is missing, malformed, unknown or revoked, or the key
has no account. All of these are indistinguishable from outside.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
unauthorized:
$ref: '#/components/examples/errorUnauthorized'
ServerError:
description: A server-side failure, logged by the service with the real cause. Retry.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
internal:
$ref: '#/components/examples/errorInternal'
RateLimited:
description: 'The key passed 600 requests in a minute. `Retry-After` says when to
come back. A well-behaved 30-second poll loop costs about two requests
a minute and never sees this.
'
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
rateLimited:
$ref: '#/components/examples/errorRateLimited'
AccountSuspended:
description: 'The key''s account is suspended. Every key of the account sees this, on
every key route, from the next request on; a long poll already in
flight is not interrupted, and the request after it gets this within
30 seconds. Mail addressed to the account''s
addresses is dropped meanwhile. Reinstatement makes the account work
again; mail that arrived during the suspension is gone, and addresses
and mail that expired meanwhile were deleted as usual.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
suspended:
$ref: '#/components/examples/errorAccountSuspended'
SubscriptionNotFound:
description: 'The subscription is unknown, expired, deleted, replaced or owned by
another key. These are indistinguishable, and the handle is dead for
good: get a new one by registering again.
'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
notFound:
$ref: '#/components/examples/errorNotFound'
schemas:
Error:
type: object
required:
- error
properties:
error:
type: string
description: Human-facing, and free to change. Branch on the status code.
Extraction:
type: object
required:
- codes
- links
properties:
codes:
type: array
description: Digit runs that look like one-time codes, in order of appearance, deduplicated.
items:
type: string
links:
type: array
description: Every URL in the bodies, in order of appearance, deduplicated.
items:
type: string
format: uri
Message:
type: object
required:
- id
- messageId
- from
- to
- subject
- text
- html
- headers
- attachments
- truncated
- rawSize
- receivedAt
properties:
id:
type: integer
description: 'Service-assigned and monotonic, but not contiguous within a
mailbox: ids are assigned service-wide, so a mailbox may see 7, 12,
13. Never compute the next cursor — carry over the `id` of the last
message you handled. This is the `after` cursor and the
single-message path parameter.
'
messageId:
type:
- string
- 'null'
description: 'The RFC 5322 `Message-ID` header, `null` when the sender set none.
Display and debug data — never an API parameter.
'
from:
type:
- string
- 'null'
description: 'The parsed `From:` header address. On a headers-only row it is the
SMTP envelope sender instead; the two can legitimately differ. A
test asserting origin must check this, not mere presence.
'
to:
type: string
description: 'Always the SMTP envelope recipient, the address that actually
routed here, so a spoofed `To:` header cannot affect it. May differ
from the `to` entry in `headers`.
'
subject:
type:
- string
- 'null'
text:
type:
- string
- 'null'
description: 'Plain-text body, capped at 256 KiB. `null` on a headers-only row
and when the mail had no text part.
'
html:
type:
- string
- 'null'
description: HTML body, capped at 256 KiB. Same nullability rule as `text`.
headers:
type: array
description: 'Every header, as ordered `[name, value]` pairs with names
lowercased, in the platform''s canonical order. Repeated headers —
`Received`, most often — are joined into one value separated by
`", "`; `set-cookie` is the one exception and appears as separate
pairs. Look headers up by name; do not rely on position.
'
items:
type: array
minItems: 2
maxItems: 2
items:
type: string
attachments:
type: array
description: Metadata only — content and raw MIME are not retrievable.
items:
type: object
required:
- filename
- mimeType
- size
properties:
filename:
type: string
mimeType:
type: string
size:
type: integer
description: Size in bytes.
truncated:
type: boolean
description: '`true` if anything was cut, or if the mail was stored headers-only.
Gate every body assertion on this: a large mail otherwise fails the
assertion with a confusing `null`.
'
rawSize:
type: integer
description: 'Size in bytes of the raw message as received. Above 1 MiB the body
is never parsed and the message is stored headers-only.
'
receivedAt:
type: string
format: date-time
description: 'When the service received it. A stored message lives 24 hours from
here, capped by its subscription''s own expiry, and is removed by an
hourly cleanup — so it may outlive its retention, normally by up to
an hour, and longer if a cleanup run fails.
'
examples:
errorNotFound:
value:
error: Not found
errorRateLimited:
summary: 'The rate limit — sent with `Retry-After: 60`.'
value:
error: rate limited
message:
summary: A verification mail with both bodies.
value:
id: 42
messageId: <abc@mail.example.com>
from: noreply@example.com
to: myapp-4f2a9c81d3e6b7a0@fabricatedemail.com
subject: Verify your account
text: Your code is 123456
html: <p>Your code is <b>123456</b></p>
headers:
- - from
- Example <noreply@example.com>
- - subject
- Verify your account
attachments:
- filename: invoice.pdf
mimeType: application/pdf
size: 31337
truncated: false
rawSize: 4096
receivedAt: '2026-08-10T12:00:00.000Z'
errorAccountSuspended:
value:
error: account suspended
errorInternal:
value:
error: internal error
messageList:
summary: One message in a poll response.
value:
messages:
- id: 42
messageId: <abc@mail.example.com>
from: noreply@example.com
to: myapp-4f2a9c81d3e6b7a0@fabricatedemail.com
subject: Verify your account
text: Your code is 123456
html: <p>Your code is <b>123456</b></p>
headers:
- - from
- Example <noreply@example.com>
- - subject
- Verify your account
attachments: []
truncated: false
rawSize: 4096
receivedAt: '2026-08-10T12:00:00.000Z'
errorUnauthorized:
value:
error: unauthorized
headers:
RetryAfter:
description: 'Seconds to wait before retrying. Present on the rate-limit `429` and
absent on the address-cap `429`, which is what tells the two apart
without reading the message.
'
required: false
schema:
type: integer
minimum: 0
parameters:
sid:
name: sid
in: path
required: true
description: The subscription id from `POST /subscriptions`.
schema:
type: string
format: uuid
messageId:
name: id
in: path
required: true
description: 'The integer `id` field of the message — never the RFC 5322 `messageId`
header. A path segment that is not an integer is `404`.
'
schema:
type: integer
minimum: 1
securitySchemes:
apiKey:
type: http
scheme: bearer
description: 'The 64-hex-character API key, created in the customer dashboard and
shown once. The scheme is matched case-insensitively. A missing header,
a malformed credential, an unknown key and a revoked key are all `401`
and indistinguishable from outside.
'
x-reserved-local-parts:
- abuse
- postmaster
- security
- support
- admin
- noreply
- no-reply
- hostmaster
- webmaster
- info
- billing
- legal
- dmarc