Skip to content

mechaHarry/webex-swift-sdk

Repository files navigation

webex-swift-sdk

Swift-first macOS SDK for Webex Developer APIs.

Current Scope

This package provides the OAuth and authenticated REST foundation:

  • user-provided Webex integration credentials
  • PKCE authorization request support
  • Apple-native browser auth boundary
  • SDK-owned loopback redirect listener on 127.0.0.1:8282
  • automatic Keychain-backed credential and refresh-token storage
  • memory-only access-token cache by default
  • coordinated token refresh
  • authenticated REST transport
  • typed REST APIs for People, Spaces/Rooms, Memberships, Messages, Teams, Team Memberships, and Webhooks
  • snapshot streams for Spaces/Rooms, Teams, Memberships, Messages, and threaded Messages
  • SDK-derived Spaces snapshot enrichment for team names and direct-space avatars
  • webhook notification decoding, signature verification, and stream-trigger projection
  • experimental receive-only realtime WebSocket events and stream triggers

Public Surface Map

This README is the public inventory for client and agent discovery. If an app needs an SDK-friendly collection view, prefer these surfaces instead of manually looping raw REST calls in UI code.

  • client.people: me, list, and get.
  • client.spaces / client.rooms: list, create, get, update, delete, plus stream(params:pageLimit:).
  • client.memberships: list, create, get, update, delete, plus stream(params:pageLimit:).
  • client.messages: list, create, get, edit, delete, stream(params:pageLimit:), and threadedStream(params:pageLimit:).
  • client.teams: list, create, get, update, delete, plus stream(params:pageLimit:). Use this for a team roster; do not infer the full team list from paged spaces.
  • client.teamMemberships: list, create, get, update, and delete.
  • client.webhooks: list, create, get, update, and delete. WebexWebhookNotification can decode inbound webhook payloads, WebexWebhookSignatureVerifier validates signed requests, and streamTrigger() projects webhook notifications into WebexStreamTrigger.
  • client.realtime.connect(options:): exposes connection states, decoded realtime events, and triggers that can drive stream refreshes.

Example

import WebexSwiftSDK

let registry = WebexClientRegistry()
let accounts = try await registry.listAccounts()

for account in accounts {
    let client = try await registry.client(for: account.id)
    let person = try await client.people.me()
    print(person.displayName ?? person.id)
}

The host macOS app owns UI, account selection, and window-to-account routing. The SDK owns OAuth, token lifecycle, local secure storage, and authenticated Webex REST execution.

Snapshot Streams

Snapshot streams are a stateful SDK layer over the wire-faithful REST APIs. They keep the previous snapshot visible while a refresh or next-page load is running, then emit a reconciled snapshot when the SDK has new data. They are not push streams by themselves; use realtime or webhook triggers to ask a stream to refresh.

let stream = client.spaces.stream(
    params: .init(sortBy: .lastActivity, max: 20),
    pageLimit: 5
)

Task {
    for await snapshot in stream.snapshots {
        print(snapshot.items.map(\.title))
        print("refreshing:", snapshot.isRefreshing)
        print("has more:", snapshot.pagination.hasMore)
    }
}

await stream.refresh()

let snapshot = await stream.currentSnapshot()
if snapshot.pagination.hasMore, !snapshot.pagination.capReached {
    await stream.loadNextPage()
}

The max parameter remains the Webex REST page size. The stream pageLimit is only a local safety cap for how many pages explicit loadNextPage() calls may accumulate before the stream reports pagination.capReached.

Available snapshot adapters:

  • client.spaces.stream(...) returns SpacesStream.
  • client.rooms.stream(...) uses the same implementation through the Rooms compatibility alias.
  • client.teams.stream(...) returns TeamsStream.
  • client.memberships.stream(...) returns MembershipsStream.
  • client.messages.stream(...) returns MessagesStream.
  • client.messages.threadedStream(...) returns MessagesThreadStream.

All snapshot streams expose snapshots, currentSnapshot(), refresh(), loadNextPage(), and refreshOnTriggers(...). SpacesStream also exposes refreshEnrichment().

Space streams enrich each WebexSpace item with SDK-derived details such as item.enriched.teamName and item.enriched.spaceAvatar. Direct REST calls remain wire-faithful: client.spaces.list and client.spaces.get do not make follow-up enrichment calls and decode space.enriched == .empty. Use await stream.refreshEnrichment() to refresh cached enrichment details without reloading the base spaces page.

Migration note: SpacesStream is now a named stream wrapper instead of an alias to WebexSnapshotStream<WebexSpace>, and RoomsStream aliases SpacesStream. Existing client code that consumes stream.snapshots, currentSnapshot(), refresh(), loadNextPage(), or refreshOnTriggers can keep those calls. Code that constructed WebexSnapshotStream<WebexSpace> directly or accepted that concrete generic type should accept SpacesStream/RoomsStream instead.

For team rosters, use client.teams.stream(...) or client.teams.list(...). client.spaces.stream(...) is not a complete team source because it only sees team IDs attached to the currently returned spaces.

Realtime

Realtime support is an experimental Swift-native WebSocket listener exposed through client.realtime. It is receive-only: use REST APIs for writes and detail fetches, and use realtime events as triggers for refreshing SDK Snapshot Streams or app state.

let connection = client.realtime.connect()

Task {
    for await state in connection.states {
        print(state)
    }
}

Task {
    for await event in connection.events {
        print(event.resource, event.event, event.decodeStatus)
    }
}

let stream = client.messages.stream(params: .init(roomID: roomID, max: 25))
let triggerTask = stream.refreshOnTriggers(connection.triggers) { trigger in
    trigger.resource == WebexRealtimeResource.messages.rawValue
        && trigger.roomID == roomID
}

For realtime OAuth, the Webex token must be granted spark:all and spark:kms. A host macOS app should set those scopes directly in WebexIntegrationConfiguration; shell variables such as WEBEX_SCOPES are only for examples.

let configuration = WebexIntegrationConfiguration(
    clientID: userProvidedClientID,
    clientSecret: userProvidedClientSecret,
    redirectURI: URL(string: "http://127.0.0.1:8282/oauth/callback")!,
    scopes: ["spark:all", "spark:kms"],
    prefersEphemeralWebBrowserSession: false
)

The token response's granted scopes are authoritative. If Webex grants only a narrow REST scope such as spark:people_read, REST People calls can still work while realtime WDM device registration fails with HTTP 403. After changing integration scopes in Webex, reauthorize so the new grants are present in the stored token record.

Realtime event ACKs use the Mercury frame id, while event resourceID remains the REST resource id that apps use for follow-up API calls. The WebexRealtimeEventsSmoke output includes both fields for protocol debugging. The smoke also enables WebexRealtimeOptions.diagnosticHandler, so it prints decoded event metadata, filtered-event decisions, ACK failures, frame decode failures, and reconnect reasons without dumping raw payloads by default. These diagnostics include Mercury source metadata such as sourceEventType, activityVerb, and objectType when available. The WebSocket transport prepares the WDM URL with text wire-format query parameters before connecting; using the raw WDM URL can make Webex send binary frames that the JSON event layer cannot decode.

Cancel the task returned by refreshOnTriggers(...) when the owning view model or runtime is torn down.

Spaces

Webex's REST API still uses /v1/rooms, while modern product language calls these collaboration containers spaces. The SDK exposes client.spaces as the preferred interface and client.rooms as a compatibility alias.

let page = try await client.spaces.list(params: .init(max: 50))
for space in page.items {
    print(space.id, space.title ?? "(untitled)")
}

if let nextPage = page.nextPage {
    let next = try await client.spaces.list(nextPage: nextPage)
    print("Fetched another \(next.items.count) spaces")
}

let created = try await client.spaces.create(.init(title: "Incident Review"))
let updated = try await client.spaces.update(
    spaceID: created.id,
    .init(title: "Incident Review - Closed")
)
try await client.spaces.delete(spaceID: updated.id)

For developers following Webex's endpoint reference, client.rooms maps to the same implementation as client.spaces.

Teams

Teams are available through client.teams. Use the documented Teams API for creating, listing, fetching, renaming, and deleting teams.

Use spark:teams_read for list/get calls and spark:teams_write for create/update/delete calls.

let team = try await client.teams.create(.init(name: "Incident Response"))
let teams = try await client.teams.list(params: .init(max: 25))

let renamed = try await client.teams.update(
    teamID: team.id,
    .init(name: "Incident Response - Archive")
)

Team memberships are available through client.teamMemberships:

Use spark:team_memberships_read for list/get calls and spark:team_memberships_write for create/update/delete calls.

let member = try await client.teamMemberships.create(.init(
    teamID: team.id,
    personEmail: "person@example.com",
    isModerator: true
))

let members = try await client.teamMemberships.list(params: .init(teamID: team.id, max: 50))
let updatedMember = try await client.teamMemberships.update(
    teamMembershipID: member.id,
    .init(isModerator: false)
)
try await client.teamMemberships.delete(teamMembershipID: updatedMember.id)

Team spaces use the existing Spaces API. List spaces for a team with ListSpacesParams(teamID:) or create a team space by setting teamID in CreateSpaceRequest. Team spaces still use the Spaces/Rooms scopes. Webex does not document moving a space between teams or removing a space from a team after creation.

let teamSpaces = try await client.spaces.list(params: .init(teamID: team.id, max: 25))
let newTeamSpace = try await client.spaces.create(.init(
    title: "Incident Review",
    teamID: team.id
))

try await client.teams.delete(teamID: renamed.id)

WebexTeam and WebexTeamMembership preserve returned-but-undocumented JSON fields in additionalFields. This is useful for inspecting wire-faithful metadata such as future visual or lifecycle fields, but the SDK only exposes documented team writes as typed request properties.

Webhooks

Webhooks are available through client.webhooks.

let createdWebhook = try await client.webhooks.create(.init(
    name: "Message Events",
    targetURL: "https://example.com/webex",
    resource: .messages,
    event: .created,
    filter: "roomId=\(spaceID)",
    secret: webhookSecret
))

let webhookPage = try await client.webhooks.list(params: .init(max: 25))
let webhook = try await client.webhooks.get(webhookID: createdWebhook.id)
try await client.webhooks.delete(webhookID: webhook.id)

For inbound webhook handlers, validate the request before decoding or trusting the payload:

guard WebexWebhookSignatureVerifier.isValidRequest(
    payload: requestBody,
    headers: requestHeaders,
    secret: webhookSecret
) else {
    throw WebhookError.invalidSignature
}

let notification = try JSONDecoder().decode(WebexWebhookNotification.self, from: requestBody)
let trigger = notification.streamTrigger()

Memberships

Memberships manage who belongs to a Webex space and whether a member is a moderator.

Use spark:memberships_read for list/get calls and spark:memberships_write for create/update/delete calls.

let members = try await client.memberships.list(params: .init(roomID: spaceID, max: 50))
for member in members.items {
    print(member.id, member.personDisplayName ?? member.personEmail ?? "(unknown)")
}

let created = try await client.memberships.create(.init(
    roomID: spaceID,
    personEmail: "person@example.com"
))
let updated = try await client.memberships.update(
    membershipID: created.id,
    .init(isModerator: true)
)
try await client.memberships.delete(membershipID: updated.id)

People

People reads are available through client.people. Use spark:people_read for normal search/detail reads and spark-admin:people_read for org-wide listing.

let me = try await client.people.me()
let people = try await client.people.list(params: .init(id: me.id, excludeStatus: true))
print(people.items.first?.displayName ?? me.id)

Messages

Messages are available through client.messages. Use spark:messages_read for list/get calls and spark:messages_write for create/edit/delete calls.

let page = try await client.messages.list(params: .init(roomID: spaceID, max: 25))
for message in page.items {
    print(message.id, message.text ?? message.markdown ?? "(no text)")
}

if let nextPage = page.nextPage {
    let olderMessages = try await client.messages.list(nextPage: nextPage)
    print("Fetched another \(olderMessages.items.count) messages")
}

let created = try await client.messages.create(.init(
    roomID: spaceID,
    markdown: "**Status:** investigating"
))
let edited = try await client.messages.edit(
    messageID: created.id,
    .init(roomID: spaceID, markdown: "**Status:** resolved")
)
try await client.messages.delete(messageID: edited.id)

Examples

  • Examples/WebexClientSmoke: interactive OAuth smoke test that uses the SDK-owned loopback listener, stores a registry account, exchanges an authorization code, creates WebexClient, and calls people.me().
  • Examples/WebexPeopleReadSmoke: interactive OAuth smoke test that reads the current person and performs a bounded People list lookup.
  • Examples/WebexSpacesListSmoke: interactive OAuth smoke test that lists Spaces with explicit bounded pagination.
  • Examples/WebexMembershipsListSmoke: interactive OAuth smoke test that lists Memberships for WEBEX_ROOM_ID with explicit bounded pagination.
  • Examples/WebexMessagesListSmoke: interactive OAuth smoke test that lists Messages for WEBEX_ROOM_ID with explicit bounded pagination.
  • Examples/WebexMessagesStreamWindowSmoke: native SwiftUI smoke window that subscribes to MessagesStream snapshots and auto-refreshes the stream from realtime message triggers.
  • Examples/WebexSpacesEnrichedSnapshotSmoke: native SwiftUI smoke window that compares wire-faithful Spaces snapshot fields with SDK-derived item.enriched fields.
  • Examples/WebexTeamsSnapshotSmoke: native SwiftUI smoke window that displays TeamsStream snapshots and surfaces returned WebexTeam.additionalFields visually.
  • Examples/WebexRealtimeEventsSmoke: interactive OAuth or direct-token smoke test that connects to Webex realtime, validates granted realtime scopes in OAuth mode, and prints connection states/events.