Email Status & Lifecycle
Understand email status states and tracking events
Every email in Tratto has a status (the state of the email) and events (what happened to it). Understand the difference and how to track them.
Status vs Events
- Status: The current state of the email (
scheduled,queued,sending,sent,delivered,bounced,complained,failed,skipped,canceled) - Events: Discrete tracking records (opened, clicked, bounced, complained)
Example: An email can have status delivered and also have events opened, clicked, and clicked again.
Email Status Lifecycle
An email progresses through these statuses:
(only with scheduledAt)
scheduled ──→ queued ──→ sending ──→ sent ──→ delivered
│ │ │ │ │
▼ ▼ ▼ └────┬─────┘
canceled skipped failed ▼
skipped failed bounced, complainedAn email sent without scheduledAt starts at queued. Opens, clicks and
unsubscribes are events, not statuses: they never change the
status.
Status Definitions
| Status | Description |
|---|---|
scheduled | Email is scheduled for future delivery (you set scheduledAt). It moves to queued when its time comes |
queued | Email is in the queue, will be sent shortly (minutes) |
sending | Tratto has picked the email up and is handing it to the mail provider. It lasts seconds |
sent | Email was handed to the mail server, but not yet confirmed by the recipient's server |
delivered | The recipient's mail server confirmed receipt |
bounced | The recipient's mail server rejected the email |
complained | The recipient marked the email as spam |
failed | Tratto did not send the email (an unverified sender domain, an unpublished template, an attachment that was deleted, a refusal by the mail provider, etc.). With API 0.29.0 or later the reason is in failureReason: see Why an email failed |
skipped | Tratto did not send the email because the recipient unsubscribed or is on the suppression list. Nothing was sent |
canceled | You canceled a scheduled email with DELETE /v1/emails/:id before it was sent |
GET /v1/emails?status= filters by any of these values, sending included; several can be combined with commas (?status=queued,sending).
failed, bounced and complained are final: nothing that arrives later
changes them, with the one exception below.
When delivery is interrupted
If the delivery is interrupted while Tratto is handing the email to the mail
provider (for example the worker that was sending it stops), Tratto cannot
know whether the provider accepted it. In that case the email becomes failed
and is not retried: Tratto prefers not sending an email to sending it
twice. The email may still have reached the recipient. If the provider later
reports a delivery, a bounce or a complaint for it, the status moves to
delivered, bounced or complained.
An email that stays in sending for more than a few minutes is in this
situation and will be closed as failed when Tratto picks it up again.
Why an email failed
Requires API 0.29.0 or later.
GET /v1/emails/{id} and GET /v1/emails return failureReason on a
failed email: a stable code such as template_not_published,
provider_rejected or attachment_unavailable. The same code arrives as
data.reason in the failed webhook event,
whose data.error adds a sentence for people. The list of codes and their
meaning is in The failed event. New codes
may be added, so handle values you do not know. Emails that failed before this
field existed have no failureReason.
interrupted is the only code that is not final: see
When delivery is interrupted.
State Transitions
scheduled (only if scheduledAt was set)
├─→ canceled (DELETE /v1/emails/:id)
├─→ skipped (the recipient unsubscribed)
└─→ queued (the scheduled time has come)
queued
├─→ skipped (unsubscribed or suppressed recipient)
├─→ failed (the email cannot be built or sent)
└─→ sending (Tratto hands the email to the mail provider)
├─→ failed (rejected by the provider, or delivery interrupted)
└─→ sent (the mail server accepted the email)
├─→ delivered (the recipient's server confirms delivery)
│ └─→ bounced, complained
└─→ bounced, complainedGet Email Status
Retrieve an email's current status and details.
cURL
curl https://api.tratto.email/v1/emails/email_abc123 \
-H "Authorization: Bearer tratto_live_..."Node.js
const response = await fetch('https://api.tratto.email/v1/emails/email_abc123', {
headers: { 'Authorization': 'Bearer tratto_live_...' },
});
const { data } = await response.json();
console.log('Status:', data.status);
console.log('Created:', data.createdAt);
console.log('Sent:', data.sentAt);Python
import requests
response = requests.get(
'https://api.tratto.email/v1/emails/email_abc123',
headers={'Authorization': 'Bearer tratto_live_...'},
)
data = response.json()['data']
print(f"Status: {data['status']}")
print(f"Created: {data['createdAt']}")PHP
$tratto = new \Tratto\Client(getenv('TRATTO_API_KEY'));
$email = $tratto->emails->get('email_abc123');
echo "Status: {$email['status']}\n";
echo "Created: {$email['createdAt']}\n";Response
{
"data": {
"id": "email_abc123",
"status": "delivered",
"from": "hello@yourdomain.com",
"to": "user@example.com",
"subject": "Welcome!",
"createdAt": "2025-06-30T12:00:00Z",
"sentAt": "2025-06-30T12:00:05Z",
"deliveredAt": "2025-06-30T12:00:10Z",
"scheduledAt": null,
"tags": []
}
}Email Events
Events are discrete tracking records for what happened to an email after it was sent. Unlike status (a single state), an email can have many events.
Event Types
| Event Type | Description |
|---|---|
sent | Email was accepted by the mail server |
delivered | Mail server confirmed delivery to recipient's mailbox |
opened | Recipient opened the email (pixel tracking) |
clicked | Recipient clicked a link in the email |
bounced | Mail server rejected delivery (hard or soft bounce) |
complained | Recipient marked email as spam |
unsubscribed | Recipient clicked unsubscribe link |
failed | The email was not sent; data.reason and data.error say why (requires API 0.29.0 or later) |
Get Email Events
Retrieve all events for a specific email.
cURL
curl https://api.tratto.email/v1/emails/email_abc123/events \
-H "Authorization: Bearer tratto_live_..."Node.js
const response = await fetch('https://api.tratto.email/v1/emails/email_abc123/events', {
headers: { 'Authorization': 'Bearer tratto_live_...' },
});
const { data } = await response.json();
data.forEach(event => {
console.log(`${event.type} at ${event.occurredAt}`);
});Python
import requests
response = requests.get(
'https://api.tratto.email/v1/emails/email_abc123/events',
headers={'Authorization': 'Bearer tratto_live_...'},
)
events = response.json()['data']
for event in events:
print(f"{event['type']} at {event['occurredAt']}")PHP
$tratto = new \Tratto\Client(getenv('TRATTO_API_KEY'));
$events = $tratto->emails->listEvents('email_abc123');
foreach ($events as $event) {
echo "{$event['type']} at {$event['occurredAt']}\n";
}Response
{
"data": [
{
"id": "evt_001",
"type": "sent",
"emailId": "email_abc123",
"occurredAt": "2025-06-30T12:00:05Z"
},
{
"id": "evt_002",
"type": "delivered",
"emailId": "email_abc123",
"occurredAt": "2025-06-30T12:00:10Z"
},
{
"id": "evt_003",
"type": "opened",
"emailId": "email_abc123",
"occurredAt": "2025-06-30T12:05:00Z"
},
{
"id": "evt_004",
"type": "clicked",
"emailId": "email_abc123",
"occurredAt": "2025-06-30T12:06:00Z",
"metadata": {
"url": "https://yourdomain.com/offer?ref=email"
}
}
]
}Polling vs Webhooks
Polling (GET /v1/emails/{id})
When to use:
- Check status on-demand (user views email details in your dashboard)
- Low-volume scenarios
- One-off status checks
Pros:
- Simple to implement
- No need for webhook infrastructure
Cons:
- Not real-time (you poll periodically)
- Rate-limited
- Higher latency
Example:
// Check every 10 seconds
setInterval(async () => {
const response = await fetch('https://api.tratto.email/v1/emails/email_abc123', {
headers: { 'Authorization': 'Bearer tratto_live_...' },
});
const { data } = await response.json();
if (data.status === 'delivered') {
console.log('Email delivered!');
clearInterval();
}
}, 10000);Webhooks (Real-time Push)
When to use:
- Real-time event processing
- High-volume scenarios
- Trigger downstream actions (email automation, analytics, etc.)
Pros:
- Real-time
- No polling overhead
- Efficient
Cons:
- Requires webhook endpoint
- Must verify HMAC signatures
- Handle retries and idempotency
Example:
// Your webhook endpoint receives events
app.post('/webhooks/tratto', (req, res) => {
const event = req.body;
console.log(`Email ${event.emailId} was ${event.type}`);
if (event.type === 'delivered') {
// Trigger automation
}
res.status(200).json({ ok: true });
});See Webhooks for setup.
Tracking Email Delivery
Understanding Bounce Types
Hard Bounce: Permanent delivery failure
- Invalid email address
- Domain doesn't exist
- User doesn't exist
Soft Bounce: Temporary delivery failure
- Mailbox full
- Server temporarily unavailable
- Message too large
Complaint: Recipient marked as spam (don't re-send)
Auto-Unsubscribe
When an email bounces or is complained, Tratto automatically:
- Updates the contact's status to
bouncedorcomplained - Creates a bounce/complaint event
- Sends a webhook event (if configured)
You can then:
- Stop sending to bounced addresses
- Implement re-engagement campaigns for complained addresses
- Monitor bounce rates for domain health
Status Summary API
Get aggregate statistics across multiple emails.
Coming soon: Endpoint to query email statuses by status, date range, or tags.
Next Steps
- Set up real-time webhooks? See Webhooks
- Send emails? Go to Send Email
- Understand bounce/complaint handling? Read Deliverability
Edit this page on GitHub
Last updated on