{ "asyncapi": "3.1.0", "info": { "title": "Warmbly Realtime Gateway", "version": "1.0.0", "description": "Resumable WebSocket gateway for live organization events. The socket speaks the Phoenix channel protocol (serializer 1.0.0). Authenticate with an API key (REALTIME_SUBSCRIBE permission) or a short-lived JWT passed as the `token` query parameter. Events carry a monotonic per-organization `seq`; reconnecting clients resume by replaying the gap. See the human guide at https://docs.warmbly.com/api/realtime/.", "contact": { "name": "Warmbly", "url": "https://docs.warmbly.com" }, "license": { "name": "Proprietary", "url": "https://warmbly.com" } }, "defaultContentType": "application/json", "servers": { "production": { "host": "realtime.warmbly.com", "pathname": "/socket/websocket", "protocol": "wss", "description": "Production gateway. Connect with ?vsn=1.0.0&token=.", "security": [{ "$ref": "#/components/securitySchemes/token" }] } }, "channels": { "org": { "address": "org:{org_id}", "title": "Organization channel", "description": "Per-organization event stream + team presence. A subscriber receives every event the member/key is permitted to see, optionally narrowed by intents. Messages are Phoenix channel frames [join_ref, ref, topic, event, payload].", "parameters": { "org_id": { "description": "The organization id (UUID) to subscribe to." } }, "messages": { "join": { "$ref": "#/components/messages/Join" }, "hello": { "$ref": "#/components/messages/Hello" }, "event": { "$ref": "#/components/messages/Event" }, "resumed": { "$ref": "#/components/messages/Resumed" }, "resumeFailed": { "$ref": "#/components/messages/ResumeFailed" }, "rateLimited": { "$ref": "#/components/messages/RateLimited" }, "presenceState": { "$ref": "#/components/messages/PresenceState" }, "presenceDiff": { "$ref": "#/components/messages/PresenceDiff" }, "presenceUpdate": { "$ref": "#/components/messages/PresenceUpdate" } } } }, "operations": { "joinOrg": { "action": "send", "channel": { "$ref": "#/channels/org" }, "summary": "Join the org channel (optionally with intents and a resume token).", "messages": [{ "$ref": "#/channels/org/messages/join" }], "reply": { "channel": { "$ref": "#/channels/org" }, "messages": [{ "$ref": "#/channels/org/messages/hello" }] } }, "receiveOrgStream": { "action": "receive", "channel": { "$ref": "#/channels/org" }, "summary": "Receive live events, resume markers, presence, and rate-limit notices.", "messages": [ { "$ref": "#/channels/org/messages/event" }, { "$ref": "#/channels/org/messages/resumed" }, { "$ref": "#/channels/org/messages/resumeFailed" }, { "$ref": "#/channels/org/messages/rateLimited" }, { "$ref": "#/channels/org/messages/presenceState" }, { "$ref": "#/channels/org/messages/presenceDiff" } ] }, "updatePresence": { "action": "send", "channel": { "$ref": "#/channels/org" }, "summary": "Update your own presence activity (JWT members only).", "messages": [{ "$ref": "#/channels/org/messages/presenceUpdate" }] } }, "components": { "securitySchemes": { "token": { "type": "httpApiKey", "in": "query", "name": "token", "description": "An API key with the REALTIME_SUBSCRIBE permission, or a short-lived connection JWT, passed as the `token` query parameter on the socket URL." } }, "messages": { "Join": { "name": "phx_join", "title": "Join (phx_join)", "summary": "Subscribe to org:{org_id}. Optionally declare intents and a resume token.", "payload": { "$ref": "#/components/schemas/JoinPayload" } }, "Hello": { "name": "phx_reply", "title": "HELLO (join reply)", "summary": "Heartbeat cadence and the current stream sequence.", "payload": { "$ref": "#/components/schemas/Hello" } }, "Event": { "name": "event", "title": "Domain event", "summary": "A live or replayed event. The frame event name is the event_type (e.g. CAMPAIGN_UPDATED); the payload carries the body plus a monotonic seq.", "payload": { "$ref": "#/components/schemas/Event" } }, "Resumed": { "name": "resumed", "title": "Resume complete", "summary": "Sent after a successful resume replay; the events preceded it.", "payload": { "$ref": "#/components/schemas/Resumed" } }, "ResumeFailed": { "name": "resume_failed", "title": "Resume failed", "summary": "The buffer no longer covers your position (or the token was malformed). Do a full REST resync, then continue from current_seq.", "payload": { "$ref": "#/components/schemas/ResumeFailed" } }, "RateLimited": { "name": "rate_limited", "title": "Outbound rate limited", "payload": { "$ref": "#/components/schemas/RateLimited" } }, "PresenceState": { "name": "presence_state", "title": "Presence snapshot", "payload": { "$ref": "#/components/schemas/PresenceState" } }, "PresenceDiff": { "name": "presence_diff", "title": "Presence diff", "payload": { "$ref": "#/components/schemas/PresenceDiff" } }, "PresenceUpdate": { "name": "presence:update", "title": "Update own presence", "payload": { "$ref": "#/components/schemas/PresenceUpdate" } } }, "schemas": { "JoinPayload": { "type": "object", "description": "Payload of the phx_join frame for org:{org_id}.", "properties": { "intents": { "type": "array", "items": { "type": "string" }, "description": "Optional event-family tokens (case-insensitive substring of the event type, e.g. CAMPAIGN, EMAIL, AUDIT). Absent/empty = the full permitted stream. Not a security boundary." }, "resume": { "type": "object", "description": "Resume token from a reconnecting client.", "properties": { "last_seq": { "type": "integer", "minimum": 0, "description": "The highest seq the client has processed. The server replays events with seq greater than this." } }, "required": ["last_seq"] } } }, "Hello": { "type": "object", "required": ["org_id", "heartbeat_interval_ms", "server_timeout_ms", "seq"], "properties": { "org_id": { "type": "string", "format": "uuid" }, "role": { "type": "string", "description": "The member's role in the org (JWT sockets)." }, "heartbeat_interval_ms": { "type": "integer", "description": "Send a heartbeat on the phoenix topic at least this often." }, "server_timeout_ms": { "type": "integer", "description": "The server closes the socket after this long with no heartbeat." }, "seq": { "type": "integer", "description": "Current stream sequence. A fresh client should start tracking from here." }, "resume_supported": { "type": "boolean" } } }, "Event": { "type": "object", "required": ["event_type", "seq"], "description": "Invalidation-oriented: the body carries ids, not full resource state. Refetch over REST for current contents. Permission- and intent-filtered identically for live and replayed events.", "properties": { "event_type": { "type": "string", "description": "e.g. AUDIT_CREATED, CAMPAIGN_UPDATED, EMAIL_SENT, EMAIL_RECEIVED. Also the frame event name." }, "seq": { "type": "integer", "description": "Monotonic per-organization sequence. Track the highest seen; dedupe replays by it." }, "org_id": { "type": "string", "format": "uuid" }, "user_id": { "type": "string", "format": "uuid" }, "campaign_id": { "type": "string", "format": "uuid" }, "email_account_id": { "type": "string", "format": "uuid" } }, "additionalProperties": true }, "Resumed": { "type": "object", "required": ["from", "current_seq", "replayed"], "properties": { "from": { "type": "integer", "description": "The last_seq the client resumed from." }, "current_seq": { "type": "integer", "description": "The stream sequence after replay; the client is now caught up to here." }, "replayed": { "type": "integer", "description": "Number of events delivered during replay (after permission + intent filtering)." } } }, "ResumeFailed": { "type": "object", "required": ["reason", "current_seq"], "properties": { "reason": { "type": "string", "enum": ["buffer_evicted", "invalid_resume"] }, "current_seq": { "type": "integer", "description": "Resync over REST, then continue live from here." } } }, "RateLimited": { "type": "object", "properties": { "category": { "type": "string", "example": "ws_message" }, "retry_after_ms": { "type": "integer" } } }, "PresenceState": { "type": "object", "description": "Phoenix presence snapshot: a map of user_id -> { metas: [...] }.", "additionalProperties": true }, "PresenceDiff": { "type": "object", "description": "Phoenix presence diff with joins and leaves.", "properties": { "joins": { "type": "object", "additionalProperties": true }, "leaves": { "type": "object", "additionalProperties": true } } }, "PresenceUpdate": { "type": "object", "properties": { "page": { "type": "string" }, "resource": { "type": "string", "description": "e.g. thread:, campaign:, contact:." }, "action": { "type": "string", "enum": ["viewing", "editing", "replying", "idle"] } } } } } }