API reference
InboxRhino is a receive-only test-email API. Create an address, wait for a real inbound message, inspect it, then delete the inbox. There is no send endpoint, no org-wide message search, and no SDK. Call the API from tests, CI, or a backend — browser JavaScript on an arbitrary origin is blocked by CORS.
Downloads
Both files are free. No account is required to download them. Put your API key only in Postman collection variables or a secret manager — never commit it.
Import the collection in Postman: File → Import, then select the downloaded JSON. Set the collection variable apiKey to your live key. OpenAPI is also at /openapi.yaml for agents that fetch specs.
Notes for AI agents
Prefer this page plus /openapi.yaml over guessing. Public operations have stable operationId values. Do not call /console/*, /setup, or invent send/search routes.
- Recommended sequence:
getUsage→createInbox→ tell the app under test to send mail todata.address→listOrWaitForMessageswithwait_seconds=180,limit=1,include=content→ assert →deleteInbox. - Empty non-waiting list: HTTP
200with{ "data": [], "next_cursor": null }. Wait timeout: HTTP204with an empty body. Do not parse JSON on 204. include=contentis valid only withlimit=1. Otherwise the API returns 422invalid_include.- Message filters are AND.
subjectis a case-insensitive substring.senderis an exact email.sender_domainis an exact domain. Usereceived_after(RFC 3339) so an older matching message is not reused. - Invalid query values return 422. They are never clamped. Out-of-range
limitorwait_secondsis an error. - API keys cannot create keys, invite users, or change billing. Those exist only in the signed-in console.
- Attachment bytes live at https://files.inboxrhino.in using the same Bearer key. Files are not malware-scanned. Treat them as untrusted.
Base URL and conventions
| Host | Use |
|---|---|
| https://api.inboxrhino.in | REST /v1 and /health |
| https://files.inboxrhino.in | Attachment downloads (same path and auth as GET /v1/attachments/{id}) |
| https://test.inboxrhino.in | Receiving domain only. Inbox addresses are local-part@test.inboxrhino.in. Not an HTTP API. |
- JSON request and response bodies use snake_case. Unknown JSON properties are rejected.
- Timestamps are RFC 3339 in UTC.
- Collection endpoints return
{ "data": [...], "next_cursor": string | null }. Single resources return{ "data": { ... } }. Usage is a bare object (nodatawrapper). - Default page size is 50, maximum 100. Pass the previous
next_cursorascursor. Cursors are opaque. - Every response includes
X-Request-Id. Errors also repeat that id in the JSON body. /v1is backward compatible. Breaking changes require a new versioned path.- Receive-only: you cannot send mail through this API. SMTP to a deleted or unknown address is rejected.
Authentication
Create keys in the web console. The secret is shown once. Format: ir_live_ + 16 lowercase hex characters + _ + 48 lowercase hex characters.
Authorization: Bearer ir_live_<16-hex-public-id>_<48-hex-secret>GET /health is the only public route that does not require a key. CORS allows the console host, *.chatgpt.site, and http://localhost / 127.0.0.1 with a port. Automated tests should send the Bearer header from the test runner, not from a third-party web page.
Typical test workflow
- Create an inbox. Store
data.idanddata.address. - Point the application under test at that address (signup, reset, invite, OTP, and similar).
- Wait with
wait_seconds=180and a subject or sender filter. Handle 200 (match) and 204 (timeout) separately. - Assert on
text,html, headers, or attachment metadata. - Delete the inbox so active-inbox quota is released. Deleting a message does not restore monthly email quota.
curl --get https://api.inboxrhino.in/v1/inboxes/INBOX_ID/messages \
--header "Authorization: Bearer $INBOXRHINO_API_KEY" \
--data-urlencode "wait_seconds=180" \
--data-urlencode "limit=1" \
--data-urlencode "include=content" \
--data-urlencode "subject=Verify" \
--data-urlencode "received_after=2026-09-03T10:00:00Z"Quotas and rate limits
| Limit | Value |
|---|---|
| Active inboxes | 11 |
| Inbound emails | 33 per UTC calendar month |
| Message retention | 30 days |
| HTTP rate limit | 120 requests / minute / API key; 600 / minute / organisation |
| Concurrent waits | 30 long-polls / organisation; 10 / API key |
| Wait duration | 0–180 seconds |
Email quota is reserved when the message is accepted. Failed parse or persistence releases that reservation. Over-quota inbound SMTP is rejected with Monthly recipient quota exceeded and is not stored. Mail to an unknown or deleted address is rejected with Recipient address rejected.
Health
/health
operationId: getHealth
No authentication. Confirms the Worker is up.
GET https://api.inboxrhino.in/health
{"status":"ok","service":"inboxrhino-api"}Inboxes
/v1/inboxes
operationId: createInbox
Creates a receive-only address on test.inboxrhino.in and the matching Cloudflare Email Routing rule. Body may be omitted or empty. The only allowed JSON field is prefix.
| Field | Where | Rules |
|---|---|---|
| Authorization | header | Bearer API key. Required. |
| Idempotency-Key | header | Optional. 8–200 characters. Retained 24 hours. Same key + same normalized body returns the cached 201. Same key + different body returns 409 idempotency_key_reused. In-flight reuse returns 409 idempotency_in_progress with Retry-After: 1. |
| prefix | JSON body | Optional. 3–40 characters after lowercase normalization. Pattern [a-z0-9], hyphens allowed in the middle, must start and end with alphanumeric. Reserved names are rejected: admin, administrator, abuse, postmaster, support, security, billing, root, hostmaster, webmaster, noreply, no-reply, donotreply, inboxrhino, contact. |
Success is 201. Omit prefix to receive a generated local-part such as bright-otter-ab12.
curl --request POST https://api.inboxrhino.in/v1/inboxes \
--header "Authorization: Bearer $INBOXRHINO_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: signup-test-1" \
--data '{"prefix":"signup-test"}'{
"data": {
"id": "inbox_0123456789abcdef0123456789abcdef",
"address": "signup-test@test.inboxrhino.in",
"local_part": "signup-test",
"status": "active",
"created_at": "2026-09-03T10:15:30.000Z"
}
}/v1/inboxes
operationId: listInboxes
Lists active inboxes for the organisation. Newest first.
| Query | Default | Rules |
|---|---|---|
| limit | 50 | Integer 1–100. Invalid values return 422 invalid_limit. |
| cursor | none | Opaque value from a previous next_cursor. Invalid values return 422 invalid_cursor. |
/v1/inboxes/{inbox_id}
operationId: getInbox
Returns one active inbox. Unknown, deleted, or foreign ids return 404 inbox_not_found.
/v1/inboxes/{inbox_id}
operationId: deleteInbox
Removes the Email Routing rule, deletes retained messages and stored objects, and frees an active-inbox slot. Success is 204 with an empty body. Monthly email quota is not restored.
curl --request DELETE https://api.inboxrhino.in/v1/inboxes/INBOX_ID \
--header "Authorization: Bearer $INBOXRHINO_API_KEY"Messages
/v1/inboxes/{inbox_id}/messages
operationId: listOrWaitForMessages
Lists retained messages for one inbox, or waits until a match arrives. There is no organisation-wide list. Filters combine with AND. Without include=content, each item is a summary (no text, html, headers, or attachments).
| Query | Default | Rules |
|---|---|---|
| wait_seconds | 0 | Integer 0–180. 0 returns immediately. Greater than 0 long-polls until a match or timeout. |
| limit | 50 | Integer 1–100. Must be 1 when include=content. |
| cursor | none | Opaque pagination cursor. |
| subject | none | Case-insensitive substring of the subject. |
| sender | none | Exact case-insensitive sender email. |
| sender_domain | none | Exact case-insensitive domain (no @). |
| received_after | none | RFC 3339. Messages with received_at at or after this instant. |
| include | none | Only allowed value: content. Requires limit=1. Adds text, html, headers, attachments. |
| Status | When |
|---|---|
| 200 | At least one match, or a non-waiting empty list ({ "data": [], "next_cursor": null }). |
| 204 | wait_seconds > 0 and no match before timeout. Empty body — do not call response.json(). |
| 404 | Inbox missing: inbox_not_found. |
| 422 | Invalid query: invalid_limit, invalid_cursor, invalid_include, invalid_wait_seconds, invalid_sender, invalid_sender_domain, invalid_received_after. |
| 429 | rate_limit_exceeded or long_poll_limit_exceeded. |
{
"data": [
{
"id": "msg_0123456789abcdef0123456789abcdef",
"inbox_id": "inbox_0123456789abcdef0123456789abcdef",
"from": { "name": "Acme", "address": "noreply@example.com" },
"to": "signup-test@test.inboxrhino.in",
"subject": "Verify your email",
"preview": "Use this code to verify your address.",
"size_bytes": 4096,
"received_at": "2026-09-03T10:16:02.000Z",
"expires_at": "2026-10-03T10:16:02.000Z",
"internet_message_id": "<abc@example.com>",
"text": "Your code is 482193",
"html": "<p>Your code is 482193</p>",
"headers": [["Subject", "Verify your email"], ["From", "Acme <noreply@example.com>"]],
"attachments": [
{
"id": "att_0123456789abcdef0123456789abcdef",
"filename": "invoice.pdf",
"content_type": "application/pdf",
"size_bytes": 20480,
"disposition": "attachment",
"content_id": null,
"download_url": "https://files.inboxrhino.in/v1/attachments/att_0123456789abcdef0123456789abcdef"
}
]
}
],
"next_cursor": null
}/v1/messages/{message_id}
operationId: getMessage
Returns the complete parsed message: summary fields plus internet_message_id, text, html, headers (array of [name, value] pairs), and attachments. Always includes content; no include query is needed.
curl --request GET https://api.inboxrhino.in/v1/messages/MESSAGE_ID \
--header "Authorization: Bearer $INBOXRHINO_API_KEY"/v1/messages/{message_id}
operationId: deleteMessage
Deletes the message and stored objects. Success is 204. Does not restore monthly email quota. Prefer deleting the inbox at the end of a test unless you must keep the address.
Attachments
/v1/attachments/{attachment_id}
operationId: downloadAttachment
Downloads raw bytes. Use the download_url from the message, which points at https://files.inboxrhino.in. The same path on https://api.inboxrhino.in is implemented by the same Worker. Auth is still the API Bearer key. Response is not JSON: Content-Disposition is attachment, Content-Type is the stored MIME type, Cache-Control is private, no-store. Content is not malware-scanned.
curl --request GET https://files.inboxrhino.in/v1/attachments/ATTACHMENT_ID \
--header "Authorization: Bearer $INBOXRHINO_API_KEY" \
--output invoice.pdfUsage
/v1/usage
operationId: getUsage
Current free-tier plan, UTC month window, active inbox count, and emails received this period. No data wrapper. plan is currently always free.
{
"plan": "free",
"period": { "starts_at": "2026-09-01T00:00:00.000Z", "ends_at": "2026-10-01T00:00:00.000Z" },
"inboxes": { "active": 4, "limit": 11, "remaining": 7 },
"emails": { "received": 21, "limit": 33, "remaining": 12 }
}Errors
Public /v1 errors always use this envelope. Branch on error.code, not on the human message. Include request_id or the X-Request-Id header when asking for support.
{
"error": {
"code": "inbox_not_found",
"message": "Inbox not found.",
"request_id": "8f1a2c3d9e0b1a2c"
}
}| code | HTTP | Meaning |
|---|---|---|
| invalid_api_key | 401 | Missing, malformed, unknown, or revoked Bearer key. |
| invalid_json | 422 | POST body is not a JSON object. |
| unknown_property | 422 | Request body included a field other than prefix. |
| invalid_prefix | 422 | Prefix failed length, character, or reserved-name rules. |
| invalid_idempotency_key | 422 | Idempotency-Key was shorter than 8 or longer than 200 characters. |
| idempotency_key_reused | 409 | Same Idempotency-Key was reused with a different normalized body. |
| idempotency_in_progress | 409 | Same key is still running. Response includes Retry-After: 1. Retry once. |
| idempotency_unavailable | 503 | Cached idempotent result could not be read. Retry the same request. |
| inbox_quota_exceeded | 409 | Organisation already has 11 active inboxes. |
| address_unavailable | 409 | Requested local-part is already taken. |
| address_generation_failed | 503 | Could not allocate a unique generated address. Retry. |
| email_routing_unavailable | 502 or 503 | Cloudflare Email Routing could not create or delete the exact address rule. |
| inbox_not_found | 404 | Inbox id is unknown, deleted, or belongs to another organisation. |
| message_not_found | 404 | Message id is unknown or already deleted. |
| attachment_not_found | 404 | Attachment metadata or stored bytes are gone. |
| invalid_limit | 422 | limit is not an integer from 1 to 100. Values are not clamped. |
| invalid_cursor | 422 | cursor is not a cursor previously returned as next_cursor. |
| invalid_include | 422 | include must be content, and only with limit=1. |
| invalid_wait_seconds | 422 | wait_seconds is not an integer from 0 to 180. |
| invalid_sender | 422 | sender is not an email address. |
| invalid_sender_domain | 422 | sender_domain contains @ or whitespace. |
| invalid_received_after | 422 | received_after is not an RFC 3339 timestamp. |
| rate_limit_exceeded | 429 | More than 120 requests/minute per key or 600/minute per organisation. Retry at the next UTC minute. |
| long_poll_limit_exceeded | 429 | More than 30 concurrent waits per organisation or 10 per key. |
| internal_error | 500 | Unexpected failure. Include X-Request-Id when reporting. |