Send a test
/api/v1/campaigns/{campaign}/test-sendSends the campaign to addresses you name, so you can see it as a recipient would. Does not touch the audience and does not count against the campaign report.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
campaignrequired | path | string | The campaign identifier. |
Request
/api/v1/campaigns/{campaign}/test-sendcurl -X POST 'https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/test-send' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY'const response = await fetch('https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/test-send', {
method: 'POST',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
},
});
const { data } = await response.json();import requests
response = requests.post(
"https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/test-send",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->post('https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/test-send');
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/campaigns/01JBT8XQ2M9WYC3K4F6R7S8T9V/test-send")
request = Net::HTTP::Post.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.
dataobjectA TEST SEND IS NOT A SEND. It goes through the per-message path, not the fan-out: the campaign's state, its recipient ledger and its stats are all untouched, and the subject is prefixed so nobody mistakes it for the real thing. It is also the one campaign action that does not re-assert the marketing-review gate — you can test a campaign you cannot yet send.
objectstringcampaign_test_sendcampaign_idstringstatestringdraft | scheduled | sending | paused | sent | canceled | unknownThe campaign's lifecycle state, unchanged by the test send — identical before and after, which is the point of publishing it here.
sentintegerResults with status `sent`. Derivable, and kept: it is the headline, and it is computed in the same pass as `results` so the two cannot disagree.
requestedintegerAddresses attempted — at most five.
resultsarray<object>One row per address, in the order given.
emailstringThe address this row is about. In practice NEVER null — a row exists only because an address was attempted, and the addresses are validated before any of this runs. The marker is defensive, not a case you have to handle.
statusstringsent | suppressed | rate_limited | failed | unknown`sent` was handed to the mail server; `suppressed` is on the organization's suppression list; `rate_limited` hit the organization's send rate limit; `failed` threw, see `error`. `unknown` means we did not recognise the value and should never be seen.
errorstringThe raw exception message, PHP function names and all. Unstructured and not a closed set — do not parse it. Same wart as `campaign_recipient.error`, kept for the same reason. `null` means NONE — nothing went wrong on this row — and it is null exactly when `status` is `sent`: `suppressed` and `rate_limited` both carry a written explanation here rather than an exception, so a non-null `error` does not on its own mean the send threw.
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.