Wait for a sandbox message
/api/v1/sandbox/inboxes/{id}/messages/awaitHolds the request open until a message matching your filters arrives in the inbox, then
returns it in full (the same object as GET /sandbox/messages/{id}). If nothing matches
within timeout seconds the answer is 204 No Content — "not yet", not an error —
so a test can simply call again.
This is the call for end-to-end tests: trigger the action in your app, then wait for the email it should have sent and assert on its subject, HTML or links.
- Pass
since(the time your test started) so a message from an earlier run never satisfies this one. Only messages received after it are considered. tomatches any recipient, including Bcc.subjectmatches if it is contained in the subject.- At most five waits can be open at once per organization; the sixth gets
429.
A CI step with curl, using a key with the sandbox:read scope:
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
./run-signup-test.sh "ci+${GITHUB_RUN_ID}@example.com"
for attempt in 1 2 3 4; do
STATUS=$(curl -s -o message.json -w '%{http_code}' -G \
"https://api.mailyte.com/api/v1/sandbox/inboxes/$INBOX_ID/messages/await" \
-H "Authorization: Bearer $MAILYTE_API_KEY" \
--data-urlencode "to=ci+${GITHUB_RUN_ID}@example.com" \
--data-urlencode "subject=Confirm your email" \
--data-urlencode "since=$SINCE" \
--data-urlencode "timeout=30")
[ "$STATUS" = "200" ] && break
[ "$STATUS" = "204" ] || { echo "await failed: $STATUS"; cat message.json; exit 1; }
done
[ "$STATUS" = "200" ] || { echo "no confirmation email in 2 minutes"; exit 1; }
jq -r '.data.html' message.json | grep -o 'https://app.example.com/confirm/[^"]*'Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string | The id identifier. |
to | query | string | A recipient address (envelope or To header). |
subject | query | string | Text the subject contains. |
since | query | string | ISO 8601. Only messages received after this count. |
timeout | query | integer | Seconds to wait, 1–30. Default 30. |
Request
/api/v1/sandbox/inboxes/{id}/messages/awaitcurl -X GET 'https://app.mailyte.com/api/v1/sandbox/inboxes/01JBT8XQ2M9WYC3K4F6R7S8T9V/messages/await' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY'const response = await fetch('https://app.mailyte.com/api/v1/sandbox/inboxes/01JBT8XQ2M9WYC3K4F6R7S8T9V/messages/await', {
method: 'GET',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
},
});
const { data } = await response.json();import requests
response = requests.get(
"https://app.mailyte.com/api/v1/sandbox/inboxes/01JBT8XQ2M9WYC3K4F6R7S8T9V/messages/await",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->get('https://app.mailyte.com/api/v1/sandbox/inboxes/01JBT8XQ2M9WYC3K4F6R7S8T9V/messages/await');
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/sandbox/inboxes/01JBT8XQ2M9WYC3K4F6R7S8T9V/messages/await")
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
Success.
dataobjectidstringULID of the stored message.
inbox_idstringsubjectstringheader_fromstringThe From header, as written.
header_toarray<string>The To header addresses.
envelope_rcptsarray<string>Every address the message was sent to, INCLUDING Bcc: blind recipients are in the envelope, never in the headers, so this is the only place they show.
size_bytesintegerattachment_countintegersourcestringsmtp | api | inbound`api` for a test-mode API key send; `inbound` for mail sent to the inbox's `inbound_address`.
simulated_outcomestringdelivered | bounce | softbounce | complaint | suppressedThe outcome simulated for this message (a `@sim.mailyte.com` address or an `X-Mailyte-Simulate` header). Null means the default: accepted, then delivered.
spam_scorenumberNull until the message has been scored; scoring runs after it arrives.
read_atstringWhen it was marked read. Null means unread.
received_atstringWhen the sandbox accepted it.
headersarray<object>Every header in order, repeated names included.
namestringvaluestring
textstringThe text/plain part, decoded. Null when there is none.
htmlstringThe text/html part, decoded and NOT sanitised: it is exactly what your code sent. Render it only in a sandboxed frame with scripts disabled.
attachmentsarray<object>indexintegerPass to GET /sandbox/messages/{id}/attachments/{index}.
filenamestringcontent_typestringsizeintegercontent_idstringSet for an inline (cid:) part.
eventsarray<object>The simulated delivery timeline. These exist only in the sandbox: they never reach your suppression list, your live event log or your live webhooks.
event_typestringe.g. `email.accepted`, `email.delivered`, `email.bounced`.
recipientstringdetailobjectcreated_atstring
webhook_deliveriesarray<object>Every sandbox webhook attempt this message caused, with your endpoint's response.
idintegerwebhook_idstringmessage_idstringThe sandbox message that caused it. Null for a `sandbox.test` event.
event_typestringattemptinteger1 to 5. Retries back off 30 s, 2 min, 5 min, 15 min, 30 min.
status_codeintegerNull when no response arrived; see `error`.
duration_msintegerresponse_excerptstringThe first 1 KB of your response body.
errorstringWhy no response arrived (timeout, refused, blocked address).
created_atstringWhen the attempt was made.
spamobjectThe spam filter's verdict, as a receiving server would score it. Null until `analysed_at` is set; null after that means scoring was unavailable.
scorenumberrequired_scorenumberThe score at which mail is treated as spam.
actionstringe.g. `no action`, `add header`, `reject`.
symbolsarray<object>The rules that fired, largest effect first.
namestringscorenumberdescriptionstringoptionsarray<string>
checksarray<object>Problems found in the message (missing text part, images without alt text, insecure links, missing List-Unsubscribe on bulk mail...). An empty list means none; null means not analysed yet (see `analysed_at`) or the step failed.
idstringseveritystringerror | warning | infotitlestringdetailstring
html_supportobjectHow well the HTML is supported across email clients. Null for a message with no HTML part, before `analysed_at` is set, or if the step failed.
score_pctintegerMarket-share-weighted share of clients that fully support every feature used.
detected_countintegerclientsarray<object>familystringsupportstringfull | partial | none
featuresarray<object>Features the message uses that fail or are partial somewhere.
slugstringtitlestringunsupported_inarray<string>partial_inarray<string>
analysed_atstringWhen analysis finished. It runs a few seconds after the message arrives; until then `spam`, `checks` and `html_support` are null.
Returned inside the standard envelope.
Errors
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait. |
Every status, with what causes it and what to do, is on the error reference.