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).

bash
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.

GET/whoami

Who the credentials belong to. With an API key it returns the key's project, so a client can start from RECEIVER_API_KEY alone.

bash
curl https://api.receiver.email/api/v1/whoami \
  -H "Authorization: Bearer recv_live_..."

Response (API key)

json
{
  "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)

json
{ "authType": "jwt", "user": { "id": "[email protected]", "email": "[email protected]" } }
POST/auth/otp/send

Send an OTP code to an email address for login.

bash
curl -X POST https://api.receiver.email/api/v1/auth/otp/send \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]"}'

Response

json
{
  "message": "OTP sent to [email protected]"
}
POST/auth/otp/verify

Verify the OTP code and receive JWT tokens.

bash
curl -X POST https://api.receiver.email/api/v1/auth/otp/verify \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "code": "123456"}'

Response

json
{
  "accessToken": "eyJhbGci...",
  "refreshToken": "eyJhbGci...",
  "user": { "id": "[email protected]", "email": "[email protected]" },
  "organization": { "id": "org_abc123", "name": "user's organization", "role": "admin", "subscriptionTier": "free" },
  "organizations": [ ... ]
}
POST/auth/refresh

Exchange 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.

GET/auth/me

Get the logged-in user and their organizations, with limits and usage. Requires a dashboard access token.

POST/auth/logout

Sign out: revokes the session for the refresh token in the body (or the access token in the header). Always returns 204.

GET/auth/sessions

List the devices signed in to your account.

POST/auth/sessions/revoke-others

Sign 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.

POST/projects/:projectId/inboxes

Mint a new address. Stores nothing. The returned createdAt is the natural since for waiting on it.

Body (optional)

ParameterTypeDescription
prefixstringStart 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.
bash
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)

json
{
  "address": "[email protected]",
  "prefix": "signup-k2m9qa",
  "projectId": "proj_abc",
  "createdAt": "2026-11-05T10:00:00.000Z"
}

Emails

GET/projects/:projectId/emails

List emails for a project with optional filters.

Query Parameters

ParameterTypeDescription
recipientstringFilter by recipient email address
senderstringFilter by sender email address
receivedAfterISO 8601Only emails received after this timestamp
receivedBeforeISO 8601Only emails received before this timestamp
limitnumberMax results (default: 20, max: 100)
cursorstringPagination cursor from previous response
bash
curl "https://api.receiver.email/api/v1/projects/proj_abc/[email protected]&limit=10" \
  -H "Authorization: Bearer recv_live_..."

Response

json
{
  "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.

GET/projects/:projectId/emails/wait

Long-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

ParameterTypeDescription
recipientstringOnly mail to this address
senderstringOnly mail from this address
subjectstringSubject contains this text (case-insensitive)
sinceISO 8601Only mail received strictly after this time (default: now minus 60 seconds). receivedAfter is accepted as an alias.
timeoutnumberSeconds to wait (default 30, max 60)
bash
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

json
{
  "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

json
{ "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.

GET/projects/:projectId/emails/otp

Extract 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

ParameterTypeDescription
recipient, sender, subject, sinceThe same filters as wait. since defaults to now minus 60 seconds, so an old code never matches.
codeLengthnumberOnly codes of this length: 4, 6 or 8
timeoutnumberSeconds to wait for a code (default 0: answer now; max 60)
bash
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

json
{
  "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.

GET/projects/:projectId/emails/stream

Server-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

ParameterTypeDescription
recipientstringOnly mail to this address
sinceISO 8601Only mail received strictly after this time (default: now)
bash
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.

GET/projects/:projectId/emails/:emailId

Get a single email by ID, including full text and HTML bodies, headers, links and attachments.

GET/projects/:projectId/emails/:emailId/text

Get the plain text body of an email.

GET/projects/:projectId/emails/:emailId/html

Get the HTML body of an email.

GET/projects/:projectId/emails/:emailId/raw

Get the raw RFC822 email content.

GET/projects/:projectId/emails/:emailId/attachments/:index

Download an attachment by its position in the email's attachments list (0-based).

DELETE/projects/:projectId/emails/:emailId

Delete an email permanently, including its stored raw message and attachments.

POST/projects/:projectId/test-email

Send 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).

bash
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)

json
{ "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.

POST/projects/:projectId/webhooks

Create a webhook. The url must be https (http://localhost is allowed outside production). The signing secret is returned once, in this response.

bash
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)

json
{
  "id": "wh_abc",
  "url": "https://example.com/hooks/receiver",
  "events": ["email.received"],
  "secret": "whsec_...",
  "createdAt": "2026-11-05T10:00:00.000Z"
}
GET/projects/:projectId/webhooks

List webhooks with their last delivery time and status. Secrets are never returned after creation.

DELETE/projects/:projectId/webhooks/:webhookId

Delete a webhook. Returns 204.

POST/projects/:projectId/webhooks/:webhookId/test

Send a sample email.received event to the webhook and return the delivery.

GET/projects/:projectId/webhooks/:webhookId/deliveries

The 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
json
{
  "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.

TypeScript
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);
});
Python
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

GET/projects

List all projects accessible to the authenticated user.

bash
curl https://api.receiver.email/api/v1/projects \
  -H "Authorization: Bearer recv_live_..."
POST/projects

Create a new project.

bash
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

json
{
  "id": "proj_new123",
  "organizationId": "org_abc123",
  "name": "CI Tests",
  "identifier": "b7kx2m9n",
  "emailSuffix": "[email protected]",
  "createdAt": "2026-01-15T10:00:00Z"
}
GET/projects/:id

Get project details.

PATCH/projects/:id

Update a project (name).

DELETE/projects/:id

Delete a project with all its emails, API keys and stored files. Requires a dashboard access token.

Organizations

GET/organizations

List all organizations the user belongs to.

POST/organizations

Create a new organization.

GET/organizations/:id

Get organization details including usage and subscription info.

PATCH/organizations/:id

Rename an organization or change its slug. Admins only.

GET/organizations/:id/members

List members and their roles.

PATCH/organizations/:id/members/:userId

Change a member's role (admin, member or viewer). Admins only. An organization always keeps at least one admin.

DELETE/organizations/:id/members/:userId

Remove a member (admins), or leave the organization by passing your own user ID.

POST/organizations/:id/invitations

Invite someone by email with a role. They log in with that address and accept. Admins only.

bash
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"}'
GET/organizations/:id/invitations

List pending invitations. Admins only.

DELETE/organizations/:id/invitations/:invitationId

Revoke a pending invitation. Admins only.

GET/invitations

List invitations addressed to your email.

POST/invitations/:invitationId/accept

Join the organization with the invited role. Use /decline to refuse.

GET/organizations/:id/audit-log

Who changed what in the organization: projects, API keys, members, invitations and settings. Newest first, paged with cursor. Admins only.

API Keys

GET/projects/:projectId/api-keys

List API keys for a project. Key values are masked.

POST/projects/:projectId/api-keys

Create a new API key. The full key is only returned in this response.

bash
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"}'
GET/projects/:projectId/api-keys/:keyId

Get one API key's metadata, including when it was last used or revoked.

DELETE/projects/:projectId/api-keys/:keyId

Revoke an API key. It stops working immediately.

Error Codes

HTTP StatusError CodeDescription
400INVALID_REQUESTMissing or malformed parameters or body
401UNAUTHORIZEDMissing, invalid or expired token or API key
401INVALID_OTPWrong or expired login code, or too many attempts
403FORBIDDENNot a member of the organization, or insufficient role
404NOT_FOUNDResource does not exist or is not visible to you
409CONFLICTResource already exists (for example an organization slug)
429RATE_LIMIT_EXCEEDEDToo many requests. See the X-RateLimit-Reset header.
500INTERNAL_ERRORUnexpected server error

Error response format:

json
{
  "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().