Message injection
Injection takes a raw message over HTTPS and stores it as if it had arrived on port 25. A test uses it to build and
debug its own to filters and pinned patterns against a message it
controls, to replay a message captured from real mail, or to try Waridex from a network that blocks port 25, as many
home and cloud networks do.
Injection proves nothing about how the app under test sends its mail, so every injected message says so wherever it is shown. Nothing is relayed and nothing leaves Waridex: the message is stored in the inbox you name and nowhere else.
The call
Section titled “The call”curl -sS --fail-with-body "$API/v1/inboxes/$INBOX_ID/messages" \ -H "Authorization: Bearer $WARIDEX_API_KEY" \ -H "Content-Type: message/rfc822" \ --data-binary @message.emlPOST /v1/inboxes/{id}/messages needs the email:inject scope, which a new key does not start with: add it on the
key’s workspace in the dashboard. The body is the message exactly as it would follow
SMTP’s DATA: RFC 5322 headers and body, sent as Content-Type: message/rfc822. Nothing in it is rewritten, and
GET /v1/messages/{id}/raw returns the bytes you sent. Any other content type is
415 request.invalid; a request that names none is read as a message.
{ "messages": [ { "id": "0199a3c4-8e2f-7b90-a1c4-5d6e7f809a1b", "envelopeTo": "user1@signup-k3f9x2.waridex.email", "created": true } ]}The answer is 201 Created when at least one row was made and 200 OK when every recipient already had the message,
both with one row per recipient in the order they were taken. Location names the first message’s
GET /v1/messages/{id}.
Every message needs a Message-ID
Section titled “Every message needs a Message-ID”A Message-ID header is required: without one the call is
400 message.message_id_required. It is what makes a retry safe. The same
Message-ID to the same recipient of the same inbox never makes a second row, whether the first arrived over SMTP or
by injection, so a test that retries after a lost response stores nothing twice.
A message saved from a mail client carries one. A message written by hand needs a line such as
Message-ID: <sample-1@example.com>.
Recipients
Section titled “Recipients”to, repeatable, names them. Each must be an address of the inbox: any local part under its domain, compared ignoring case.- Without
to, the recipients are the addresses of the message’sTo,CcandBccheaders that belong to the inbox, in that order and each once; when the headers name none, the inbox’s defaultaddress. - A
tonaming any other address — another inbox’s, a foreign domain, the bare receiving domain — is400 message.recipient_invalidnaming it. The headers are read the other way about: an address of theirs that is not the inbox’s is passed over, so a message captured from real mail keeps its originalToand still lands. Either way, more than 20 recipients is400 message.too_many_recipients.
As on port 25, each recipient gets its own message, all of them sharing one stored copy, and envelopeTo is the
address as given, so wait with to= finds each. The envelope sender is the first address of
the From header, or empty where the header has none, as SMTP records a null sender.
What injection changes, and what it does not
Section titled “What injection changes, and what it does not”Everything after the message is admitted is the pipeline mail from port 25 goes through: extraction when it arrives,
the monthly quota counted once per message and workspace, pending wait calls completed, the workspace’s statistics,
and retention.
Two things differ, and both are visible in the message:
| Field | Injected | Received over SMTP |
|---|---|---|
source | Api | Smtp |
spfResult, dkimResult | left out | the sender’s results, for diagnosis |
SPF and DKIM are not evaluated for an injected message: there is no connecting client to check, and a signature on a
replayed message would describe its first delivery. Every message the API returns carries source, so a suite that
mixes injected and received mail can tell them apart.
Limits
Section titled “Limits”The first four are the limits port 25 applies, and the key’s 600 calls a minute apply besides. The last two are the route’s own: an injection is an HTTP request the API must hold open while it reads the message, which mail arriving on port 25 is not.
| Limit | Beyond it |
|---|---|
| 10 MB a message | 413 message.too_large, checked against Content-Length before the body is read and while a body without one is read |
| the message must parse | 400 message.unparsable |
| the plan’s monthly quota | on Free, 403 quota.exceeded with tier, quota and resetAt, and nothing is stored; on Team and Business the message is accepted and counted |
| 300 recipients a minute for the workspace, the minute mail on port 25 shares | 429 rate_limit.exceeded with Retry-After until the window ends. Each recipient takes a place, as it would at RCPT TO, so one call to twenty addresses spends twenty |
| eight injections at once in the API, two of them for one workspace | the call waits for its turn; one that waits more than ten seconds is 429 rate_limit.exceeded with Retry-After |
| the body must keep up 64 KB a second after its first five seconds | below that the request ends with 408 request.invalid, so a slow upload cannot hold a turn for long |
A call answered anything but 201 or 200 takes no place in the workspace’s minute: one the rate limit refuses takes
none, and one that fails after it gives its places back, so an outage cannot spend the minute. The inbox itself is
checked first, so another workspace’s inbox, a deleted one and an expired one are
404 inbox.not_found.
From the dashboard
Section titled “From the dashboard”The Email page uploads one .eml file at a time into an inbox, through the same route: “Upload .eml” in the inbox’s
header, beside Refresh and Manage. It is for the file a test run leaves behind — a message whose code was extracted
wrongly, or a sender’s new template — and the viewer then shows it with its codes, candidates and links, so “use this
code as the pattern” can pin a pattern from it before the app under test is wired up.

There is no recipient field: the file’s own addresses decide, as above. The dialog refuses a file over 10 MB before
sending anything, and a message the API refuses leaves the file chosen so it can be sent again. Everything in this
page holds for the upload as for a call from a test, the Message-ID included. Workspace roles holding
email:inject — Member and Admin, not Viewer — see the action, and the API refuses the others regardless.
Problems
Section titled “Problems”| Problem | When |
|---|---|
400 message.message_id_required | the message has no Message-ID |
400 message.recipient_invalid | a recipient is not an address of the inbox |
400 message.too_many_recipients | more than 20 recipients |
400 message.unparsable | the message could not be parsed |
403 quota.exceeded | a Free workspace is over its monthly quota |
403 scope.missing | the key or the member does not hold email:inject |
404 inbox.not_found | the inbox is another workspace’s, deleted or expired |
413 message.too_large | the message is over 10 MB |
415 request.invalid | the content type is not message/rfc822 |
429 rate_limit.exceeded | the workspace’s inbound minute is spent, or no turn freed in ten seconds |
503 store.unavailable | the message store is unreachable; nothing is stored |