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, complained

An email sent without scheduledAt starts at queued. Opens, clicks and unsubscribes are events, not statuses: they never change the status.

Status Definitions

StatusDescription
scheduledEmail is scheduled for future delivery (you set scheduledAt). It moves to queued when its time comes
queuedEmail is in the queue, will be sent shortly (minutes)
sendingTratto has picked the email up and is handing it to the mail provider. It lasts seconds
sentEmail was handed to the mail server, but not yet confirmed by the recipient's server
deliveredThe recipient's mail server confirmed receipt
bouncedThe recipient's mail server rejected the email
complainedThe recipient marked the email as spam
failedTratto 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
skippedTratto did not send the email because the recipient unsubscribed or is on the suppression list. Nothing was sent
canceledYou 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, complained

Get 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 TypeDescription
sentEmail was accepted by the mail server
deliveredMail server confirmed delivery to recipient's mailbox
openedRecipient opened the email (pixel tracking)
clickedRecipient clicked a link in the email
bouncedMail server rejected delivery (hard or soft bounce)
complainedRecipient marked email as spam
unsubscribedRecipient clicked unsubscribe link
failedThe 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:

  1. Updates the contact's status to bounced or complained
  2. Creates a bounce/complaint event
  3. 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


Edit this page on GitHub

Last updated on