# Inspect Messaging Rosters, Conversations, and Email Evidence

Read job-scoped messaging membership, conversation response evidence, and privacy-safe notification email delivery observations.

Source: https://www.opentrain.ai/docs/developers/api-reference/messages/inspection/

Use these read-only operations when an agent needs to understand a job's
messaging surface without changing membership, creating conversations, or
sending mail. Every operation requires `messages:read`, a claimed account, the
messaging surface enabled for the account, and the relevant conversation or
job access.

The same operations are available through the Public API, the OpenTrain CLI,
the official SDK, and the product MCP server. The API's OpenAPI document at
[`/api/public/v1/openapi.json`](https://app.opentrain.ai/api/public/v1/openapi.json)
is the exact contract of record.

> Warning
>
> These reads never add, remove, merge, repair, or move conversations or
> members. A membership preflight is not authorization to execute a later
> write. Proposal DMs and post-hire Job DMs are separate conversations.

## Read a conversation roster

`GET /api/public/v1/messaging/management/messaging_management.members.list`

Pass `conversationId`. The response is a page of active members by default.
Set `includeRemoved=true` to include historical membership rows. Set
`includeWorkEmail=true` only when you hold `participant_work_email:read`;
managed work addresses are returned only for that explicitly authorized read,
and personal signup addresses are never returned.

```bash
curl -sS \
  "https://app.opentrain.ai/api/public/v1/messaging/management/messaging_management.members.list?conversationId=<CONVERSATION_ID>&limit=50" \
  -H "Authorization: Bearer $OT_API_TOKEN"

opentrain messages members list \
  --conversation-id <CONVERSATION_ID> --limit 50 --json
```

Follow `nextCursor` while `hasMore` is `true`. The response identifies the
conversation context, membership source, role, contract relationship, and
privacy-safe display label. It does not return a personal email address.

## Inspect one member or eligibility

`GET /api/public/v1/messaging/management/messaging_management.members.get`

Pass exactly one selector: `userId`, `contractId`, or a managed `workEmail`.
An unknown or non-member selector returns `membership: "none"`; it is not a
directory search. Channel responses may include an observational `preflight`
with add/remove eligibility. The preflight never reserves permission and does
not perform a membership write.

```bash
opentrain messages members get \
  --conversation-id <CONVERSATION_ID> --contract-id <CONTRACT_ID> --json
```

## List a job's conversations

`GET /api/public/v1/messaging/management/messaging_management.conversations.list`

Pass `jobId` and optionally filter by `contextType` (`proposal_dm`, `job_dm`,
or `channel`), `participantUserId`, `updatedAfter`, or
`needsEmployerReply`. `updatedAfter` filters conversation-row changes; it is
not a complete message-edit change feed. Returned rows include participants,
contract state, latest activity, and bounded pointers for the latest worker
message and employer response. `needsEmployerReply` is organization-level
response evidence, not proof that an issue is resolved.

```bash
curl -sS \
  "https://app.opentrain.ai/api/public/v1/messaging/management/messaging_management.conversations.list?jobId=<JOB_ID>&contextType=job_dm&limit=50" \
  -H "Authorization: Bearer $OT_API_TOKEN"

opentrain messages conversations list \
  --job-id <JOB_ID> --context job_dm --limit 50 --json
```

Follow `nextCursor` until it is `null`. Compare the returned snapshot marker
with a new read before acting; a freshness marker is an observation, not an
atomic send precondition.

## Audit notification email observations

`GET /api/public/v1/messages/email-audit`

Provide one of these mutually exclusive selectors:

- `jobId` plus `messageIds` (one to 50 IDs);
- `jobId` plus `conversationId`, `since`, and `until` (a range of at most 31
  days); or
- `jobId` plus `milestoneId`, optionally narrowed by `recipientUserId`.

Use `cursor` and `limit` (1–100) for paging. The result is an observation of
stored notification decisions and the delivery ledger. It never sends or
resends an email.

```bash
curl -sS \
  "https://app.opentrain.ai/api/public/v1/messages/email-audit?jobId=<JOB_ID>&messageIds=<MESSAGE_ID>" \
  -H "Authorization: Bearer $OT_API_TOKEN"

opentrain messages email-audit \
  --job-id <JOB_ID> --message-ids <MESSAGE_ID> --json
```

Interpret the status conservatively:

- `SENT` means provider acceptance, not recipient delivery.
- `DELIVERED` requires a primary-recipient delivery event.
- A later complaint is separate evidence and does not erase delivery.
- `UNKNOWN` and `unobservedMessageIds` mean the durable correlation is
  missing; they are not proof that a send failed or succeeded.
- BCC counts describe the provider envelope. They do not prove delivery to a
  third-party recipient.

The response is privacy-safe: it exposes masked recipient labels and bounded
delivery state, never raw addresses, provider payloads, signed URLs, or
credentials.

## MCP and SDK names

The stdio and hosted MCP server expose the same read contracts as:

- `opentrain_list_messaging_members`
- `opentrain_get_messaging_member`
- `opentrain_list_job_conversations`
- `opentrain_audit_notification_emails`

The SDK methods are `listManagedMessagingMembers`,
`getManagedMessagingMember`, `listJobConversations`, and
`auditNotificationEmails`. Use `response_format: "summary"` or
`response_format: "json"` on MCP reads; structured content remains the
validated result in either mode.

MCP clients validate closed response schemas before returning data. SDK clients
expose the same typed methods and response models, but callers should still
treat a response as untrusted input at their application boundary. All four
operations are read-only and require the matching server release.
