Technical docs

Custom Channel Integration

Overview

Use the private employee message API when an external chat system needs to send user messages to an Alloy AI teammate. This API is intended for custom corporate messengers, internal chat tools, support systems, or middleware that receives messages from another chat provider.

Base URL and auth

Base URL:

```text {api_url}/api/private/employee ```

Authenticate every request with your organization private API key. Send the key in one of these headers:

HeaderFormat
`Authorization``Bearer {private_api_key}`
`api-key``{private_api_key}`

Missing or invalid keys return `401` with `{ "error": "Unauthorized access" }`.

Message identity

Each request must include a `channel` value. Treat it as the stable identifier of your external chat system, for example `corporate-messenger`, `intranet-chat`, or `support-portal`.

For an ongoing chat, also send:

  • `conversation.id`: the chat, room, thread, or dialog ID from your system.
  • `contact.id`: the sender's user ID from your system.

Use the same `channel`, `conversation.id`, and `contact.id` on later requests so Alloy can continue the same chat history.

Send a message

```text POST /api/private/employee/{employee_id}/messages ```

Sends one user message to an AI teammate. By default the request waits for the AI response; set `is_async: true` to queue the run and poll it separately.

`{employee_id}` is the AI teammate ID.

Request body

FieldTypeRequiredNotes
`message`stringyesNon-empty user message text.
`channel`stringyesStable identifier for your external chat system.
`attachments`arraynoDefaults to `[]`. Each item carries base64 file data.
`is_async`booleannoDefaults to `false`. When `true`, returns the run ID immediately with `message: null` and an empty output-attachment array.
`conversation`objectconditionalRequired together with `contact` for a saved chat. Omit both for a one-off request.
`conversation.id`stringyes, when `conversation` is presentChat, room, thread, or dialog ID from your system.
`conversation.thread_id`string or `null`noOptional subthread ID from your system.
`conversation.title`string, empty string, or `null`noOptional display title for the chat.
`contact`objectconditionalRequired together with `conversation` for a saved chat. Omit both for a one-off request.
`contact.id`stringyes, when `contact` is presentSender's user ID from your system.
`contact.first_name`string, empty string, or `null`noSender first name.
`contact.last_name`string, empty string, or `null`noSender last name.
`contact.email`string, empty string, or `null`noSender email. Updates the saved contact channel profile.
`contact.phone`string, empty string, or `null`noSender phone. Updates the saved contact channel profile.

`conversation` and `contact` are all-or-nothing. Sending only one of them returns `400`.

Attachment item

FieldTypeRequiredNotes
`name`stringyesOriginal file name.
`mime_type`stringyesMIME type.
`size`integeryesFile size in bytes, minimum `1`. The server checks the decoded `data` size, not just this declared value.
`data`stringyesBase64 encoded file content.

Attachments are saved only for saved chats, meaning requests that include both `conversation` and `contact`. If decoded file size does not match `size`, the request returns `400`. Attachment count, per-file size, and total size are also enforced against the deployment's chat attachment limits (defaults `10` files, `10 MB` each, `30 MB` total); exceeding them returns `422` with `ATTACHMENT_LIMIT_EXCEEDED`.

Saved chat vs one-off request

Use a saved chat when your external system has a conversation that should remain visible and continue over time:

  • include both `conversation` and `contact`
  • Alloy saves the incoming message
  • Alloy can reuse the same chat on later calls when you send the same identifiers
  • the response includes the saved incoming `messageId`

Use a one-off request when you only need an immediate answer and do not want to save chat history:

  • omit both `conversation` and `contact`
  • Alloy returns an AI response for that single message
  • the response has `messageId: null`

Response

```json { "success": true, "data": { "runId": "workflow-run-id", "message": "AI teammate response", "messageId": "saved-incoming-message-id-or-null", "attachments": [ { "name": "report.pdf", "mime_type": "application/pdf", "size": 4096, "data": "base64-encoded-file-content" } ] } } ```

FieldDescription
`data.runId`ID of the processing run for this request.
`data.message`AI teammate response for synchronous execution; `null` when `is_async: true`.
`data.messageId`ID of the saved incoming message, or `null` for one-off requests.
`data.attachments`Array of workflow output attachments generated by a synchronous run: `{ name, mime_type, size, data }` with base64-encoded file content. Empty for an asynchronous response or when the workflow produced none.

Asynchronous execution and polling

Send `is_async: true` when the caller should not wait for the workflow to finish:

```json { "channel": "corporate-messenger", "message": "Prepare the report", "is_async": true } ```

The initial response contains `data.runId`, `data.message: null`, `data.messageId`, and `data.attachments: []`. Poll:

```text GET /api/private/workflow/run/{run_id} ```

An unfinished run returns:

```json { "success": true, "data": { "runId": "workflow-run-id", "status": "in_progress", "result": null } } ```

When the run is finished, `data.result` contains the run's last output and `data.attachments` contains any generated files using the same `{ name, mime_type, size, data }` base64 shape as the synchronous response. A run that does not exist in the authenticated organization returns `404` with `{ "error": "Workflow run not found" }`.

Errors

StatusBodyMeaning
`400``{ "error": "..." }`Invalid request body or attachment size mismatch.
`401``{ "error": "Unauthorized access" }`Missing or invalid private API key.
`404``{ "error": "AI employee not found" }`The AI teammate ID does not exist in the authenticated organization.
`422``{ "error": "AI employee has no runnable workflow" }`The AI teammate is not ready to process messages.
`422``{ "error": { "code": "ATTACHMENT_LIMIT_EXCEEDED", "reason": "MAX_ATTACHMENT_COUNT" \"MAX_ATTACHMENT_SIZE" \"MAX_TOTAL_ATTACHMENT_SIZE", "message": "...", "files": [...], "limits": {...} } }`Attachments exceed the configured chat attachment limits.

Example: one-off request

```bash curl -X POST "{api_url}/api/private/employee/{employee_id}/messages" -H "Authorization: Bearer {private_api_key}" -H "Content-Type: application/json" -d '{ "channel": "corporate-messenger", "message": "What is our PTO policy?" }' ```

Example: saved chat

```bash curl -X POST "{api_url}/api/private/employee/{employee_id}/messages" -H "Authorization: Bearer {private_api_key}" -H "Content-Type: application/json" -d '{ "channel": "corporate-messenger", "message": "I need help with invoice 123", "conversation": { "id": "room-42", "thread_id": null, "title": "Finance support" }, "contact": { "id": "user-777", "first_name": "Alex", "last_name": "Customer" } }' ```

Example: saved chat with attachment

```bash curl -X POST "{api_url}/api/private/employee/{employee_id}/messages" -H "api-key: {private_api_key}" -H "Content-Type: application/json" -d '{ "channel": "corporate-messenger", "message": "Here is the screenshot", "conversation": { "id": "room-42" }, "contact": { "id": "user-777" }, "attachments": [ { "name": "screenshot.png", "mime_type": "image/png", "size": 20482, "data": "iVBORw0KGgo..." } ] }' ```

List messages for a saved chat

```text GET /api/private/employee/{employee_id}/messages ```

Returns saved messages for a conversation previously used with `POST`.

Query parameters

ParameterTypeRequiredDefaultNotes
`conversation_id`stringyesnoneThe same value you sent as `conversation.id`.
`channel`stringyesnoneThe same value you sent as `channel`.
`page`integerno`1`Minimum `1`.
`limit`integerno`30`Minimum `1`, maximum `50`.

Response

```json { "success": true, "data": [ { "id": "message-id", "conversationId": "conversation-id", "threadId": null, "content": "I need help with invoice 123", "type": "text", "sender": "customer", "status": "sent", "timestamp": 1780000000000 } ], "pagination": { "page": 1, "limit": 30, "total": 1, "totalPages": 1, "has_more": false } } ```

Message objects can include additional fields such as attachments, reply references, reaction state, sender display name, and timestamps.

Errors

StatusBodyMeaning
`400``{ "error": "..." }`Missing or invalid query parameters.
`401``{ "error": "Unauthorized access" }`Missing or invalid private API key.
`404``{ "error": "Conversation not found" }`No saved chat exists for that `channel` and `conversation_id`.

Example

```bash curl "{api_url}/api/private/employee/{employee_id}/messages?channel=corporate-messenger&conversation_id=room-42&page=1&limit=30" -H "api-key: {private_api_key}" ```

Integration pattern

A custom messenger bridge usually does this:

  1. Receive a user message from the external chat system.
  2. Convert the source chat system name to a stable `channel` value.
  3. Convert the external chat ID to `conversation.id`.
  4. Convert the external sender ID and name to `contact`.
  5. Call `POST /api/private/employee/{employee_id}/messages`.
  6. For synchronous execution, send `data.message` back to the external chat system and deliver `data.attachments` when present.
  7. For asynchronous execution, store `data.runId`, poll `GET /api/private/workflow/run/{run_id}` until the run finishes, then deliver its `result` and `attachments`.
  8. Use `GET /api/private/employee/{employee_id}/messages` only when your integration needs to reload saved history.

The employee message API supports synchronous responses and asynchronous polling. It does not register an outbound webhook for custom channels.

Start building your AI team