Custom application Bridge API
HTTP NDJSON bridge endpoints, media transfer, and message fields.
An external runner initiates every connection to Mopheus. Outbound deliveries arrive over a continuously flushed HTTP NDJSON response; inbound messages, assets, and delivery results use separate HTTP requests. Mopheus does not call the runner back. Paths below are relative to the Mopheus API origin.
Authentication and common format
| Item | Contract |
|---|---|
| Bridge base path | /api/v1/channel/bridge |
| Version | version: 1 in events and inbound messages; X-Bridge-Protocol-Version: 1 on the stream |
| Credential | Authorization: Bearer mcb_<installation-uuid>_<secret>; plaintext is returned only on create or rotation |
| JSON response | Success: {"success":true,"data":{...}}; error: {"success":false,"code":"...","error":"...","requestId":"..."}. Optional keys may be absent |
| IDs and time | Internal IDs are UUIDs; platform IDs remain their original strings; timestamps use RFC 3339 |
Bridge tokens authorize only their installation. Member login and workspace permissions are required for management endpoints. HTTP 400 means invalid input, 401 invalid credential, 403 disabled feature or insufficient member permission, 404 inaccessible resource, 409 conflicting stream or stale delivery attempt, and 5xx server failure. Handle code and HTTP status, not the wording of error. Use HTTPS in production.
Installation management
These member-authenticated endpoints are under /api/v1/channel and require the standard workspace request header.
| Method and path | Request | Successful data |
|---|---|---|
POST /bridge/installations | {"name":"Example","platformKey":"example","assigneeType":1,"assigneeId":"<agent-uuid>","externalBotId":"bot-1"} | {"installation":{...}} |
POST /bridge/installations/{installationId}/credentials | Empty body | {"bridgeToken":"mcb_..."}; first call generates a token, later calls replace it and end the old stream |
DELETE /bridge/installations/{installationId}/credentials/{credentialId} | No body; V1 credentialId equals the installation ID | null; revokes the current token and closes the stream |
platformKey matches ^[a-z0-9][a-z0-9_-]{1,49}$. name is at most 100 bytes; externalBotId at most 255 bytes. assigneeType is 1 for an Agent or 3 for a Team, and assigneeId must belong to the workspace. Creation does not issue a token; generate it on the installation detail page when configuring the runner. Store the one-time token securely.
Outbound NDJSON stream
GET /api/v1/channel/bridge/stream
Authorization: Bearer mcb_...
Accept: application/x-ndjson
X-Bridge-Protocol-Version: 1
X-Bridge-Runner-Id: example-runner
X-Bridge-Version: example/1.0.0
X-Bridge-Platform-Key: example
X-Bridge-Inbound-Capabilities: text,image,file,audio,video,reply,thread,mention
X-Bridge-Outbound-Capabilities: text,image,file,audio,video,reply,thread,mentionHTTP 200 returns application/x-ndjson; charset=utf-8. Parse each line as an independent JSON object and ignore empty lines. The response stays open; hello arrives first, followed by heartbeat (nominally every 20 seconds) and delivery. Only one stream per installation is allowed; another returns 409. Disable proxy buffering and reconnect with exponential backoff.
{"version":1,"type":"hello","id":"<uuid>","sentAt":"2026-10-14T10:00:00Z","payload":{"connectionId":"<uuid>","heartbeatSeconds":20,"deliveryLeaseSeconds":60,"acceptedCapabilities":{"inbound":["text","file"],"outbound":["text","file"]}}}
{"version":1,"type":"heartbeat","id":"<uuid>","sentAt":"2026-10-14T10:00:20Z","payload":{"connectionId":"<uuid>"}}
{"version":1,"type":"delivery","id":"<uuid>","sentAt":"2026-10-14T10:00:22Z","envelope":{"deliveryId":"<uuid>","attemptId":"<uuid>","attempt":1,"leaseExpiresAt":"2026-10-14T10:01:22Z","completedParts":[],"target":{"type":"chat","conversationId":"group-1","conversationType":"group","threadId":"topic-1"},"replyTo":{"messageId":"platform-parent-1"},"content":{"text":"Report attached","format":"plain","parts":[{"partId":"text:0","type":"text","text":"Report attached"},{"partId":"file:0","type":"attachment","attachmentId":"<uuid>","kind":"file","filename":"report.pdf","mimeType":"application/pdf","sizeBytes":20480,"downloadPath":"/api/v1/channel/bridge/deliveries/<deliveryId>/assets/<attachmentId>"}]}}}These are three separate lines, not a JSON array. deliveryId is stable across retries; attemptId changes on each lease. target.type is the existing outbound target enum, commonly chat; target fields appear when applicable. V1 stores only whole-delivery status, so the runner must durably record each deliveryId + partId -> platformMessageId before acknowledging. completedParts does not replace that local record. Prefer the platform idempotency key deliveryId:partId.
Inbound messages
POST /api/v1/channel/bridge/messages accepts JSON up to 256 KiB:
{"version":1,"type":"message.inbound","id":"<request-uuid>","sentAt":"2026-10-14T10:02:00Z","payload":{"eventId":"platform-event-1","messageId":"platform-message-1","occurredAt":"2026-10-14T10:01:59Z","conversation":{"id":"group-1","type":"group","title":"Engineering","threadId":"topic-1"},"sender":{"id":"user-1","kind":"user","stableId":"account-1","displayName":"Alice"},"recipient":{"botId":"bot-1","addressedToBot":true},"replyTo":{"messageId":"platform-parent-1","rootMessageId":"platform-root-1"},"content":{"kind":"mixed","text":"See report","parts":[{"type":"text","text":"See report"},{"type":"mention","id":"user-2","label":"@Bob"},{"type":"attachment","assetId":"<uploaded-asset-uuid>","kind":"file"}]},"extensions":{"platformSpecific":"optional"}}}conversation.type is direct | group; sender.kind is user | bot | system; content.kind is text | image | file | audio | video | mixed | unknown. unknown requires readable text. Ordered parts can be text | mention | attachment | link | code | emoji | quote. An attachment references a ready asset uploaded under the same installation and messageId. Text is limited to 64 KiB, parts to 100, attachments to 20. Both eventId and messageId are required. Retry with the same messageId and unchanged content; edits and recalls have no V1 endpoint.
Success example: {"success":true,"data":{"messageId":"platform-message-1","status":"ingested","chatSessionId":"<uuid>","mediaState":"pending"}}.
data.status | Runner action |
|---|---|
ingested | Terminal: stored, but agent processing may still be pending |
duplicate | Terminal: this installation already processed the messageId |
dropped | Terminal: channel rule ignored it; inspect reason |
rejected | Terminal: invalid event age or recipient |
retry | Keep local event and retry after retryAfterMs |
Retain and retry the original event on network or 5xx failures. Events older than seven days or more than five minutes in the future are rejected. Bot/system senders are dropped to avoid loops. V1 deduplicates by installation and messageId only; it cannot detect content changes under the same ID.
Images and attachments
Download and decrypt platform media in the runner. Reserve, upload raw bytes, then send the message referencing the asset:
POST /api/v1/channel/bridge/assets
Content-Type: application/json
{"messageId":"platform-message-1","sourceKey":"file:0","kind":"file","filename":"report.pdf","mimeType":"application/pdf","sizeBytes":20480,"sha256":"<64-character-hex-SHA256>"}Successful data: {"assetId":"<uuid>","state":"upload","uploadPath":"/api/v1/channel/bridge/assets/<uuid>"}. ready means no PUT is needed. Keep sourceKey stable on retries. Files are limited to 25 MiB; filenames cannot contain paths. V1 validates the reservation size and digest format, but does not persist that digest for cross-request comparison.
PUT /api/v1/channel/bridge/assets/<assetId>
Content-Type: application/pdf
X-Bridge-Filename: report.pdf
X-Content-SHA256: <SHA256-of-PUT-bytes>
<raw bytes>Successful data: {"state":"ready"}. An upload already in progress returns 409; repeating a completed PUT returns ready. V1 has no chunking or Range resume. Submit the inbound message only after every asset is ready.
For outbound attachments, GET the delivery part's downloadPath with X-Bridge-Attempt-Id: <attemptId> and the Bridge Bearer token. The response is raw application/octet-stream, not JSON. Only the current leased attempt may download an attachment belonging to its message.
Delivery result and renewal
POST /api/v1/channel/bridge/deliveries/<deliveryId>/result
Content-Type: application/json
{"attemptId":"<uuid>","status":"sent","parts":[{"partId":"text:0","platformMessageId":"platform-1"},{"partId":"file:0","platformMessageId":"platform-2"}]}sent requires every delivery part, each with a unique known partId and nonempty real platformMessageId. The response data contains deliveryId, attemptId, status: "sent", and possibly completedParts. A repeated result for the same successful attempt returns sent. A failure body uses status: "failed" and may include category, platformCode, message, retryAfterMs, and parts. Retryable categories are rate_limited | transient | unknown; a retry returns status: "pending". Other failures or exhausted attempts return failed.
POST /api/v1/channel/bridge/deliveries/<deliveryId>/renew with {"attemptId":"<uuid>"} returns {"leaseExpiresAt":"<RFC3339>"} in data. Renewal extends the lease by 60 seconds, bounded by the task expiry. An expired or replaced attempt returns 409. Platform acceptance followed by a runner crash before local persistence can cause a duplicate: delivery is at least once, not exactly once.
Binding and examples
An authenticated member redeems an external user's one-time binding token via POST /api/v1/channel/bind with {"token":"<one-time-binding-token>"}. This endpoint uses member authentication, not a Bridge token. Runnable examples and checks are in examples/channel-bridge/ and scripts/test-channel-bridge.sh. A production runner must persist inbound events and actual platform-send results before reporting success.
Example runner's platform HTTP contract
This is an interface exposed by examples/channel-bridge/runner.py, runner.go, and runner.mjs, not a Mopheus server endpoint. The runner listens on 127.0.0.1:8765 by default. A non-local listener requires MOPHEUS_BRIDGE_WEBHOOK_TOKEN; the platform sends it in X-Platform-Token. The runner durably records each event before responding HTTP 202.
POST http://127.0.0.1:8765/platform/messages
Content-Type: application/json
X-Platform-Token: <when configured>
{"messageId":"platform-msg-1","eventId":"event-1","occurredAt":"2026-10-14T10:01:59Z","conversation":{"id":"group-1","type":"group","title":"Engineering","threadId":"topic-1"},"sender":{"id":"user-1","kind":"user","displayName":"Alice"},"recipient":{"botId":"bot-1","addressedToBot":true},"content":{"kind":"mixed","text":"Report","parts":[{"type":"text","text":"Report"},{"type":"attachment","kind":"file","filename":"report.txt","mimeType":"text/plain","dataBase64":"aGVsbG8="}]}}| Platform field | Required | Bridge mapping |
|---|---|---|
messageId | Yes | payload.messageId; stable across retries |
eventId, occurredAt | No | payload.eventId, payload.occurredAt; default to messageId and receive time |
conversation | Yes | payload.conversation; at least id and type |
sender | Yes | payload.sender; at least id and kind |
recipient | No | payload.recipient; defaults to addressedToBot: true |
content.text, content.parts | No | Ordered text and parts; defaults to kind: text without parts |
Attachment dataBase64 | For attachments | Decoded and uploaded via POST /assets and PUT /assets/{assetId}, then replaced with assetId |
HTTP 202 returns {"accepted":true,"messageId":"platform-msg-1"}: the runner has persisted the event, but Mopheus may still be processing it. The same ID with different content returns 409; malformed requests return 400; invalid configured webhook token returns 401. The example body limit is 36 MiB and each attachment is at most 25 MiB.
For each outbound part, the runner POSTs to MOPHEUS_BRIDGE_PLATFORM_SEND_URL:
POST http://127.0.0.1:8766/send
Content-Type: application/json
{"deliveryId":"<uuid>","partId":"file:0","idempotencyKey":"<uuid>:file:0","target":{"type":"chat","conversationId":"group-1"},"replyTo":null,"part":{"partId":"file:0","type":"attachment","filename":"report.txt","mimeType":"text/plain"},"assetPath":".bridge-downloads/<uuid>-file:0","attachment":{"filename":"report.txt","mimeType":"text/plain","dataBase64":"aGVsbG8="}}
HTTP/1.1 200 OK
Content-Type: application/json
{"platformMessageId":"sim-123"}Text parts omit attachment. The platform should deduplicate by idempotencyKey. A non-2xx response or missing nonempty platformMessageId does not acknowledge delivery to Mopheus. platform_simulator.py serve implements /send and a read-only /messages outbox for exercising all three runners; a real platform can replace this URL or use MOPHEUS_BRIDGE_SEND_COMMAND to call its SDK.
Field reference
JSON names are case-sensitive camelCase. UUIDs are strings and times are RFC 3339. Required fields below apply to client requests; Mopheus fills server events.
| Field | Required | Meaning |
|---|---|---|
name, platformKey | Installation | Display name (up to 100 bytes) and platform identifier matching the stream header |
assigneeType, assigneeId | Installation | Workspace Agent (1) or Team (3) and its UUID |
externalBotId | No | Bot ID checked against a nonempty inbound recipient.botId |
installation.id | Creation response | Installation UUID and V1 credential path ID |
bridgeToken | Credential response | Returned once when generated or rotated on the detail page |
version, type, id, sentAt | Inbound | Version 1, message.inbound, transport UUID, submission time; id is not the deduplication key |
payload.eventId, .messageId, .occurredAt | Inbound | Platform event ID, installation-scoped message deduplication ID, original event time |
payload.conversation.id, .type, .title, .threadId | ID/type | Platform conversation ID, direct/group, optional display title and thread ID |
payload.sender.id, .kind, .stableId, .displayName | ID/kind | Sender ID, user/bot/system, optional stable account ID and display name; bot/system are dropped |
payload.recipient.addressedToBot, .botId | Addressed flag | Whether the bot was addressed; optional target bot ID |
payload.replyTo.messageId, .rootMessageId | When replying | Referenced platform message and optional root message |
payload.extensions | No | Platform-specific JSON; do not put secrets here |
payload.content.kind, .text, .parts | Kind/text | Overall media type, readable fallback (up to 64 KiB), ordered parts (up to 100; at most 20 attachments) |
parts[].type, .text | Type; text for text-like parts | text, mention, attachment, link, code, emoji, quote; text is up to 64 KiB per part |
parts[].id, .label | Mention | Mentioned platform user ID and display label; link/emoji also need a label |
parts[].assetId, .kind | Asset ID for attachments | Uploaded Bridge asset UUID and optional media kind |
parts[].url, .language, .unicode, .platformCode, .authorLabel | By part type | Link URL, code language, emoji identity, quote author |
data.messageId, .status, .reason | Inbound response | Original message ID, processing outcome, optional drop/rejection reason |
data.chatSessionId, .mediaState, .retryAfterMs | Inbound response | Mopheus session UUID, none/pending, retry delay in milliseconds when applicable |
assets.messageId, .sourceKey, .kind | Asset reserve | Owning message ID, stable within-message asset key, media kind |
assets.filename, .mimeType, .sizeBytes, .sha256 | Asset reserve | Basename, MIME, byte size (1 to 25 MiB), original SHA-256 hex |
data.assetId, .state, .uploadPath | Asset response | Bridge UUID, upload/ready, relative PUT path |
X-Content-SHA256, X-Bridge-Filename | PUT headers | Digest of uploaded bytes and original filename |
| Stream/delivery field | Meaning |
|---|---|
version, type, id, sentAt | Protocol version, hello/heartbeat/delivery, event UUID, server send time |
hello.connectionId, .heartbeatSeconds, .deliveryLeaseSeconds | Stream UUID, heartbeat period and initial lease in seconds |
hello.acceptedCapabilities.inbound/outbound, heartbeat.connectionId | Accepted capability lists and heartbeat stream UUID |
envelope.deliveryId, .attemptId, .attempt | Stable delivery UUID, current attempt UUID, attempt count starting at 1 |
envelope.leaseExpiresAt, .expiresAt, .completedParts | Current lease deadline, optional final expiry, server-known part IDs |
envelope.target.type, .conversationId, .conversationType, .userId, .threadId | chat/user target and applicable platform identifiers |
envelope.replyTo.messageId | Platform message to quote, when present |
envelope.content.text, .format, .parts[] | Readable snapshot, V1 plain format, ordered outbound parts |
parts[].partId, .type, .text | Stable part ID, type and optional text |
parts[].attachmentId, .kind, .filename, .mimeType, .sizeBytes, .downloadPath | Asset UUID, metadata and authenticated download path |
result.attemptId, .status, .parts[].partId, .parts[].platformMessageId | Attempt ID, sent/failed, every successful part and its actual platform message ID |
result.platformRequestId, .category, .platformCode, .message, .retryAfterMs | Optional platform trace ID and failure details/retry delay |
renew.attemptId, renew.data.leaseExpiresAt | Attempt to extend and its returned deadline |
The result response echoes data.deliveryId, data.attemptId, and data.status (sent, pending, or failed). data.completedParts may be null or empty in V1. Retries use a new attemptId.