REST API Reference
The Receiver.email REST API is available at https://api.receiver.email/api/v1. All requests must be authenticated.
Authentication
API requests are authenticated using a Bearer token in the Authorization header. You can use either an API key (for SDK/automation) or a JWT access token (for browser sessions).
curl https://api.receiver.email/api/v1/projects \
-H "Authorization: Bearer recv_live_your_api_key"An API key belongs to one project. It can read the project, mint addresses, wait for, read and delete mail, and send a test email to its own project. Creating API keys, managing webhooks and deleting the project need a dashboard session (a JWT); those endpoints are marked JWT only.
/whoamiWho the credentials belong to. With an API key it returns the key's project, so a client can start from RECEIVER_API_KEY alone.
curl https://api.receiver.email/api/v1/whoami \
-H "Authorization: Bearer recv_live_..."Response (API key)
{
"authType": "apikey",
"project": {
"id": "proj_abc",
"name": "Signup tests",
"identifier": "a3kx9p2m",
"emailSuffix": "[email protected]"
},
"organizationId": "org_abc123",
"apiKey": { "id": "key_xyz", "name": "CI Pipeline" }
}Response (JWT)
{ "authType": "jwt", "user": { "id": "[email protected]", "email": "[email protected]" } }/auth/otp/sendSend an OTP code to an email address for login.
curl -X POST https://api.receiver.email/api/v1/auth/otp/send \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'Response
{
"message": "OTP sent to [email protected]"
}/auth/otp/verifyVerify the OTP code and receive JWT tokens.
curl -X POST https://api.receiver.email/api/v1/auth/otp/verify \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "code": "123456"}'Response
{
"accessToken": "eyJhbGci...",
"refreshToken": "eyJhbGci...",
"user": { "id": "[email protected]", "email": "[email protected]" },
"organization": { "id": "org_abc123", "name": "user's organization", "role": "admin", "subscriptionTier": "free" },
"organizations": [ ... ]
}/auth/refreshExchange a refresh token for a new access token and a new refresh token. Each refresh token works once; reusing an old one signs the session out.
/auth/meGet the logged-in user and their organizations, with limits and usage. Requires a dashboard access token.
/auth/logoutSign out: revokes the session for the refresh token in the body (or the access token in the header). Always returns 204.
/auth/sessionsList the devices signed in to your account.
/auth/sessions/revoke-othersSign out every session except the current one.
Inboxes
An inbox is any address under a project: {prefix}{emailSuffix}. Nothing is stored until mail arrives, so there is nothing to create or clean up. Minting addresses through the API guarantees they are unique and well formed.
/projects/:projectId/inboxesMint a new address. Stores nothing. The returned createdAt is the natural since for waiting on it.
Body (optional)
| Parameter | Type | Description |
|---|---|---|
prefix | string | Start of the address. A random 6-character suffix is added (signup becomes signup-k2m9qa). Allowed: a-z, 0-9 and . _ + -, 1 to 40 characters, lowercased. Omitted: inbox- plus 8 random characters. |
curl -X POST https://api.receiver.email/api/v1/projects/proj_abc/inboxes \
-H "Authorization: Bearer recv_live_..." \
-H "Content-Type: application/json" \
-d '{"prefix": "signup"}'Response (201)
{
"address": "[email protected]",
"prefix": "signup-k2m9qa",
"projectId": "proj_abc",
"createdAt": "2026-11-05T10:00:00.000Z"
}Emails
/projects/:projectId/emailsList emails for a project with optional filters.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
recipient | string | Filter by recipient email address |
sender | string | Filter by sender email address |
receivedAfter | ISO 8601 | Only emails received after this timestamp |
receivedBefore | ISO 8601 | Only emails received before this timestamp |
limit | number | Max results (default: 20, max: 100) |
cursor | string | Pagination cursor from previous response |
curl "https://api.receiver.email/api/v1/projects/proj_abc/[email protected]&limit=10" \
-H "Authorization: Bearer recv_live_..."Response
{
"emails": [
{
"id": "email_xyz789",
"projectId": "proj_abc",
"sendTo": "[email protected]",
"sender": "[email protected]",
"subject": "Reset your password",
"textBodyPreview": "Click the link below to reset...",
"hasAttachments": false,
"attachmentCount": 0,
"size": 2048,
"receivedAt": "2026-01-15T10:30:00Z",
"links": ["https://example.com/reset?token=abc123"]
}
],
"hasMore": false
}The since parameter
wait, otp and stream take since, an ISO 8601 timestamp. They match only mail whose receivedAt is strictly after it. Wait from an inbox's createdAt for its first email, and from the last email's receivedAt for the next one. When omitted, wait and otp look back 60 seconds.
/projects/:projectId/emails/waitLong-poll for an email. The server holds the request until a matching email arrives or the timeout expires. Both outcomes return 200; a timeout is not an error.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
recipient | string | Only mail to this address |
sender | string | Only mail from this address |
subject | string | Subject contains this text (case-insensitive) |
since | ISO 8601 | Only mail received strictly after this time (default: now minus 60 seconds). receivedAfter is accepted as an alias. |
timeout | number | Seconds to wait (default 30, max 60) |
curl "https://api.receiver.email/api/v1/projects/proj_abc/emails/[email protected]&since=2026-11-05T10:00:00.000Z&timeout=60" \
-H "Authorization: Bearer recv_live_..."Response: received
{
"status": "received",
"email": {
"id": "email_xyz789",
"sendTo": "[email protected]",
"sender": "[email protected]",
"subject": "Your Acme code",
"textBody": "Your verification code is 847291...",
"links": ["https://acme.example/verify?token=abc123"],
"receivedAt": "2026-11-05T10:00:04.120Z"
}
}Response: timeout
{ "status": "timeout", "since": "2026-11-05T10:00:00.000Z", "waitedSeconds": 60, "retry": true }On a timeout, call again with the same since. The SDKs loop for you, so their timeout can be any length.
/projects/:projectId/emails/otpExtract a verification code (4 to 8 digits, with or without separators, or alphanumeric) from matching emails, from the subject or the body. Scans up to 10 matching emails, newest first. Waits only if you pass a timeout.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
recipient, sender, subject, since | The same filters as wait. since defaults to now minus 60 seconds, so an old code never matches. | |
codeLength | number | Only codes of this length: 4, 6 or 8 |
timeout | number | Seconds to wait for a code (default 0: answer now; max 60) |
curl "https://api.receiver.email/api/v1/projects/proj_abc/emails/[email protected]&since=2026-11-05T10:00:00.000Z&timeout=60" \
-H "Authorization: Bearer recv_live_..."Response: found
{
"status": "found",
"code": "847291",
"codeLength": 6,
"source": "subject",
"email": {
"id": "email_xyz789",
"projectId": "proj_abc",
"sendTo": "[email protected]",
"sender": "[email protected]",
"subject": "Your Acme code is 847291",
"receivedAt": "2026-11-05T10:00:04.120Z"
},
"candidates": [
{ "code": "847291", "codeLength": 6, "source": "subject", "email": { "id": "email_xyz789", ... } }
]
}code is the newest code. candidates lists every code found in the matching emails, newest first. When nothing is found the response is the same { "status": "timeout", ... } as wait.
/projects/:projectId/emails/streamServer-Sent Events: every new email as it arrives, oldest first. The server ends the stream after 55 seconds; reconnect with the since it sends.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
recipient | string | Only mail to this address |
since | ISO 8601 | Only mail received strictly after this time (default: now) |
curl -N "https://api.receiver.email/api/v1/projects/proj_abc/emails/[email protected]" \
-H "Authorization: Bearer recv_live_..."Events
event: email
id: email_xyz789
data: {"email": {"id": "email_xyz789", "sender": "[email protected]", "subject": "Your Acme code", "receivedAt": "2026-11-05T10:00:04.120Z", "links": [...], "textBodyPreview": "Your verification code is 847291..."}, "otp": {"code": "847291", "codeLength": 6, "source": "body"}}
: ping
event: end
data: {"since": "2026-11-05T10:00:04.120Z"}A : ping comment arrives every 15 seconds. otp is null when the email has no code. The SDKs' stream() reconnects for you.
/projects/:projectId/emails/:emailIdGet a single email by ID, including full text and HTML bodies, headers, links and attachments.
/projects/:projectId/emails/:emailId/textGet the plain text body of an email.
/projects/:projectId/emails/:emailId/htmlGet the HTML body of an email.
/projects/:projectId/emails/:emailId/rawGet the raw RFC822 email content.
/projects/:projectId/emails/:emailId/attachments/:indexDownload an attachment by its position in the email's attachments list (0-based).
/projects/:projectId/emails/:emailIdDelete an email permanently, including its stored raw message and attachments.
/projects/:projectId/test-emailSend a test email to one of the project's own addresses, to check the whole path end to end. The subject and body carry a random 6-digit code. Limited to 10 per project per hour (429 after that).
curl -X POST https://api.receiver.email/api/v1/projects/proj_abc/test-email \
-H "Authorization: Bearer recv_live_..." \
-H "Content-Type: application/json" \
-d '{"to": "[email protected]"}'Response (202)
{ "to": "[email protected]", "sentAt": "2026-11-05T10:00:00.000Z" }to must end with the project's emailSuffix, or the API returns 400.
Webhooks
A webhook posts every new email to your endpoint as soon as it is stored. A project can have up to 5. Managing them is JWT only: use the dashboard, or a dashboard session token.
/projects/:projectId/webhooksCreate a webhook. The url must be https (http://localhost is allowed outside production). The signing secret is returned once, in this response.
curl -X POST https://api.receiver.email/api/v1/projects/proj_abc/webhooks \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/receiver", "events": ["email.received"]}'Response (201)
{
"id": "wh_abc",
"url": "https://example.com/hooks/receiver",
"events": ["email.received"],
"secret": "whsec_...",
"createdAt": "2026-11-05T10:00:00.000Z"
}/projects/:projectId/webhooksList webhooks with their last delivery time and status. Secrets are never returned after creation.
/projects/:projectId/webhooks/:webhookIdDelete a webhook. Returns 204.
/projects/:projectId/webhooks/:webhookId/testSend a sample email.received event to the webhook and return the delivery.
/projects/:projectId/webhooks/:webhookId/deliveriesThe last 50 deliveries: event id, succeeded or failed, status code, attempts and error.
The delivery
Each email is sent as a POST with a JSON body and these headers:
Receiver-Event-Id:evt_<emailId>, the same on every retry. Use it to ignore duplicates.Receiver-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256>User-Agent:receiver-webhooks/1
{
"id": "evt_email_xyz789",
"type": "email.received",
"createdAt": "2026-11-05T10:00:04.300Z",
"data": {
"email": {
"id": "email_xyz789",
"projectId": "proj_abc",
"sendTo": "[email protected]",
"sender": "[email protected]",
"subject": "Your Acme code",
"receivedAt": "2026-11-05T10:00:04.120Z",
"links": ["https://acme.example/verify?token=abc123"],
"textBodyPreview": "Your verification code is 847291..."
},
"otp": { "code": "847291", "codeLength": 6, "source": "body" }
}
}Respond with any 2xx status. Anything else, or no answer within 10 seconds, is retried: 3 attempts in all, immediately, after 2 seconds and after 10 seconds.
Verifying the signature
The signature is an HMAC-SHA256 of <t>.<raw body> with your webhook secret. Compute it over the raw request body (before JSON parsing), compare in constant time, and reject old timestamps to stop replays.
import crypto from "node:crypto";
export function verifyReceiverSignature(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5 minute tolerance
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
const wanted = Buffer.from(expected, "hex");
return given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}
// Express: keep the raw body for this route
app.post("/hooks/receiver", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyReceiverSignature(raw, req.get("Receiver-Signature") ?? "", process.env.RECEIVER_WEBHOOK_SECRET!)) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
console.log(event.data.otp?.code);
res.sendStatus(204);
});import hashlib, hmac, time
def verify_receiver_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > 300: # 5 minute tolerance
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Projects
/projectsList all projects accessible to the authenticated user.
curl https://api.receiver.email/api/v1/projects \
-H "Authorization: Bearer recv_live_..."/projectsCreate a new project.
curl -X POST https://api.receiver.email/api/v1/projects \
-H "Authorization: Bearer recv_live_..." \
-H "Content-Type: application/json" \
-d '{"organizationId": "org_abc123", "name": "CI Tests"}'Response
{
"id": "proj_new123",
"organizationId": "org_abc123",
"name": "CI Tests",
"identifier": "b7kx2m9n",
"emailSuffix": "[email protected]",
"createdAt": "2026-01-15T10:00:00Z"
}/projects/:idGet project details.
/projects/:idUpdate a project (name).
/projects/:idDelete a project with all its emails, API keys and stored files. Requires a dashboard access token.
Organizations
/organizationsList all organizations the user belongs to.
/organizationsCreate a new organization.
/organizations/:idGet organization details including usage and subscription info.
/organizations/:idRename an organization or change its slug. Admins only.
/organizations/:id/membersList members and their roles.
/organizations/:id/members/:userIdChange a member's role (admin, member or viewer). Admins only. An organization always keeps at least one admin.
/organizations/:id/members/:userIdRemove a member (admins), or leave the organization by passing your own user ID.
/organizations/:id/invitationsInvite someone by email with a role. They log in with that address and accept. Admins only.
curl -X POST https://api.receiver.email/api/v1/organizations/org_abc/invitations \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "role": "member"}'/organizations/:id/invitationsList pending invitations. Admins only.
/organizations/:id/invitations/:invitationIdRevoke a pending invitation. Admins only.
/invitationsList invitations addressed to your email.
/invitations/:invitationId/acceptJoin the organization with the invited role. Use /decline to refuse.
/organizations/:id/audit-logWho changed what in the organization: projects, API keys, members, invitations and settings. Newest first, paged with cursor. Admins only.
API Keys
/projects/:projectId/api-keysList API keys for a project. Key values are masked.
/projects/:projectId/api-keysCreate a new API key. The full key is only returned in this response.
curl -X POST https://api.receiver.email/api/v1/projects/proj_abc/api-keys \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"name": "CI Pipeline"}'/projects/:projectId/api-keys/:keyIdGet one API key's metadata, including when it was last used or revoked.
/projects/:projectId/api-keys/:keyIdRevoke an API key. It stops working immediately.
Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | INVALID_REQUEST | Missing or malformed parameters or body |
| 401 | UNAUTHORIZED | Missing, invalid or expired token or API key |
| 401 | INVALID_OTP | Wrong or expired login code, or too many attempts |
| 403 | FORBIDDEN | Not a member of the organization, or insufficient role |
| 404 | NOT_FOUND | Resource does not exist or is not visible to you |
| 409 | CONFLICT | Resource already exists (for example an organization slug) |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. See the X-RateLimit-Reset header. |
| 500 | INTERNAL_ERROR | Unexpected server error |
Error response format:
{
"error": {
"code": "NOT_FOUND",
"message": "Email not found"
}
}A wait that ends without mail is not an error: wait and otp return 200 with "status": "timeout".
Rate Limits
Requests with an API key count against your organization's limit, which depends on your subscription tier:
- Free: 10 requests/minute
- Starter: 100 requests/minute
- Pro: 1,000 requests/minute
- Enterprise: 10,000 requests/minute
Dashboard sessions are limited to 300 requests per minute per user, and login codes to 5 per hour per email address.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. When rate limited, the API returns a 429 status with a Retry-After header indicating when you can retry.
Pagination
List endpoints return hasMore and, when there is another page, nextCursor. Pass it as the cursor query parameter to get the next page. The SDKs do this for you with emails.listAll() and emails.list_all().