> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bosonware.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Monitoring email delivery

> Poll for delivery status with backoff, persist message IDs, and build a reconciliation workflow to track BosonMail emails from queued to terminal state.

A successful `sendEmail` response means the BosonMail API accepted your message, not that it reached the recipient. This guide shows you how to interpret statuses, persist message IDs, poll with backoff, and build a background reconciliation job so you always know the final outcome.

## Accepted vs delivered

When `sendEmail` returns `success: true`, the email is typically `"queued"`. It then moves through states such as `"sent"`, `"delivered"`, or terminal states like `"failed"`, `"bounced"`, and `"rejected"`. The exact transition order and meaning of each value is defined by the BosonMail backend and is not fully documented by BosonWare.

> [!WARNING]
>
> The general meaning of status values is described below, but confirm the exact semantics and transitions with BosonWare. Contact [support@bosonware.com](mailto:support@bosonware.com) for authoritative definitions.

| Status | General meaning |
| - | - |
| queued | Accepted by the API, waiting to be processed |
| sent | Handed off to the outgoing mail relay |
| delivered | Confirmed delivery to the recipient's mail server |
| failed | Permanent delivery failure |
| bounced | Rejected by the recipient's server (often address-related) |
| rejected | Blocked or refused before delivery attempt |
| deferred | Temporarily delayed; may retry automatically |

Terminal statuses you can stop polling on are: `delivered`, `failed`, `bounced`, `rejected`. Do not treat `deferred` or `sent` as terminal.

## Persist message IDs

Store the `messageId` from every successful send so you can check status later. A minimal database schema might look like this:

```sql schema.sql theme={null}
CREATE TABLE email_outbox (
  id SERIAL PRIMARY KEY,
  message_id VARCHAR(255) UNIQUE,
  recipient VARCHAR(255) NOT NULL,
  subject VARCHAR(500) NOT NULL,
  submission_status VARCHAR(50) NOT NULL DEFAULT 'queued',
  delivery_status VARCHAR(50),
  last_checked_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT NOW()
);
```

After each send, insert a row:

```typescript persist.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";

const apiKey = process.env.BOSONMAIL_API_KEY;
if (!apiKey) throw new Error("BOSONMAIL_API_KEY is not set");

const client = new BosonMailClient({ apiKey });

async function sendAndRecord(to: string, subject: string, text: string) {
  const result = await client.sendEmail(to, subject, { text });

  if (!result.success) {
    console.error("Send failed:", result.errorMessage);
    return result;
  }

  // Insert into your database here
  // INSERT INTO email_outbox (message_id, recipient, subject, submission_status)
  // VALUES (result.messageId, to, subject, result.status);

  console.log("Recorded message ID:", result.messageId);
  return result;
}
```

## Poll with backoff

The SDK does not poll automatically. Use `getEmailStatus` in a loop with a delay between attempts. Stop when you reach a terminal status or the maximum number of attempts.

```typescript poll-status.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";
import type { EmailSentStatus } from "@bosonware-ent/bosonmail";

const apiKey = process.env.BOSONMAIL_API_KEY;
if (!apiKey) throw new Error("BOSONMAIL_API_KEY is not set");

const client = new BosonMailClient({ apiKey });

const TERMINAL_STATUSES: EmailSentStatus[] = [
  "delivered",
  "failed",
  "bounced",
  "rejected",
];

async function pollEmailStatus(
  messageId: string,
  options: { maxAttempts?: number; intervalMs?: number } = {}
): Promise<{ success: boolean; finalStatus?: EmailSentStatus; error?: string }> {
  const { maxAttempts = 6, intervalMs = 5000 } = options;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const result = await client.getEmailStatus(messageId);

    if (!result.success) {
      // Stop early on authentication or missing email errors
      if (result.errorCode === "unauthorized" || result.errorCode === "not_found") {
        return { success: false, error: result.errorMessage };
      }

      // Retry on transient SDK or network errors
      if (attempt === maxAttempts) {
        return { success: false, error: result.errorMessage };
      }

      await sleep(intervalMs * attempt); // linear backoff
      continue;
    }

    const status = result.data!.status;
    const delivery = result.data!.delivery_status;

    console.log(`Attempt ${attempt}: status=${status}, delivery=${delivery}`);

    if (TERMINAL_STATUSES.includes(status)) {
      return { success: true, finalStatus: status };
    }

    if (attempt < maxAttempts) {
      await sleep(intervalMs);
    }
  }

  return { success: false, error: "Max polling attempts reached without a terminal status" };
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// Example usage
async function main() {
  const outcome = await pollEmailStatus("msg_abc123", { maxAttempts: 6, intervalMs: 5000 });
  console.log(outcome);
}

main();
```

## Background reconciliation job

For production workloads, run a scheduled job that checks all pending messages instead of polling inline after every send.

```typescript reconcile-job.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";

const apiKey = process.env.BOSONMAIL_API_KEY;
if (!apiKey) throw new Error("BOSONMAIL_API_KEY is not set");

const client = new BosonMailClient({ apiKey });

interface PendingEmail {
  id: number;
  message_id: string;
}

async function reconcilePending(emails: PendingEmail[]) {
  for (const email of emails) {
    const result = await client.getEmailStatus(email.message_id);

    if (!result.success) {
      if (result.errorCode === "not_found") {
        // Mark as failed; the message may have expired or the ID was wrong
        await updateEmailStatus(email.id, { delivery_status: "failed", last_checked_at: new Date() });
      }
      continue;
    }

    await updateEmailStatus(email.id, {
      submission_status: result.data!.status,
      delivery_status: result.data!.delivery_status,
      last_checked_at: new Date(),
    });
  }
}

// Placeholder for your ORM or query builder
async function updateEmailStatus(
  id: number,
  fields: { submission_status?: string; delivery_status?: string; last_checked_at?: Date }
) {
  // UPDATE email_outbox SET ... WHERE id = $1
  console.log("Updated email", id, fields);
}
```

Run this on a cron schedule (for example, every 5 minutes) or with `setInterval` in a long-running worker process.

## Error codes to watch for

| errorCode | Meaning | Action |
| - | - | - |
| unauthorized | Authentication missing or invalid | Check your API key and scopes |
| not\_found | Email could not be found | The message ID may be wrong or expired |
| invalid\_id | Supplied message ID invalid | Verify the ID format |
| request\_failed | Network request failed or timed out | Retry with backoff |
| invalid\_response | API returned incomplete status response | Report to BosonWare support |
| http\_\<status> | API returned an HTTP error with no code | Check status and retry if transient |

## Do not poll from browsers

`getEmailStatus` requires your BosonMail API key. Never call it from browser code. Run polling and reconciliation jobs on your server or in a secure background worker.

## Delivery lifecycle diagram

```mermaid theme={null}
sequenceDiagram
    participant App as Your app
    participant SDK as BosonMail SDK
    participant API as BosonMail API
    participant DB as Your database

    App->>SDK: sendEmail(to, subject, body)
    SDK->>API: POST /emails/send
    API-->>SDK: { success: true, messageId }
    SDK-->>App: { success: true, messageId, status: "queued" }
    App->>DB: INSERT message_id, status "queued"

    loop Poll every 5s, up to 6 attempts
        App->>SDK: getEmailStatus(messageId)
        SDK->>API: GET /emails/:id/status
        API-->>SDK: { success: true, data: { status, delivery_status } }
        SDK-->>App: EmailStatusResult
        App->>DB: UPDATE status, delivery_status
    end
```

## Next steps

> [card] [Production best practices](https://docs.bosonware.com/guides/production-best-practices.md)
>
> Secure keys, handle timeouts, and manage retries in production environments.

> [card] [SDK reference: Email status](https://docs.bosonware.com/sdk/email-status.md)
>
> Read the concise SDK reference for getEmailStatus and status fields.

> [card] [Error handling](https://docs.bosonware.com/sdk/error-handling.md)
>
> Understand all SDK error shapes and how to respond to each.

> [card] [First email](https://docs.bosonware.com/guides/first-email.md)
>
> Return to the basics: send your first email and check its status.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.