# Configure Cartesia API key

> Obtain a Cartesia API key, enter it during onboarding or Settings, Keychain storage (account cartesiaAPIKey), advisory validation via APIKeyValidator, and persisted cartesiaKeyInvalid / cartesiaOutOfCredits flags that drive Home service-issue cards.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `README.md`
- `InkIt/SettingsStore.swift`
- `InkIt/CartesiaKeyValidator.swift`
- `InkIt/OnboardingView.swift`
- `InkIt/SettingsView.swift`
- `InkIt/CartesiaStreamingClient.swift`

---

---
title: "Configure Cartesia API key"
description: "Obtain a Cartesia API key, enter it during onboarding or Settings, Keychain storage (account cartesiaAPIKey), advisory validation via APIKeyValidator, and persisted cartesiaKeyInvalid / cartesiaOutOfCredits flags that drive Home service-issue cards."
---

InkIt stores your Cartesia credential in the macOS Keychain under account `cartesiaAPIKey`, reads it through `SettingsStore.cartesiaAPIKey` on every dictation, and passes it to `CartesiaStreamingClient` as the `X-API-Key` header on the Ink-2 STT WebSocket. Advisory validation (`CartesiaKeyValidator`) probes the key at entry time; runtime STT failures set persisted `cartesiaKeyInvalid` or `cartesiaOutOfCredits` flags that drive the Home “Dictation is paused” banner until the next successful transcription.

## Prerequisites

- A Cartesia account with an API key (free tier includes roughly 15,000 words of dictation per month).
- InkIt installed and launched at least once.
- For first-run setup: microphone and Accessibility permissions granted on the onboarding **Permissions** step before the API key step.

<Note>
Cartesia powers transcription only. Optional Polish uses separate per-provider keys (`llm.{provider}`) documented on [Configure Polish](/configure-polish).
</Note>

## Obtain a Cartesia API key

1. Open [play.cartesia.ai/keys](https://play.cartesia.ai/keys) (linked from onboarding and Settings).
2. Sign in or create a Cartesia account.
3. Create or copy an API key. Keys use the `sk_car_…` prefix shown in InkIt placeholders.

<Tip>
The README points to [play.cartesia.ai](https://play.cartesia.ai) for the free tier. InkIt’s in-app links target the `/keys` path directly.
</Tip>

## Enter the key

<Tabs>
<Tab title="Onboarding">

Onboarding step order: Welcome → Permissions → **API key** → Try It → Done.

<Steps>
<Step title="Reach the API key step">

Complete **Permissions** (microphone and Accessibility both granted), then advance to **Turn on the engine**.

</Step>
<Step title="Paste your key">

Enter the key in the masked `SecureField` (placeholder `sk_car_…`). The field binds to `settings.cartesiaAPIKey`; each change persists immediately to Keychain.

</Step>
<Step title="Review validation (optional)">

`CartesiaKeyValidator` debounces keystrokes by 0.6s and shows an inline verdict: **Verified**, **Invalid key**, or **Couldn’t verify**. Validation is advisory — it never blocks **Continue**.

</Step>
<Step title="Continue">

**Continue** enables when the trimmed key is non-empty. Emptying the key re-locks forward steps in the progress dots.

</Step>
</Steps>

</Tab>
<Tab title="Settings">

<Steps>
<Step title="Open Settings">

From Home, open Settings (gear). Go to **Dictation** → **Transcription**, or search for “Cartesia API key”.

</Step>
<Step title="Edit the key">

`CartesiaKeyField` uses `APIKeyField` with the same validator. The field masks at rest and reveals plaintext while focused (`RevealableSecureField`).

</Step>
<Step title="Confirm validation">

The trailing glyph and caption mirror onboarding states. **Invalid key** and **Couldn’t verify — check your connection** appear under the field for error states.

</Step>
</Steps>

</Tab>
</Tabs>

## Storage and persistence

```text
┌─────────────────────┐     didSet      ┌──────────────────────────────────┐
│ OnboardingView      │ ──────────────► │ SettingsStore.cartesiaAPIKey     │
│ CartesiaKeyField    │                 │ @Published                       │
└─────────────────────┘                 └──────────────┬───────────────────┘
                                                       │
                                                       ▼
                                        ┌──────────────────────────────────┐
                                        │ Keychain (account: cartesiaAPIKey) │
                                        │ service: Bundle.main.bundleId    │
                                        │ accessible: AfterFirstUnlock     │
                                        └──────────────────────────────────┘
```

### Keychain account

<ParamField body="cartesiaAPIKey" type="string" required>
Keychain generic-password account name for the Cartesia STT credential. Written on every `cartesiaAPIKey` change; an empty string removes the item.
</ParamField>

| Property | Value |
| --- | --- |
| Keychain account | `cartesiaAPIKey` |
| Keychain service | App bundle identifier (e.g. `com.cartesia.InkIt`) |
| Accessibility | `kSecAttrAccessibleAfterFirstUnlock` (readable while locked; excluded from iCloud backup) |
| Swift binding | `SettingsStore.cartesiaAPIKey` |
| Legacy UserDefaults key | `cartesiaAPIKey` (migrated to Keychain on launch, then scrubbed) |

<Warning>
Ad-hoc “Sign to Run Locally” builds use a UserDefaults fallback (`secretFallback.cartesiaAPIKey`) because Keychain items do not survive unstable code signatures. Release and Developer ID builds use Keychain normally.
</Warning>

## Advisory validation

`APIKeyValidator` is the shared validation engine; `CartesiaKeyValidator` supplies the Cartesia-specific probe.

### Probe request

:::endpoint GET /voices
Credit-free list call used only to confirm authentication. Listing voices costs nothing and requires a valid key. HTTP status is the verdict — unlike the STT WebSocket handshake, which cannot distinguish offline from rejected keys.
:::

<RequestExample>

```http
GET https://api.cartesia.ai/voices?limit=1 HTTP/1.1
X-API-Key: sk_car_…
Cartesia-Version: 2026-03-01
```

</RequestExample>

### Verdict mapping

| HTTP status | `APIKeyValidator.State` | UI label |
| --- | --- | --- |
| 2xx | `verified` | Verified |
| 400, 401, 403 | `invalidKey` | Invalid key |
| Other / transport error | `couldNotVerify` | Couldn’t verify |
| Empty input | `idle` | (hidden) |

Behavioral details:

- **Debounced**: 0.6s after the last keystroke before the network call.
- **Cached**: A settled verdict for the same key is not re-fetched.
- **Non-blocking**: Onboarding and Settings never require `verified` to save or proceed.
- **Timeout**: 8 seconds on the probe request.

<Info>
Validation success does not clear `cartesiaKeyInvalid` or `cartesiaOutOfCredits`. Only a successful live transcription clears those flags.
</Info>

## Runtime usage

When dictation starts, `AppCoordinator` guards on a non-empty key, then constructs `CartesiaStreamingClient(apiKey: settings.cartesiaAPIKey)`.

| Guard | Notch message | Persistent flag |
| --- | --- | --- |
| Empty key at hotkey press | `Add your API key` | None |
| Invalid key during STT (401/403) | `Invalid API key` | `cartesiaKeyInvalid = true` |
| Out of credits (402 / `quota_exceeded` / `plan_upgrade_required`) | `Out of credits` | `cartesiaOutOfCredits = true` |
| Offline, 5xx, rate limit | Respective `STTFailure.notchMessage` | None (transient) |

`STTFailure` classification lives in `CartesiaStreamingClient` and covers WebSocket upgrade failures (`urlSession(_:task:didCompleteWithError:)`) and in-session `error` events.

## Service-issue flags and Home banner

Two UserDefaults-backed booleans record user-fixable Cartesia problems:

<ParamField body="cartesiaKeyInvalid" type="boolean">
Set when STT returns `STTFailure.invalidKey` (401/403). Cleared on the next successful non-empty transcription.
</ParamField>

<ParamField body="cartesiaOutOfCredits" type="boolean">
Set when STT returns `STTFailure.outOfCredits` (402 or billing error codes). Cleared on the next successful non-empty transcription.
</ParamField>

`SettingsStore.transcriptionIssue` derives the active problem:

```swift
var transcriptionIssue: ServiceIssue? {
    guard !cartesiaAPIKey.trimmingCharacters(in: .whitespaces).isEmpty else { return nil }
    if cartesiaKeyInvalid { return .keyInvalid }
    if cartesiaOutOfCredits { return .outOfCredits }
    return nil
}
```

When `transcriptionIssue` is non-nil, Home shows a full-width **Dictation is paused** banner (not a transient notch flash):

| Issue | Message | CTA | Action |
| --- | --- | --- | --- |
| `keyInvalid` | Your Cartesia API key is invalid. Update it to start dictating again. | Update your Cartesia key | Opens Settings → Dictation |
| `outOfCredits` | You're out of Cartesia credits. Review your plan… | Review your Cartesia plan | Opens [play.cartesia.ai/subscription](https://play.cartesia.ai/subscription) |

`keyInvalid` takes precedence over `outOfCredits` when both flags are set. No key on file suppresses the banner entirely (treated as setup, not a fault).

```mermaid
stateDiagram-v2
    [*] --> Healthy: key set, no flags
    Healthy --> KeyInvalid: STT 401/403
    Healthy --> OutOfCredits: STT 402 / quota_exceeded
    KeyInvalid --> Healthy: successful transcription
    OutOfCredits --> Healthy: successful transcription
    note right of KeyInvalid
        cartesiaKeyInvalid = true
        Home banner + notch flash
    end note
    note right of OutOfCredits
        cartesiaOutOfCredits = true
        Home banner + notch flash
    end note
```

## Verify configuration

<Steps>
<Step title="Check inline validation">

In onboarding or Settings, confirm **Verified** after pasting the key. **Couldn’t verify** usually means offline — retry when connected.

</Step>
<Step title="Run Try It or dictate">

Complete onboarding **Try It**, or hold/toggle your hotkey and speak into any editable field. A successful session pastes text and clears any prior service-issue flags.

</Step>
<Step title="Confirm Home is clear">

With a valid key and healthy account, the **Dictation is paused** banner should not appear. If it persists after updating the key, dictate once successfully — flags clear only on live STT success, not on validation alone.

</Step>
</Steps>

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Notch: `Add your API key` | Empty `cartesiaAPIKey` | Enter key in Settings → Dictation → Transcription |
| Notch: `Invalid API key`; Home banner | Rejected key at STT time | Replace key at [play.cartesia.ai/keys](https://play.cartesia.ai/keys), then dictate successfully |
| Notch: `Out of credits`; Home banner | Billing/quota exhausted | Review plan at [play.cartesia.ai/subscription](https://play.cartesia.ai/subscription) |
| Settings shows Verified but banner remains | Flags not cleared until live success | Complete one successful dictation after fixing the key |
| Validation: Couldn’t verify | Network or non-auth HTTP response | Check connectivity; validation is advisory |
| Key lost after local ad-hoc build | Unstable signature invalidates Keychain | Re-enter key, or use a stably signed build |

For STT failure classification, notch vs Home surfacing, and regression contracts, see [STT troubleshooting](/stt-troubleshooting).

## Related pages

<CardGroup>
<Card title="Quickstart" href="/quickstart">
First dictation walkthrough including onboarding API key entry and Try It.
</Card>
<Card title="Cartesia STT reference" href="/cartesia-stt-reference">
WebSocket contract, Ink-2 parameters, and `STTFailure` classification table.
</Card>
<Card title="Settings reference" href="/settings-reference">
All `SettingsStore` keys including service-issue flags and Keychain accounts.
</Card>
<Card title="STT troubleshooting" href="/stt-troubleshooting">
Diagnose transcription failures, notch messages, and Home card behavior.
</Card>
</CardGroup>
