Email statuses and events
After a 202 response you can read the email by its id: the email status, the status and reason of each recipient, open and click events. The same statuses and reasons arrive in email.* webhooks.
Before you start
Endpoints
| Request | Returns |
|---|---|
GET /api/v1/public/emails/{emailId} | The email and all its recipients, each with the first event of every type |
GET /api/v1/public/emails/{emailId}/recipients/{recipientId} | Delivery details for one recipient: the recipient server's SMTP response, click count |
GET /api/v1/public/emails/{emailId}/recipients/{recipientId}/events | All events of a recipient, paginated |
emailId is data.id from the send response, recipientId is recipients[].id. The email is readable right after the 202 response.
You can read emails accepted since ДД.ММ.ГГГГ by any method, including SMTP. Earlier sends can't be read by emailId (404) — their deliveries are in the sending history.
These endpoints use the shared public API request limit; when it's exceeded you get 429 RATE_LIMIT_EXCEEDED and a Retry-After header with the number of seconds before you can retry.
Reading an email
- TypeScript
- Go
- Python
- cURL
const emailId = process.env.EMAIL_ID;
const response = await fetch(
`https://api.emailmassivo.com/api/v1/public/emails/${emailId}`,
{ headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` } },
);
const { data } = await response.json();
console.log(data.status, data.recipients.map((r) => [r.email, r.status]));
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
emailID := os.Getenv("EMAIL_ID")
req, _ := http.NewRequest("GET", "https://api.emailmassivo.com/api/v1/public/emails/"+emailID, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("API_TOKEN"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var body struct {
Data struct {
Status string `json:"status"`
Recipients []struct {
Email string `json:"email"`
Status string `json:"status"`
} `json:"recipients"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&body)
fmt.Println(body.Data.Status)
for _, r := range body.Data.Recipients {
fmt.Println(r.Email, r.Status)
}
}
import os
import requests
email_id = os.environ['EMAIL_ID']
response = requests.get(
f'https://api.emailmassivo.com/api/v1/public/emails/{email_id}',
headers={'Authorization': f'Bearer {os.environ["API_TOKEN"]}'},
)
email = response.json()['data']
print(email['status'], [(r['email'], r['status']) for r in email['recipients']])
curl https://api.emailmassivo.com/api/v1/public/emails/$EMAIL_ID \
-H "Authorization: Bearer $API_TOKEN"
{
"ok": true,
"data": {
"id": "0199b2f4-6c1e-7a3d-9f2b-4e8c1d7a5b60",
"status": "delivered",
"keyId": 42,
"from": { "email": "noreply@shop.com", "name": "Shop" },
"subject": "Order confirmation",
"templateId": null,
"createdAt": "2026-09-30T10:15:00.000Z",
"sentAt": "2026-09-30T10:15:02.310Z",
"completedAt": "2026-09-30T10:15:04.870Z",
"recipients": [
{
"id": "3f6c2a9e-8b1d-4c7a-9e2f-5a1b7c3d9e01",
"email": "client@example.com",
"name": "John",
"type": "to",
"status": "delivered",
"reason": null,
"sentAt": "2026-09-30T10:15:02.310Z",
"completedAt": "2026-09-30T10:15:04.870Z",
"events": [
{ "type": "delivered", "occurredAt": "2026-09-30T10:15:04.870Z" },
{ "type": "clicked", "occurredAt": "2026-09-30T10:20:30.050Z", "url": "https://shop.com/orders/1234" }
]
},
{
"id": "c4a07e13-5d2b-4e8f-b916-7f3a2d0c8b44",
"email": "manager@shop.com",
"name": null,
"type": "cc",
"status": "bounced",
"reason": {
"code": "DELIVERY_MAILBOX_FULL",
"bounceType": "transient",
"message": "Recipient mailbox is full.",
"smtpResponse": "552 5.2.2 Mailbox full"
},
"sentAt": "2026-09-30T10:15:02.310Z",
"completedAt": "2026-09-30T10:15:05.020Z",
"events": [
{ "type": "bounced", "occurredAt": "2026-09-30T10:15:05.020Z" }
]
}
]
},
"meta": { "requestId": "01JB2W...", "timestamp": "2026-09-30T10:16:00.000Z" }
}
| Field | Meaning |
|---|---|
id | Email identifier |
status | Email status — see Email status |
keyId | The sending key the email was accepted with |
from | Sender |
subject | Subject after variable substitution |
templateId | The email's template or null. When sent by alias — the id of the template found by the alias |
createdAt | When the email was accepted |
sentAt | The earliest send time among to recipients, or null |
completedAt | When the email reached a final status, or null |
recipients | All recipients of the email, each with its own id and status |
Recipient
| Field | Meaning |
|---|---|
id | Identifier of the delivery to this recipient |
email, name | Address and name — as in the request |
type | to, cc, or bcc |
status | Delivery status — see Statuses |
reason | Reason for bounced, failed, and suppressed, otherwise null — see Reason |
sentAt | When the email was handed off for sending to this recipient; null if it wasn't sent |
completedAt | When the delivery reached a final status, or null |
events | The first event of each type, oldest first — see Events |
Statuses
queued → sending → delivered | bounced | failed
recipients also have: suppressed
| Status | Meaning | Final |
|---|---|---|
queued | The email is accepted and waiting to be sent | no |
sending | The email has been handed off for sending | no |
delivered | The recipient's mail server confirmed receipt | yes |
bounced | Not delivered for a reason on the recipient's side: the address doesn't exist, the mailbox is full, the server refused or is unreachable — or the address is on the EmailMassivo list of undeliverable addresses. The bounce type is reason.bounceType | yes |
failed | The email couldn't be sent. The recipient address is fine — you can send the email again | yes |
suppressed | Recipients only: the recipient unsubscribed from this sending key's emails or complained about them. The email wasn't sent to them | yes |
opened, clicked, unsubscribed, and complained are events, not statuses: after an open the recipient status stays delivered.
delivered sometimes turns into bouncedOccasionally the recipient's mail server sends a non-delivery report after it has already confirmed receipt. The recipient's status then changes from delivered to bounced, and an email.bounced webhook arrives. There are no other transitions out of a final status.
Email status
The email status is a summary of the to recipients: while delivery is in progress — queued or sending; when delivered to everyone — delivered; otherwise — bounced or failed. cc and bcc don't affect it.
Make decisions about a specific address based on that recipient's status and reason.
Reason
{
"code": "DELIVERY_MAILBOX_FULL",
"bounceType": "transient",
"message": "Recipient mailbox is full.",
"smtpResponse": "552 5.2.2 Mailbox full"
}
| Field | Meaning |
|---|---|
code | Reason code — see the table below |
bounceType | bounced only: permanent, transient, or undetermined |
message | Fixed English text for the code |
smtpResponse | bounced after a send attempt only: the recipient server's response with IP addresses removed. If the response isn't available, the field is absent |
bounceType
bounceType | Meaning | What to do |
|---|---|---|
permanent | The address doesn't exist or is on the EmailMassivo list of undeliverable addresses | Remove the address from your base, don't send to it |
transient | A refusal right now: the mailbox is full, the email was taken for spam, the sender domain's signature check failed, a rate limit, a temporary failure. The address is alive | You can send later; for content or signature refusals fix the cause first |
undetermined | The server refused, but the reason is unclear or it's a recipient policy for a live address | Not a reason to remove the address; if refusals repeat, investigate using smtpResponse |
Remove an address from your base on bounceType: permanent, not on any bounced.
Reason codes
| Status | code | bounceType | Meaning |
|---|---|---|---|
suppressed | RECIPIENT_UNSUBSCRIBED | — | The recipient unsubscribed from this key's emails |
suppressed | RECIPIENT_COMPLAINED | — | The recipient complained about this key's emails |
bounced | RECIPIENT_KNOWN_BOUNCED | permanent | The address is already known as undeliverable; the email wasn't sent |
bounced | RECIPIENT_BLOCKED | permanent | The address is blocked; the email wasn't sent |
bounced | RECIPIENT_INVALID | permanent | The address is considered invalid; the email wasn't sent |
bounced | RECIPIENT_DOMAIN_INVALID | permanent | The address's domain doesn't accept mail; the email wasn't sent |
bounced | DELIVERY_MAILBOX_NOT_FOUND | permanent | The mailbox doesn't exist |
bounced | DELIVERY_DOMAIN_NOT_FOUND | permanent | The domain doesn't exist or has no mail server |
bounced | DELIVERY_REJECTED | permanent or undetermined | The recipient server rejected the email |
bounced | DELIVERY_MAILBOX_FULL | transient | The mailbox is full |
bounced | DELIVERY_RATE_LIMITED | transient | The recipient server is limiting the rate of incoming email |
bounced | DELIVERY_SPAM_REJECTED | transient | The recipient server took the email for spam |
bounced | DELIVERY_SENDER_AUTH_FAILED | transient | The sender domain's SPF, DKIM, or DMARC check failed |
bounced | DELIVERY_TEMPORARY_FAILURE | transient | The recipient server temporarily refused the email |
bounced | DELIVERY_CONNECTION_FAILED | undetermined | The recipient's mail server couldn't be reached |
bounced | DELIVERY_UNDETERMINED | undetermined | The server refused for an undetermined reason |
failed | INTERNAL_ERROR | — | The email couldn't be sent |
RECIPIENT_* codes on bounced mean an early rejection: the email wasn't sent to this recipient, no quota was charged for them, sentAt is null, and there's no smtpResponse. failed never has an SMTP response.
Sending again
bounced and failed are final statuses: there will be no more delivery attempts for this email. Sending again means a new email: a new request with a new Idempotency-Key, and quota is charged for it again.
suppressed— don't send to this recipient through this key: they opted out themselves.bouncedwithpermanent— don't send to this address.bouncedwithtransient— you can send later. ForDELIVERY_SPAM_REJECTEDandDELIVERY_SENDER_AUTH_FAILED, fix the content or the domain's DNS records first — otherwise the refusal repeats.bouncedwithundetermined— you can send later; if refusals repeat, investigate usingsmtpResponse.failed— the address is fine; send the email again.
Events
| Type | When |
|---|---|
suppressed, delivered, bounced, failed | The recipient reached a final status |
opened | The recipient opened the email |
clicked | The recipient clicked a link; the event has url |
unsubscribed | The recipient unsubscribed from this sending key's emails |
complained | The recipient complained about the email |
In GET /api/v1/public/emails/{emailId} each recipient has no more than five events: the final one plus the first opened, clicked, unsubscribed, and complained.
All events of a recipient
curl "https://api.emailmassivo.com/api/v1/public/emails/$EMAIL_ID/recipients/$RECIPIENT_ID/events?type=clicked,opened&page=1&limit=20" \
-H "Authorization: Bearer $API_TOKEN"
type— comma-separated event types; without the parameter — all types.page— starts at 1;limit— 1 to 100, 20 by default. Pagination totals are inmeta.pagination.- Events are sorted by
occurredAtascending. openedandclickedhavedevice(desktop,phone,tablet, ornull) andclient(outlook,chrome,firefox,safari,apple_mail,other, ornull).
Repeated opens aren't stored: events contain only the first opened. All clicks are stored.
Delivery details for a recipient
GET /api/v1/public/emails/{emailId}/recipients/{recipientId} returns the same recipient fields as reading the email, plus:
| Field | Meaning |
|---|---|
emailId | The email this delivery belongs to |
smtpResponse | The recipient server's last response with IP addresses removed — for delivered and for bounced after a send attempt. If the response is temporarily unavailable, the field is absent |
clickCount | How many times the recipient clicked links in the email |
Responses don't include the email content, the Message-ID header, or the recipient's IP addresses and user agent.
Handling errors
| HTTP | error.code | Cause | Action |
|---|---|---|---|
| 403 | SCOPE_REQUIRED | The token lacks the email.read scope | Add the scope to the token |
| 404 | EMAIL_NOT_FOUND | There's no email with this id, it belongs to someone else, it was accepted before ДД.ММ.ГГГГ, or the id isn't a UUID. A recipients[].id passed as emailId also gets this code | Check that the path has data.id from the send response |
| 404 | EMAIL_RECIPIENT_NOT_FOUND | The email exists but has no such recipient | Take recipientId from this email's recipients[] |
| 429 | RATE_LIMIT_EXCEEDED | The shared public API request limit is exceeded | Retry after the pause from Retry-After |
See also
- Email webhooks — the same statuses and events without polling.
- Send a transactional email — the
202response and rejected recipients. - Send error codes.
- API reference: get email, recipient, recipient events.