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

# Message status

> Look up the delivery status of any email with the platform API: list the message log, or get one message's attempts and events by job ID.

The message log on the platform API records what happened to every email your organization sent:
through the email send API, the SMTP relay and campaigns.

<Info>
  **Before you start**, you need an [API key](/developers/api-keys) with **Messages** set to
  **Read Message Log** (`messages:read`).
</Info>

## How the log works

* **A message appears once a receiving server answers.** A message you sent seconds ago, or one
  that never reached a mail server, isn't in the log yet.
* **Records are kept for 90 days.** Every response includes `retentionDays` so your code doesn't
  have to hard-code it.
* **The subject isn't recorded.** Store it on your side against the `jobId` if you need it.

## Statuses

| Status      | Meaning                                                                                                                                                                                |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `delivered` | The receiving mail server accepted the message. This doesn't mean it reached the inbox rather than spam                                                                                |
| `deferred`  | The receiving server refused it temporarily. Brudcast retries up to 3 times: after 5 seconds, 30 seconds and 5 minutes                                                                 |
| `bounced`   | The receiving server rejected it. Check `smtpCode`: a `5xx` code is permanent, and the address is added to your [suppression list](/deliverability/bounces-complaints-and-suppression) |

Engagement events are recorded separately:

| Type         | Meaning                                               |
| ------------ | ----------------------------------------------------- |
| `opened`     | The recipient's mail client loaded the tracking pixel |
| `clicked`    | The recipient followed a tracked link                 |
| `complained` | The recipient marked the message as spam              |

Opens and clicks are only recorded when the sending domain has tracking turned on, on its
**Settings** tab in **Channels > Email**. Tracking is off on a new domain, and it only works on a
message's HTML part.

## Get one message

```
GET https://core-service.prod.brudcast.com/api/v1/user/messages/{jobId}
```

<ParamField path="jobId" type="string" required>
  The `jobId` returned by the email send API.
</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl https://core-service.prod.brudcast.com/api/v1/user/messages/3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10 \
    -H "X-API-Key: $BRUDCAST_API_KEY"
  ```

  ```javascript Node.js theme={"system"}
  const jobId = "3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10";
  const response = await fetch(
    `https://core-service.prod.brudcast.com/api/v1/user/messages/${jobId}`,
    { headers: { "X-API-Key": process.env.BRUDCAST_API_KEY } },
  );

  const { data } = await response.json();
  console.log(data.deliveryAttempts);
  ```

  ```python Python theme={"system"}
  import os
  import requests

  job_id = "3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10"
  response = requests.get(
      f"https://core-service.prod.brudcast.com/api/v1/user/messages/{job_id}",
      headers={"X-API-Key": os.environ["BRUDCAST_API_KEY"]},
  )

  print(response.json()["data"]["deliveryAttempts"])
  ```

  ```php PHP theme={"system"}
  <?php
  $jobId = "3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10";
  $ch = curl_init("https://core-service.prod.brudcast.com/api/v1/user/messages/" . $jobId);
  curl_setopt_array($ch, [
      CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("BRUDCAST_API_KEY")],
      CURLOPT_RETURNTRANSFER => true,
  ]);

  $body = json_decode(curl_exec($ch), true);
  print_r($body["data"]["deliveryAttempts"]);
  ```
</CodeGroup>

```json theme={"system"}
{
  "success": true,
  "message": "Message fetched successfully",
  "data": {
    "jobId": "3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10",
    "messageId": "<3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10@…>",
    "senderAddress": "billing@mail.example.com",
    "campaignId": null,
    "contactId": null,
    "recipients": ["jane@example.com"],
    "deliveryAttempts": [
      {
        "recipient": "jane@example.com",
        "status": "delivered",
        "smtpCode": 250,
        "smtpMessage": "2.0.0 OK",
        "mxHost": "mx.example.com",
        "outboundIp": "203.0.113.10",
        "retryCount": 0,
        "attemptedAt": "2026-09-11T09:30:12.000Z"
      }
    ],
    "engagementEvents": [
      {
        "type": "opened",
        "recipient": "jane@example.com",
        "userAgent": "Mozilla/5.0",
        "remoteIp": "198.51.100.7",
        "occurredAt": "2026-09-11T09:41:55.000Z"
      }
    ],
    "retentionDays": 90
  }
}
```

<ResponseField name="data" type="object">
  <Expandable title="properties" defaultOpen>
    <ResponseField name="jobId" type="string">The job ID from the send.</ResponseField>
    <ResponseField name="messageId" type="string">The `Message-ID` header the message went out with.</ResponseField>
    <ResponseField name="senderAddress" type="string">The from address.</ResponseField>
    <ResponseField name="campaignId" type="string | null">The campaign, or `null` for API and SMTP mail.</ResponseField>
    <ResponseField name="contactId" type="string | null">The contact the message was addressed to, when the send was contact-driven.</ResponseField>
    <ResponseField name="recipients" type="string[]">Every recipient the attempts cover.</ResponseField>

    <ResponseField name="deliveryAttempts" type="object[]">
      Each attempt, oldest first, up to 500: `recipient`, `status`, `smtpCode`, `smtpMessage`
      (the receiving server's reply), `mxHost`, `outboundIp`, `retryCount` and `attemptedAt`.
    </ResponseField>

    <ResponseField name="engagementEvents" type="object[]">
      Opens, clicks and complaints, oldest first, up to 500: `type`, `recipient`, `userAgent`,
      `remoteIp` and `occurredAt`.
    </ResponseField>

    <ResponseField name="retentionDays" type="number">How many days of history are kept.</ResponseField>
  </Expandable>
</ResponseField>

## List messages

```
GET https://core-service.prod.brudcast.com/api/v1/user/messages
```

The list has one row per recipient, showing that recipient's latest outcome, newest first. It uses
[cursor pagination](/developers/response-format#cursor-pagination).

<ParamField query="from" type="string">
  Start of the date range, as ISO 8601 in UTC. Defaults to 7 days ago. A date earlier than the
  retention window is moved forward to its start.
</ParamField>

<ParamField query="to" type="string">
  End of the date range, as ISO 8601 in UTC. Defaults to now.
</ParamField>

<ParamField query="status" type="string">
  `delivered`, `bounced` or `deferred`.
</ParamField>

<ParamField query="recipient" type="string">
  An exact email address.
</ParamField>

<ParamField query="campaignId" type="string">
  Only messages from this campaign.
</ParamField>

<ParamField query="limit" type="integer">
  Rows per page, from 1 to 100.
</ParamField>

<ParamField query="cursor" type="string">
  The `nextCursor` from the previous page.
</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -G https://core-service.prod.brudcast.com/api/v1/user/messages \
    -H "X-API-Key: $BRUDCAST_API_KEY" \
    --data-urlencode "status=bounced" \
    --data-urlencode "from=2026-09-01T00:00:00Z" \
    --data-urlencode "limit=100"
  ```

  ```javascript Node.js theme={"system"}
  const params = new URLSearchParams({
    status: "bounced",
    from: "2026-09-01T00:00:00Z",
    limit: "100",
  });

  const response = await fetch(
    `https://core-service.prod.brudcast.com/api/v1/user/messages?${params}`,
    { headers: { "X-API-Key": process.env.BRUDCAST_API_KEY } },
  );

  const { data, meta } = await response.json();
  console.log(data.length, meta.nextCursor);
  ```

  ```python Python theme={"system"}
  import os
  import requests

  response = requests.get(
      "https://core-service.prod.brudcast.com/api/v1/user/messages",
      headers={"X-API-Key": os.environ["BRUDCAST_API_KEY"]},
      params={"status": "bounced", "from": "2026-09-01T00:00:00Z", "limit": 100},
  )

  body = response.json()
  print(len(body["data"]), body["meta"]["nextCursor"])
  ```

  ```php PHP theme={"system"}
  <?php
  $query = http_build_query([
      "status" => "bounced",
      "from" => "2026-09-01T00:00:00Z",
      "limit" => 100,
  ]);

  $ch = curl_init("https://core-service.prod.brudcast.com/api/v1/user/messages?" . $query);
  curl_setopt_array($ch, [
      CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("BRUDCAST_API_KEY")],
      CURLOPT_RETURNTRANSFER => true,
  ]);

  $body = json_decode(curl_exec($ch), true);
  echo count($body["data"]), " ", $body["meta"]["nextCursor"];
  ```
</CodeGroup>

```json theme={"system"}
{
  "success": true,
  "message": "Messages fetched successfully",
  "data": [
    {
      "jobId": "3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10",
      "messageId": "<3f1c2a9e-8b7d-4c6e-9a51-2d0e7f4b6c10@…>",
      "senderAddress": "billing@mail.example.com",
      "recipient": "jane@example.com",
      "status": "bounced",
      "smtpCode": 550,
      "smtpMessage": "5.1.1 User unknown",
      "attempts": 1,
      "campaignId": null,
      "lastAttemptedAt": "2026-09-10T16:02:41.000Z"
    }
  ],
  "meta": {
    "nextCursor": "MDE5MmY4YzQtNzE5",
    "hasMore": true,
    "retentionDays": 90
  }
}
```

Keep requesting with `cursor` set to `meta.nextCursor` until `hasMore` is `false`.

## Errors

| Status | Meaning                                    | What to do                                                                                              |
| ------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `403`  | The key lacks `messages:read`              | Add **Read Message Log** to the key's permissions                                                       |
| `404`  | No message with that job ID yet            | Normal for a few seconds after sending. Retry shortly. A brief outage of the log can also show as `404` |
| `503`  | The message log is temporarily unavailable | Retry with backoff                                                                                      |

<Tip>
  Polling works for occasional checks. To react to every delivery, bounce or complaint as it
  happens, subscribe to the `email.*` events with
  [webhooks](/developers/webhooks/events-and-payloads). Read that page first: Brudcast delivers
  only two event types today, and the email events aren't among them.
</Tip>

## Related

<Columns cols={2}>
  <Card title="Webhook events and payloads" icon="webhook" href="/developers/webhooks/events-and-payloads">
    Which events are delivered, and the API calls that replace the rest.
  </Card>

  <Card title="Bounces and suppression" icon="shield-check" href="/deliverability/bounces-complaints-and-suppression">
    What happens to an address after a bounce or complaint.
  </Card>

  <Card title="Email send API" icon="send" href="/developers/sending/http-api">
    Where the `jobId` you look up here comes from.
  </Card>

  <Card title="Response format" icon="braces" href="/developers/response-format">
    Cursor pagination, for reading the whole log.
  </Card>
</Columns>
