Skip to main content
Every email sent through Ark goes through a series of states from initial submission to final delivery (or failure). Understanding this lifecycle helps you build robust email handling in your application.

Lifecycle Stages

                                    ┌──────────┐
                               ┌───▶│ Bounced  │ (NDR received after delivery)
                               │    └──────────┘
                               │         │
┌─────────┐     ┌──────────┐───┘         ▼
│ Pending │────▶│   Sent   │      ┌─────────────────┐
└─────────┘     └──────────┘      │ Suppression List│
     │                            └─────────────────┘
     │                                   ▲
     ├─────────────────────────▶ ┌──────────┐
     │                           │ HardFail │ (immediate suppression)
     │                           └──────────┘
     │                                   ▲
     ├──────────────────────────▶┌──────┐
     │                           │ Held │ (manual review required)
     │                           └──────┘

┌──────────┐  retry success   ┌──────┐
│ SoftFail │─────────────────▶│ Sent │
└──────────┘                  └──────┘

     │ max retries exhausted (18 attempts)

┌──────────┐
│ HardFail │ (suppression: "too many soft fails")
└──────────┘

Status Definitions

pending

The email has been accepted and is waiting to be processed. This is the initial state after a successful API call.
{
  "id": "msg_abc123",
  "status": "pending",
  "timestampIso": "2024-01-15T10:00:00Z"
}

sent

The email has been transmitted to the recipient’s mail server. The recipient’s server accepted the message.
{
  "id": "msg_abc123",
  "status": "sent",
  "timestampIso": "2024-01-15T10:00:02Z"
}

softfail

A temporary delivery failure occurred. Ark will automatically retry with exponential backoff up to 18 times over approximately 48 hours. If all retries fail, the message transitions to hardfail and the recipient is added to the suppression list.
{
  "id": "msg_abc123",
  "status": "softfail",
  "timestampIso": "2024-01-15T10:00:02Z"
}

hardfail

A permanent delivery failure occurred. The email will not be retried and the recipient is immediately added to the suppression list. Hard failures occur in two scenarios:
  1. Immediate rejection: The recipient’s mail server permanently rejected the email (SMTP 5xx error). Common reasons include non-existent mailbox, invalid domain, or policy rejection.
  2. Retry exhaustion: A soft failure that couldn’t be resolved after 18 retry attempts over ~48 hours.
{
  "id": "msg_abc123",
  "status": "hardfail",
  "timestampIso": "2024-01-15T10:00:02Z",
  "details": "550 5.1.1 The email account does not exist"
}
Immediate suppression on hard fail. When an email hard fails, the recipient is immediately added to the suppression list with reason hard fail. This prevents future delivery attempts to invalid addresses and protects your sender reputation.

bounced

A bounce notification (NDR - Non-Delivery Report) was received after the email was initially accepted. This happens when the recipient’s server accepts the message but later determines it cannot be delivered. The recipient is immediately added to the suppression list when a bounce is received.
{
  "id": "msg_abc123",
  "status": "bounced",
  "timestampIso": "2024-01-15T10:00:02Z",
  "bounce_details": {
    "original_recipient": "[email protected]",
    "diagnostic_code": "smtp; 550 User not found"
  }
}

held

The email is held for manual review (e.g., suspicious content detected).
{
  "id": "msg_abc123",
  "status": "held",
  "timestampIso": "2024-01-15T10:00:00Z"
}

Bounce Types

Understanding bounce types helps you diagnose delivery issues and maintain list hygiene.
TypeDescriptionArk BehaviorSuppression
Hard bouncePermanent failure - address invalidNo retry, immediate failureImmediate (hard fail)
Soft bounceTemporary failure - might succeed laterRetry up to 18 times over ~48hAfter retries exhausted (too many soft fails)
BlockRejected by content/spam filterNo retryImmediate (hard fail)

Hard Bounce Examples

SMTP CodeMeaningExample Message
550Mailbox unavailable”User unknown”, “No such user”
551User not local”User not local; please try another path”
552Message too large”Message exceeds fixed maximum message size”
553Mailbox name invalid”Mailbox name not allowed”
554Transaction failed”Delivery not authorized, message refused”

Soft Bounce Examples

SMTP CodeMeaningExample Message
421Service unavailable”Try again later”
450Mailbox busy”Mailbox temporarily unavailable”
451Local error”Requested action aborted: local error in processing”
452Insufficient storage”Mailbox full”, “Quota exceeded”
Soft bounces convert to hard fails after 18 retries. When a soft bounce exhausts all retry attempts, the message status changes to HardFail and the recipient is added to the suppression list with reason too many soft fails. This prevents indefinite retry attempts to persistently unreachable addresses.

Tracking Email Status

Via API

Poll the email status:
import time
from ark import Ark

client = Ark()

def wait_for_final_status(email_id, max_wait_seconds=30):
    start_time = time.time()
    terminal_statuses = ['sent', 'hardfail', 'bounced', 'held']

    while time.time() - start_time < max_wait_seconds:
        email = client.emails.retrieve(email_id)
        status = email.data.status

        if status in terminal_statuses:
            return email.data

        time.sleep(1)

    raise TimeoutError("Timeout waiting for final status")

# Usage
result = wait_for_final_status("msg_abc123")
print(f"Final status: {result.status}")
Receive real-time status updates:
from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhooks/ark")
async def handle_webhook(request: Request):
    event = await request.json()

    if event["event"] == "MessageSent":
        print(f"Email {event['payload']['message']['token']} sent")
        await update_email_status(event["payload"]["message"]["token"], "sent")

    elif event["event"] == "MessageBounced":
        print(f"Email bounced: {event['payload']}")
        await handle_bounce(event["payload"])

    elif event["event"] == "MessageDelayed":
        print(f"Email delayed, will retry")

    elif event["event"] == "MessageDeliveryFailed":
        print(f"Email delivery failed: {event['payload']}")

    return {"status": "ok"}

Retry Behavior

For temporary failures (softfail), Ark automatically retries using exponential backoff. The delay between retries follows the formula: (1.3^attempts) × 5 minutes.
AttemptApproximate DelayCumulative Time
15 minutes5 min
26.5 minutes12 min
38.5 minutes20 min
514 minutes45 min
832 minutes1.5 hours
1069 minutes3.5 hours
122.4 hours7 hours
155.6 hours18 hours
1814 hours~48 hours
After 18 failed attempts (configurable), the email is marked as hardfail and the recipient is automatically added to the suppression list with reason “too many soft fails”.
The maximum delivery attempts can be configured per installation. The default is 18 attempts, which spans approximately 48 hours of retry attempts.

State Transitions

FromToTriggerSuppression Action
-pendingAPI call accepted-
pendingsentTransmitted to recipient server-
pendingsoftfailTemporary delivery failure (4xx SMTP)-
pendinghardfailPermanent delivery failure (5xx SMTP)Automatic: hard fail
pendingheldHeld for review (suppression list, spam, etc.)-
softfailsentRetry succeeded-
softfailhardfailMax retries (18) exceededAutomatic: too many soft fails
sentbouncedNDR received after deliveryNone (handle via webhook)
sent-Terminal state (success)Removed if previously suppressed
hardfail-Terminal stateAlready suppressed
bounced-Terminal state-
held-Requires manual action-
Hard failures result in automatic suppression. Whether the failure is immediate (SMTP 5xx error) or after retry exhaustion, the recipient is automatically added to the suppression list. Future emails to this address will be held with status “Recipient is on the suppression list” until manually removed.
Bounced status does NOT auto-suppress. When an NDR is received and the message status changes to bounced, the recipient is NOT automatically added to the suppression list. Handle the MessageBounced webhook to manually suppress if desired.
Successful delivery clears suppression. If you manually retry a message to a suppressed address and it succeeds, the address is automatically removed from the suppression list.

Deliveries Timeline

Get the delivery attempts for an email using the deliveries endpoint:
deliveries = client.emails.deliveries("msg_abc123")

for delivery in deliveries.data:
    print(f"{delivery.status}: {delivery.timestamp}")

API Reference

View email deliveries endpoint