> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alakazam.gg/llms.txt
> Use this file to discover all available pages before exploring further.

# iOS SDK

> Install the Alakazam Portal Swift package and generate programmable worlds and drop-in AI characters from Swift.

The **Alakazam Portal** Swift package (`AlakazamPortal`) gives you the whole `/v1` surface from Swift — generate and manage worlds, create talk-to characters, mint runtime sessions — plus a drop-in **AI Character** component. Requires **iOS 15+** (macOS 12+ also supported).

## Install

Add the package in Xcode (**File → Add Package Dependencies…**), or declare it in your `Package.swift`:

<CodeGroup>
  ```text Add Package Dependencies… theme={null}
  https://github.com/Alakazam-studios/alakazam-ios.git
  ```

  ```swift Package.swift theme={null}
  dependencies: [
      .package(url: "https://github.com/Alakazam-studios/alakazam-ios.git", from: "1.0.0")
  ],
  targets: [
      .target(name: "MyApp", dependencies: [
          .product(name: "AlakazamPortal", package: "alakazam-ios")
      ])
  ]
  ```
</CodeGroup>

Zero dependencies — Foundation + AVFoundation only.

## Setup

<Steps>
  <Step title="Get a key">
    Start with an `sk_test_…` secret key ([get one here](https://play.alakazam.gg/?view=developer)) — it runs a free, mocked [sandbox](/testing). Swap to `sk_live_…` when you ship.
  </Step>

  <Step title="Store it">
    For prototype/backend use, stash the key in the Keychain with `AlakazamAuth.setApiKey(_:)` and construct `AlakazamClient()` with no arguments — it reads the stored key automatically. Or just pass the key straight through: `AlakazamClient(secretKey: "sk_test_…")`.
  </Step>
</Steps>

<Warning>
  **Secret keys are prototype/server-only.** Your `sk_` key can create worlds and mint sessions, so never ship it inside your app. At runtime, a player talks to a character through a short-lived **session token** minted by your backend — the [two-token rule](/embedding).
</Warning>

## Programmable worlds & AI characters (`/v1`)

`AlakazamClient` is a thin async/await wrapper over the [`/v1` API](/api-reference). Construct it with your secret key, or pass nothing to reuse the key stored in the Keychain.

```swift theme={null}
import AlakazamPortal

let client = AlakazamClient(secretKey: "sk_test_…")

// Generate a world from a premise
let world = try await client.createWorld(premise: "a neon rooftop at night")
print("\(world.worldId ?? "") · \(world.slug ?? "") · mock=\(world.mock ?? false)")
```

List, fork, edit, and import compiled worlds:

```swift theme={null}
let mine    = try await client.listWorlds(limit: 20)                                   // JSONValue tree
let forked  = try await client.forkWorld(world.worldId ?? "")
let edited  = try await client.editWorld(world.worldId ?? "", instruction: "add rain and a flickering sign")

// Import a pre-compiled SMWorld graph verbatim (no recompile)
let imported = try await client.importWorld(worldJson: compiledWorldJson, name: "My Level")
```

<Note>
  A `sk_test_…` key returns **deterministic mocks** — every response carries `mock: true`, and nothing spends GPU or quota. Build your whole integration for free, then swap to `sk_live_…`. See [Testing](/testing).
</Note>

### Drop-in AI Character

`AlakazamCharacter` is a `@MainActor ObservableObject` that wraps the session → say → speak loop as an NPC you can drop into any SwiftUI view or UIKit screen.

<Steps>
  <Step title="Create it">
    Construct with the `characterId` from `client.createCharacter(...)` (below) and, in a shipped app, a `sessionToken` minted by your backend:

    ```swift theme={null}
    let npc = AlakazamCharacter(
        characterId: character.id ?? "",
        sessionToken: backendMintedSessionToken   // never a secret key in a shipped app
    )
    ```
  </Step>

  <Step title="Wire it up">
    Bind `onReply` / `onError`, or observe the `@Published var lastReply` / `isConnected` directly from SwiftUI.
  </Step>

  <Step title="Prototype without a backend">
    Leave `sessionToken` `nil` and pass a `secretKey` instead — the component mints one itself for prototyping. **In a shipped build, never do this** — assign a backend-minted token.
  </Step>
</Steps>

With `autoSpeak` on (default) it plays the reply through `AVAudioPlayer` automatically:

```swift theme={null}
import AlakazamPortal

@MainActor
final class InnkeeperViewModel: ObservableObject {
    let npc: AlakazamCharacter

    init(characterId: String, sessionToken: String) {
        npc = AlakazamCharacter(characterId: characterId, sessionToken: sessionToken)
        npc.onReply = { reply in print("\(reply.stance ?? ""): \(reply.reply ?? "")") }
        npc.onError = { error in print(error) }
    }

    func greet() async {
        await npc.say("hello")   // fires onReply, then auto-speaks via AVAudioPlayer
    }
}
```

To create the character the component talks to (an editor/server step — needs the secret key), and to mint the session token your backend would hand the client:

```swift theme={null}
let character = try await client.createCharacter(name: "Iris", premise: "a wry rooftop hacker")
let session   = try await client.mintSession(worldId: character.id ?? "")   // do this on YOUR backend in production
let turn      = try await client.characterSay(character.id ?? "", sessionToken: session.token ?? "", text: "who are you?")
print("\(turn.stance ?? ""): \(turn.reply ?? "")")
```

The component also exposes `speak(_:)` (synthesize + play a line) and `showImage(_:)` (returns a `show_image` src). For a public, token-free share link to a *public* world, use `AlakazamClient.publicPlayUrl(slug)`.

## `AlakazamClient` reference

Every method maps 1:1 to a `/v1` endpoint. Typed methods decode into a `Codable` struct; the raw endpoints return a `JSONValue` tree.

| Method                                        | Auth           | Returns                                            |
| --------------------------------------------- | -------------- | -------------------------------------------------- |
| `createWorld(premise:name:frameUrl:pov:)`     | secret         | `WorldResult` — `{ worldId, slug, cover, mock }`   |
| `importWorld(worldJson:name:description:)`    | secret         | `WorldResult` — `{ worldId, slug, imported }`      |
| `getWorld(_:)`                                | secret         | `JSONValue` (graph nested under `world`)           |
| `listWorlds(limit:cursor:)`                   | secret         | `JSONValue` — `{ worlds, nextCursor }`             |
| `forkWorld(_:)`                               | secret         | `ForkResult` — `{ worldId }`                       |
| `editWorld(_:instruction:)`                   | secret         | `JSONValue` — `{ world, diagnostics, reply }`      |
| `likeWorld(_:)`                               | publishable ok | —                                                  |
| `createCharacter(name:premise:authored:)`     | secret         | `CharacterResult` — `{ id, slug, kind, mock }`     |
| `listCharacters(limit:cursor:)`               | secret         | `JSONValue` — `{ characters, nextCursor }`         |
| `mintSession(worldId:playerIdentity:origin:)` | secret         | `SessionToken` — `{ token, expiresIn, slug }`      |
| `connectSession(_:)`                          | session        | `ConnectResult` — `{ worldId, reactorJwt, world }` |
| `refreshSession(_:)`                          | session        | `RefreshResult` — `{ reactorJwt, expiresIn }`      |
| `trackSession(_:reactorSessionId:)`           | session        | —                                                  |
| `endSession(_:)`                              | session        | `EndResult` — `{ ok, seconds }`                    |
| `characterSay(_:sessionToken:text:)`          | session        | `SayResult` — `{ reply, stance, emotion, action }` |
| `characterTts(_:sessionToken:text:)`          | session        | `Data` — raw `audio/mpeg` (MP3)                    |
| `characterImage(_:sessionToken:prompt:)`      | session        | `ImageResult` — `{ src, generated }`               |
| `getUsage()`                                  | secret         | `UsageSnapshot` — `{ day, usage, caps, plan }`     |
| `publicPlayUrl(_:)`                           | —              | `String` (static helper)                           |

<Note>
  Optional params: `frameB64` on **createWorld** / **createCharacter** (seed creation from a base64
  image), `cursor` on **listWorlds** / **listCharacters** (pagination — pass the prior `nextCursor`),
  and `ttlSeconds` on **mintSession** (60–3600, omitted when 0). Empty optionals are omitted from the
  request body (never sent as `""`), so payloads match the Unity/Unreal/Android SDKs.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Android SDK" icon="smartphone" href="/android">
    The same worlds + AI characters in Kotlin.
  </Card>

  <Card title="Characters" icon="user-round" href="/characters">
    Stances, memory, voice, and images in depth.
  </Card>

  <Card title="The two-token rule" icon="key-round" href="/embedding">
    Mint session tokens server-side; never ship a secret key.
  </Card>

  <Card title="Testing sandbox" icon="flask" href="/testing">
    Build the whole integration free with a test key.
  </Card>
</CardGroup>
