Sending history
Two lists show what you sent and to whom:
| Request | List row | Period |
|---|---|---|
GET /api/v1/public/emails/recipients | A delivery to one recipient | The account's entire history |
GET /api/v1/public/emails | An email with all its recipients | Emails accepted since ДД.ММ.ГГГГ |
To search by recipient or delivery status, use the delivery list; to examine an email as a whole — with all its to, cc, and bcc — use the email list.
Before you start
- A token with the
email.readscope.
Common to both lists
- Order — newest first.
- Pagination — by cursor, forward only:
limitfrom 1 to 100 (20 by default) andcursorfrom the previous response'snextCursor. WhilehasMore: true, the response containsnextCursor; on the last pagehasMore: falseandnextCursor: null. The response doesn't include the total number of records. - Cursor — an opaque string: don't parse it or build it yourself. A cursor only works with the same filters as the first request and only for the same list; it doesn't expire. A cursor from other filters, from the other list, or a corrupted one returns
400 BAD_REQUESTwithdetails: [{ "field": "cursor", "reason": "INVALID_CURSOR" }]. - New sends don't shift the pages: as you page back in time, you get no duplicates and miss no records. New records appear at the start of the list — you see them when you request without a cursor.
- Dates
dateFromanddateTo— an ISO 8601 date or date-time in UTC, bounds inclusive. A date without a time means the whole UTC day:dateTo=2026-10-19includes all of October 19. keyId— only records of one sending key. Key not found —404 SENDING_KEY_NOT_FOUND.- Rate limit — 60 per minute per API key for each list; when it's exceeded you get
429 RATE_LIMIT_EXCEEDEDand aRetry-Afterheader.
Deliveries
GET /api/v1/public/emails/recipients — a row for every recipient of every email, including sends from before ДД.ММ.ГГГГ.
| Parameter | Meaning |
|---|---|
limit, cursor | Page size and cursor — see Common |
keyId | Sending key |
status | Comma-separated delivery statuses: queued, sending, delivered, suppressed, bounced, failed |
dateFrom, dateTo | When the delivery was accepted |
to | Recipient address or part of it |
from | Sender address |
- TypeScript
- Go
- Python
- cURL
let cursor: string | null = null;
while (true) {
const params = new URLSearchParams({ status: 'bounced,failed', dateFrom: '2026-10-01', limit: '100' });
if (cursor) params.set('cursor', cursor);
const response = await fetch(`https://api.emailmassivo.com/api/v1/public/emails/recipients?${params}`, {
headers: { 'Authorization': `Bearer ${process.env.API_TOKEN}` },
});
if (response.status === 429) {
await new Promise((r) => setTimeout(r, Number(response.headers.get('Retry-After') ?? 1) * 1000));
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { data } = await response.json();
for (const delivery of data.items) console.log(delivery.email, delivery.status, delivery.reason?.code);
if (!data.nextCursor) break;
cursor = data.nextCursor;
}
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
func main() {
params := url.Values{"status": {"bounced,failed"}, "dateFrom": {"2026-10-01"}, "limit": {"100"}}
for {
req, _ := http.NewRequest("GET", "https://api.emailmassivo.com/api/v1/public/emails/recipients?"+params.Encode(), nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("API_TOKEN"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
if resp.StatusCode == http.StatusTooManyRequests {
wait, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
resp.Body.Close()
time.Sleep(time.Duration(max(wait, 1)) * time.Second)
continue
}
if resp.StatusCode != http.StatusOK {
panic(resp.Status)
}
var body struct {
Data struct {
Items []struct {
Email string `json:"email"`
Status string `json:"status"`
Reason *struct {
Code string `json:"code"`
} `json:"reason"`
} `json:"items"`
NextCursor *string `json:"nextCursor"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&body)
resp.Body.Close()
for _, d := range body.Data.Items {
code := ""
if d.Reason != nil {
code = d.Reason.Code
}
fmt.Println(d.Email, d.Status, code)
}
if body.Data.NextCursor == nil {
break
}
params.Set("cursor", *body.Data.NextCursor)
}
}
import os
import time
import requests
params = {'status': 'bounced,failed', 'dateFrom': '2026-10-01', 'limit': 100}
while True:
response = requests.get(
'https://api.emailmassivo.com/api/v1/public/emails/recipients',
params=params,
headers={'Authorization': f'Bearer {os.environ["API_TOKEN"]}'},
)
if response.status_code == 429:
time.sleep(int(response.headers.get('Retry-After', 1)))
continue
response.raise_for_status()
data = response.json()['data']
for delivery in data['items']:
print(delivery['email'], delivery['status'], (delivery['reason'] or {}).get('code'))
if not data['nextCursor']:
break
params['cursor'] = data['nextCursor']
curl "https://api.emailmassivo.com/api/v1/public/emails/recipients?status=bounced,failed&dateFrom=2026-10-01&limit=100" \
-H "Authorization: Bearer $API_TOKEN"
The next page is the same request with &cursor=<nextCursor from the response>.
{
"ok": true,
"data": {
"items": [
{
"id": "c4a07e13-5d2b-4e8f-b916-7f3a2d0c8b44",
"emailId": "0199b2f4-6c1e-7a3d-9f2b-4e8c1d7a5b60",
"email": "manager@shop.com",
"name": null,
"type": "cc",
"status": "bounced",
"reason": {
"code": "DELIVERY_MAILBOX_FULL",
"bounceType": "transient",
"message": "Recipient mailbox is full."
},
"keyId": 42,
"from": { "email": "noreply@shop.com", "name": "Shop" },
"subject": "Order confirmation",
"createdAt": "2026-09-30T10:15:00.000Z",
"sentAt": "2026-09-30T10:15:02.310Z",
"completedAt": "2026-09-30T10:15:05.020Z"
}
],
"hasMore": true,
"nextCursor": "Q2xvc2VkIGJvb2sgY3Vyc29yIGV4YW1wbGU"
},
"meta": { "requestId": "01JB2W...", "timestamp": "2026-09-30T10:16:00.000Z" }
}
| Field | Meaning |
|---|---|
id | Delivery identifier — the same as the email's recipients[].id and recipient.id in webhooks |
emailId | The email the delivery belongs to: open it with GET /api/v1/public/emails/{emailId}. null for sends from before ДД.ММ.ГГГГ |
email | Recipient address |
name, type | Recipient name and type (to, cc, bcc); null for sends from before ДД.ММ.ГГГГ |
status, reason | Delivery status and reason — without the recipient server's SMTP response, which is in the delivery details |
keyId | Sending key |
from | Sender; name is null for sends from before ДД.ММ.ГГГГ |
subject | Subject |
createdAt, sentAt, completedAt | Acceptance, handoff for sending, and final status — as for an email recipient |
Emails
GET /api/v1/public/emails — a row per email with all its recipients. The list contains emails accepted since ДД.ММ.ГГГГ by any method, including SMTP. Earlier sends are only in the delivery list.
| Parameter | Meaning |
|---|---|
limit, cursor | Page size and cursor — see Common |
keyId | Sending key |
dateFrom, dateTo | When the email was accepted |
from | Sender address |
The email list has no status or recipient filters — use the delivery list for those.
curl "https://api.emailmassivo.com/api/v1/public/emails?keyId=42&limit=20" \
-H "Authorization: Bearer $API_TOKEN"
A list item is an email in the same shape as GET /api/v1/public/emails/{emailId}, but its recipients have no events and no SMTP response in reason. Events and the SMTP response are available when reading the email and in the delivery details.
Handling errors
| HTTP | error.code | Cause | Action |
|---|---|---|---|
| 400 | BAD_REQUEST | Invalid filter or cursor — the field is in details[].field | Fix the parameter; on INVALID_CURSOR, start from the first page |
| 403 | SCOPE_REQUIRED | The token lacks the email.read scope | Add the scope to the token |
| 404 | SENDING_KEY_NOT_FOUND | No key with this keyId | Check keyId |
| 429 | RATE_LIMIT_EXCEEDED | More than 60 requests per minute | Retry after the pause from Retry-After |
See also
- Email statuses and events — reading a single email, statuses and reasons.
- Email webhooks — statuses delivered to your server without polling.
- API reference: list deliveries, list emails.