Integrations

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

ItemContract
Bridge base path/api/v1/channel/bridge
Versionversion: 1 in events and inbound messages; X-Bridge-Protocol-Version: 1 on the stream
CredentialAuthorization: Bearer mcb_<installation-uuid>_<secret>; plaintext is returned only on create or rotation
JSON responseSuccess: {"success":true,"data":{...}}; error: {"success":false,"code":"...","error":"...","requestId":"..."}. Optional keys may be absent
IDs and timeInternal 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 pathRequestSuccessful data
POST /bridge/installations{"name":"Example","platformKey":"example","assigneeType":1,"assigneeId":"<agent-uuid>","externalBotId":"bot-1"}{"installation":{...}}
POST /bridge/installations/{installationId}/credentialsEmpty 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 IDnull; 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,mention

HTTP 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.statusRunner action
ingestedTerminal: stored, but agent processing may still be pending
duplicateTerminal: this installation already processed the messageId
droppedTerminal: channel rule ignored it; inspect reason
rejectedTerminal: invalid event age or recipient
retryKeep 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 fieldRequiredBridge mapping
messageIdYespayload.messageId; stable across retries
eventId, occurredAtNopayload.eventId, payload.occurredAt; default to messageId and receive time
conversationYespayload.conversation; at least id and type
senderYespayload.sender; at least id and kind
recipientNopayload.recipient; defaults to addressedToBot: true
content.text, content.partsNoOrdered text and parts; defaults to kind: text without parts
Attachment dataBase64For attachmentsDecoded 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.

FieldRequiredMeaning
name, platformKeyInstallationDisplay name (up to 100 bytes) and platform identifier matching the stream header
assigneeType, assigneeIdInstallationWorkspace Agent (1) or Team (3) and its UUID
externalBotIdNoBot ID checked against a nonempty inbound recipient.botId
installation.idCreation responseInstallation UUID and V1 credential path ID
bridgeTokenCredential responseReturned once when generated or rotated on the detail page
version, type, id, sentAtInboundVersion 1, message.inbound, transport UUID, submission time; id is not the deduplication key
payload.eventId, .messageId, .occurredAtInboundPlatform event ID, installation-scoped message deduplication ID, original event time
payload.conversation.id, .type, .title, .threadIdID/typePlatform conversation ID, direct/group, optional display title and thread ID
payload.sender.id, .kind, .stableId, .displayNameID/kindSender ID, user/bot/system, optional stable account ID and display name; bot/system are dropped
payload.recipient.addressedToBot, .botIdAddressed flagWhether the bot was addressed; optional target bot ID
payload.replyTo.messageId, .rootMessageIdWhen replyingReferenced platform message and optional root message
payload.extensionsNoPlatform-specific JSON; do not put secrets here
payload.content.kind, .text, .partsKind/textOverall media type, readable fallback (up to 64 KiB), ordered parts (up to 100; at most 20 attachments)
parts[].type, .textType; text for text-like partstext, mention, attachment, link, code, emoji, quote; text is up to 64 KiB per part
parts[].id, .labelMentionMentioned platform user ID and display label; link/emoji also need a label
parts[].assetId, .kindAsset ID for attachmentsUploaded Bridge asset UUID and optional media kind
parts[].url, .language, .unicode, .platformCode, .authorLabelBy part typeLink URL, code language, emoji identity, quote author
data.messageId, .status, .reasonInbound responseOriginal message ID, processing outcome, optional drop/rejection reason
data.chatSessionId, .mediaState, .retryAfterMsInbound responseMopheus session UUID, none/pending, retry delay in milliseconds when applicable
assets.messageId, .sourceKey, .kindAsset reserveOwning message ID, stable within-message asset key, media kind
assets.filename, .mimeType, .sizeBytes, .sha256Asset reserveBasename, MIME, byte size (1 to 25 MiB), original SHA-256 hex
data.assetId, .state, .uploadPathAsset responseBridge UUID, upload/ready, relative PUT path
X-Content-SHA256, X-Bridge-FilenamePUT headersDigest of uploaded bytes and original filename
Stream/delivery fieldMeaning
version, type, id, sentAtProtocol version, hello/heartbeat/delivery, event UUID, server send time
hello.connectionId, .heartbeatSeconds, .deliveryLeaseSecondsStream UUID, heartbeat period and initial lease in seconds
hello.acceptedCapabilities.inbound/outbound, heartbeat.connectionIdAccepted capability lists and heartbeat stream UUID
envelope.deliveryId, .attemptId, .attemptStable delivery UUID, current attempt UUID, attempt count starting at 1
envelope.leaseExpiresAt, .expiresAt, .completedPartsCurrent lease deadline, optional final expiry, server-known part IDs
envelope.target.type, .conversationId, .conversationType, .userId, .threadIdchat/user target and applicable platform identifiers
envelope.replyTo.messageIdPlatform message to quote, when present
envelope.content.text, .format, .parts[]Readable snapshot, V1 plain format, ordered outbound parts
parts[].partId, .type, .textStable part ID, type and optional text
parts[].attachmentId, .kind, .filename, .mimeType, .sizeBytes, .downloadPathAsset UUID, metadata and authenticated download path
result.attemptId, .status, .parts[].partId, .parts[].platformMessageIdAttempt ID, sent/failed, every successful part and its actual platform message ID
result.platformRequestId, .category, .platformCode, .message, .retryAfterMsOptional platform trace ID and failure details/retry delay
renew.attemptId, renew.data.leaseExpiresAtAttempt 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.