# Messaging

> The Messaging operations of the Hub REST API (17 operations).

URL: https://docs.synap.live/docs/reference/hub-api/messaging · Generated from Synap-core/backend@c31e261ab (main)

Paths are relative to `/api/hub` on a pod.

## POST /messaging/inbound

Land a provider-agnostic inbound message into the pod

Provider-agnostic inbound sensor door. Resolves-or-creates the EXTERNAL channel and dedup-records the inbound message (folding a subject line into the body and resolving a participant email to an existing entity when no externalId is given), then fires external_message.received. NON-governed (a sensor write). Used by external bridges (Discord, Proton, …).

Request body (`application/json`): MessagingInboundRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `provider` | string | yes |  |
| `text` | string |  |  |
| `messageId` | string | yes |  |
| `externalId` | string |  |  |
| `participantEmail` | string |  |  |
| `participant` | string |  |  |
| `participantExternalId` | string |  |  |
| `subject` | string |  |  |
| `sentAt` | string |  |  |
| `workspaceId` | string |  |  |
| `projectId` | string |  |  |
| `userId` | string |  |  |
| `attachments` | array<object> |  |  |
| `headerMessageId` | string |  |  |
| `inReplyTo` | string |  |  |
| `references` | array<string> |  |  |

Responses:

- `200`: Record result
- `400`: Bad request
- `403`: Forbidden
- `500`: Internal error

## GET /messaging/accounts

List connected messaging accounts for the current user

Responses:

- `200`: List of connected accounts
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## GET /messaging/auth-url

Get hosted auth URL for connecting a new account

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `redirectUrl` | query | string | yes |  |
| `provider` | query | string |  |  |

Responses:

- `200`: Auth URL
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## POST /messaging/accounts/sync

Sync live accounts from the connector into the database

Responses:

- `200`: Sync result
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## GET /messaging/service-config

Get current messaging connector config state (masked)

Responses:

- `200`: Config state

## POST /messaging/service-config

Store Unipile connector credentials in the vault

Request body (`application/json`): MessagingServiceConfigInput

| Field | Type | Required | Description |
|---|---|---|---|
| `dsn` | string | yes |  |
| `apiKey` | string | yes |  |
| `webhookSecret` | string |  |  |

Responses:

- `200`: Stored successfully
- `400`: Missing required fields

## POST /messaging/service-config/migrate

Migrate workspace settings messaging credentials to vault

Responses:

- `200`: Migrated
- `400`: No credentials to migrate

## DELETE /messaging/accounts/{accountId}

Disconnect a messaging account

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `accountId` | path | string | yes |  |

Responses:

- `200`: Account disconnected
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## GET /messaging/conversations

List conversations — entity-linked or browse mode

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `entityId` | query | string |  |  |
| `contactEntityId` | query | string |  |  |

Responses:

- `200`: Conversations
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## POST /messaging/channels/connect

Track or link an external conversation thread

Request body (`application/json`): ConnectChannelRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `entityId` | string |  |  |
| `externalThreadId` | string | yes |  |
| `accountId` | string | yes |  |
| `provider` | string | yes |  |
| `participantName` | string |  |  |

Responses:

- `200`: Channel created, upgraded, or already exists
- `400`: Account not found or not owned by current user
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error

## DELETE /messaging/channels/{channelId}

Unlink an external conversation from a CRM entity

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `channelId` | path | string | yes |  |

Responses:

- `200`: Channel unlinked
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Channel not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error

## POST /messaging/channels/{channelId}/mark-read

Clear unread flag for a linked channel

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `channelId` | path | string | yes |  |

Responses:

- `200`: Marked as read
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Channel not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error

## GET /messaging/linked-unread

All unread conversations linked to entities

Responses:

- `200`: Unread linked conversations
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## GET /messaging/conversations/{threadId}/messages

Get messages in a conversation thread

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `threadId` | path | string | yes |  |
| `accountId` | query | string | yes |  |

Responses:

- `200`: Messages
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Account not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## POST /messaging/conversations/{threadId}/import

Import a conversation thread into the pod as a governed proposal

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `threadId` | path | string | yes |  |

Request body (`application/json`): ImportThreadRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `accountId` | string | yes |  |
| `workspaceId` | string |  |  |
| `sessionId` | string |  |  |

Responses:

- `200`: Import proposal created (or no messages to import)
- `400`: Bad request
- `403`: Access denied
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## POST /messaging/conversations/{threadId}/send

Send a message in a conversation thread

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `threadId` | path | string | yes |  |

Request body (`application/json`): SendMessageRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `accountId` | string | yes |  |
| `body` | string | yes |  |

Responses:

- `200`: Message sent
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
- `503`: Connector not configured

## GET /messaging/channels

List all external channels (DB-backed inbox)

Parameters:

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `provider` | query | string |  |  |
| `unreadOnly` | query | "true" \| "false" |  |  |

Responses:

- `200`: External channels list
- `400`: Bad request — the wrapped door refused the input
- `403`: Forbidden
- `404`: Not found
- `409`: Conflict — a refusal the caller can act on
- `412`: Precondition failed
- `500`: Internal error
