<!-- AI agents: this is the Markdown version of a Reception documentation page. Index: /llms.txt -->

# Users & accounts

Connect ReceptionKit to your app’s sign-in, sign-out, and account deletion so your team knows who is chatting.

## Connect your sign-in {#connect}

After [configuration](/docs/get-started), add these calls to your existing account coordinator on the main actor:

| Account event | Call |
| --- | --- |
| Restore a session or complete sign-in | [identify(userId:name:email:)](/docs/sdk-reference#identify) |
| Sign out or switch accounts | [logout()](/docs/sdk-reference#logout), before the next user’s `identify` |
| Delete the account | Await [deleteData()](/docs/sdk-reference#delete-data) before discarding the session; keep it on failure and allow retry. |

Pass what your app knows about the signed-in user and `nil` for the rest. Do not invent placeholder names or pass anonymous or guest IDs.

```swift
Reception.shared.identify(userId: user.id, name: user.name, email: user.email)
```

```swift
Reception.shared.logout()
```

```swift
try await Reception.shared.deleteData()
```

Apps without sign-in need none of these calls; your team sees “User #…”. Identity verification comes later, with its own dashboard setup prompt.

## Verified users {#verified-users}

Your server signs a token, and Reception checks its signature before marking the customer as verified. Use this when your team acts on identity, for example for account changes, refunds, or data requests. App-supplied names, emails, and user IDs alone are labels anyone with your public App ID could send.

Those labels show **Unverified** in the conversation header and customer details until a token verifies them. For Owners, its tooltip links to the app’s identity verification section. Guests without a name, email, or user ID show no badge. Verified customers show an orange seal in the list, header, and details.

Every app already has an identity secret. In **Settings → Apps → your app → Identity verification**:

1. Choose **Copy setup prompt** and give it to your coding agent, or follow the examples below by hand.
2. Choose **Reveal** and store the secret on your server as `RECEPTION_IDENTITY_SECRET`. **Add it to your server** shows the Firebase command `firebase functions:secrets:set RECEPTION_IDENTITY_SECRET` (then paste the secret), **Dashboard → Edge Functions → Secrets** for Supabase, or a `.env` line.
3. Deploy your server changes and the app integration. The section shows when the last user was verified so you can check the result.

Only Owners can reveal and manage the secret. Each app has its own; store one per app if several apps share a server. Never put the secret in your app, its configuration files, or a coding-agent prompt. If your app has no backend, keep using `identify(userId:name:email:)`.

Verified identity stays on the server’s chat record until the owner marks it unverified or deletes it. Logout ends access through the old SDK session without removing its historical identity. Verification does not merge chats across devices.

### Create a token on your server {#create-token}

Reception accepts a JSON Web Token (JWT) signed with HS256 and your identity secret. Use the secret exactly as shown in the dashboard, including `ids_`; don't decode it.

| Claim | Value |
| --- | --- |
| `user_id` | Required. Your stable user ID as a string of up to 255 characters, used exactly as sent. Whole numbers up to 9,007,199,254,740,991 are accepted too; send larger IDs as strings. |
| `exp` | Required. When the token expires. Seven days works well for most apps. |
| `email` | Optional. The user's email address, up to 254 characters. Reception shows it as sent, so include only an address your app knows belongs to the user. |
| `name` | Optional. The name your team sees, up to 200 characters. |

A verified chat shows exactly the claims of the latest accepted token, so include `email` and `name` if your team should see them. `null` counts as not set. Reception ignores other claims and rejects tokens whose `nbf` lies in the future. Tokens can have up to 4,096 characters, far more than these claims need.

Create tokens only in an endpoint of your API that requires sign-in, for example `GET /me/reception-token`. Take the user from your authenticated session, never from the request body or query; otherwise anyone could request a token for any user. Never create tokens inside the app.

#### Firebase {#firebase}

Add this function to your Cloud Functions and deploy it with `firebase deploy --only functions`. Cloud Functions require the Blaze plan.

```js Firebase · functions/index.js
const { onCall, HttpsError } = require("firebase-functions/v2/https");
const { defineSecret } = require("firebase-functions/params");
const jwt = require("jsonwebtoken");

const receptionSecret = defineSecret("RECEPTION_IDENTITY_SECRET");

// Signs a Reception identity token for the signed-in Firebase user.
exports.receptionToken = onCall({ secrets: [receptionSecret] }, (request) => {
  const { uid, token } = request.auth ?? {};
  if (!uid || token.firebase.sign_in_provider === "anonymous") throw new HttpsError("unauthenticated", "Sign in first.");
  const claims = { user_id: uid, email: token.email_verified ? token.email : undefined, name: token.name };
  return { token: jwt.sign(claims, receptionSecret.value(), { algorithm: "HS256", expiresIn: "7d" }) };
});
```

Install `jsonwebtoken` in your functions folder: `npm install jsonwebtoken`.

#### Supabase {#supabase}

Add this Edge Function and deploy it with `supabase functions deploy reception-token`.

```ts Supabase · supabase/functions/reception-token/index.ts
import { createClient } from "npm:@supabase/supabase-js@2";
import { SignJWT } from "npm:jose@5";

// Signs a Reception identity token for the signed-in Supabase user.
Deno.serve(async (req) => {
  const jwt = req.headers.get("Authorization")?.replace("Bearer ", "") ?? "";
  const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!);
  const { data: { user } } = await supabase.auth.getUser(jwt);
  if (!user || user.is_anonymous) return new Response("Sign in first.", { status: 401 });
  const token = await new SignJWT({
    user_id: user.id,
    email: user.email_confirmed_at ? user.email : undefined, // Requires "Confirm email" in your Auth settings.
    name: user.user_metadata.full_name ?? user.user_metadata.name,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setExpirationTime("7d")
    .sign(new TextEncoder().encode(Deno.env.get("RECEPTION_IDENTITY_SECRET")!));
  return Response.json({ token });
});
```

#### Node.js {#node}

```js Node.js · jsonwebtoken
import jwt from "jsonwebtoken";

// In a signed-in route: res.json({ token: receptionToken(req.user) })
export function receptionToken(user) {
  return jwt.sign(
    { user_id: String(user.id), email: user.email, name: user.name },
    process.env.RECEPTION_IDENTITY_SECRET,
    { algorithm: "HS256", expiresIn: "7d" },
  );
}
```

#### Other languages {#other-languages}

```js Node.js · jose
import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.RECEPTION_IDENTITY_SECRET);

// In a signed-in route: res.json({ token: await receptionToken(req.user) })
export function receptionToken(user) {
  return new SignJWT({ user_id: String(user.id), email: user.email, name: user.name })
    .setProtectedHeader({ alg: "HS256" })
    .setExpirationTime("7d")
    .sign(secret);
}
```

```python Python · PyJWT
import os
from datetime import datetime, timedelta, timezone

import jwt


# In a signed-in view: {"token": reception_token(request.user)}
def reception_token(user):
    claims = {
        "user_id": str(user.id),
        "email": user.email,
        "name": user.name,
        "exp": datetime.now(timezone.utc) + timedelta(days=7),
    }
    return jwt.encode(claims, os.environ["RECEPTION_IDENTITY_SECRET"], algorithm="HS256")
```

```php PHP · firebase/php-jwt
<?php

use Firebase\JWT\JWT;

// In a signed-in route: ['token' => receptionToken($request->user())]
function receptionToken(object $user): string
{
    $claims = [
        'user_id' => (string) $user->id,
        'email' => $user->email,
        'name' => $user->name,
        'exp' => time() + 7 * 24 * 60 * 60,
    ];
    return JWT::encode($claims, getenv('RECEPTION_IDENTITY_SECRET'), 'HS256');
}
```

```ruby Ruby · ruby-jwt
require "jwt"

# In a signed-in action: render json: { token: reception_token(current_user) }
def reception_token(user)
  claims = {
    user_id: user.id.to_s,
    email: user.email,
    name: user.name,
    exp: Time.now.to_i + 7 * 24 * 60 * 60
  }
  JWT.encode(claims, ENV.fetch("RECEPTION_IDENTITY_SECRET"), "HS256")
end
```

```go Go · golang-jwt
import (
	"os"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// Call it from a signed-in handler with the user from your session.
func receptionToken(userID, email, name string) (string, error) {
	claims := jwt.MapClaims{
		"user_id": userID,
		"exp":     time.Now().Add(7 * 24 * time.Hour).Unix(),
	}
	if email != "" {
		claims["email"] = email
	}
	if name != "" {
		claims["name"] = name
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(os.Getenv("RECEPTION_IDENTITY_SECRET")))
}
```

### Pass the token to ReceptionKit {#pass-token}

Call `Reception.shared.identify(token:)` after `Reception.configure(appId:)`: once after a user signs in, and on every app launch while a user is signed in. ReceptionKit keeps the token in memory only and sends it with its next chat request: when the app launches or returns to the foreground, when the chat opens, or with the first message. The call does not send the token immediately, but switching users also requests remote sign-out of the old session.

Place this in your account code, where your app knows the signed-in user. `YourAPI` stands for your existing API client and `signedInUserId` for its current user. Before calling `identify(token:)`, check that the same user is still signed in, so a sign-out during the request can't attach the previous user's token:

```swift
import ReceptionKit

@MainActor
func connectSupportIdentity(api: YourAPI, userId: String) async {
    do {
        let token = try await api.receptionToken() // Your authenticated endpoint.
        guard api.signedInUserId == userId else { return } // Signed out or switched meanwhile.
        Reception.shared.identify(token: token)
    } catch {
        // Try again on the next launch; the rejected token does not change existing verification.
    }
}
```

With the Firebase or Supabase function from above:

```swift Firebase
import FirebaseAuth
import FirebaseFunctions
import ReceptionKit

@MainActor
func connectSupportIdentity() async {
    struct Response: Decodable { let token: String }
    guard let uid = Auth.auth().currentUser?.uid else { return }
    let receptionToken: Callable<[String: String], Response> = Functions.functions().httpsCallable("receptionToken")
    // A sign-out or account switch while waiting must not attach the previous user's token.
    guard let response = try? await receptionToken.call([:]), Auth.auth().currentUser?.uid == uid else { return }
    Reception.shared.identify(token: response.token)
}
```

```swift Supabase
import Supabase
import ReceptionKit

@MainActor
func connectSupportIdentity(supabase: SupabaseClient) async {
    struct Response: Decodable { let token: String }
    guard let userId = supabase.auth.currentUser?.id else { return }
    // A sign-out or account switch while waiting must not attach the previous user's token.
    guard let response: Response = try? await supabase.functions.invoke("reception-token"),
          supabase.auth.currentUser?.id == userId else { return }
    Reception.shared.identify(token: response.token)
}
```

A chat that started without identity, or with `identify(userId:)` for the same user ID, becomes verified and keeps its history. When the token belongs to someone else, ReceptionKit logs out the previous chat right away, and the next message starts a new chat for the new user.

`identify(token:)` never replaces `Reception.shared.logout()`. Call logout when a user signs out, so the next person on the device can't continue the previous chat. Once a chat is verified, `identify(userId:name:email:)` no longer changes its identity; send a new token to update the name or email. See [Login and logout](/docs/accounts#sign-out).

### When a token is rejected {#rejected-token}

ReceptionKit drops a token and emits `ReceptionEvent.identityRejected(_:)` only when Reception returns `identity_token_invalid`, `identity_token_expired`, or `identity_not_configured`. These rejections do not remove an already verified identity. An unverified chat can continue if your app allows it; otherwise it shows the sign-in notice. A rejected registration leaves the send ready to retry.

A token for another user resets the old session and stays staged for the next send. This switch, including one caused by `identity_mismatch`, does not emit `identityRejected`.

Fetch a new token when the old one has expired. For other codes, fix the configuration of your server instead of retrying:

```swift
Reception.shared.onEvent = { event in
    if case .identityRejected(let error) = event, error.code == "identity_token_expired",
       let userId = api.signedInUserId {
        Task { await connectSupportIdentity(api: api, userId: userId) }
    }
}
```

`onEvent` holds one handler; merge this branch into your existing handler. See [Events](/docs/events).

The SDK ignores `ids_` secrets, tokens over 4,096 characters, and tokens with an unreadable `user_id`. These inputs emit no event. Other invalid tokens can reach Reception and emit `identityRejected`.

### Only verified users can chat {#only-verified}

Turn on **Only verified users can chat** in the same dashboard section to allow messages and photos only from verified device sessions. You can turn it off at any time.

- Unverified users can open chat and read earlier messages. The composer shows "Sign in to send messages." once Reception reports that verification is required. An unused chat learns this after its first send attempt.
- Staging a new token hides the notice. Reception checks it on the next request; sending still requires verification. Failed messages keep their Retry action.
- Existing unverified chats keep their history and become verified with the first valid token for the same user.

> **Important:** Set up `identify(token:)` before turning this on so unverified users can send. Already verified sessions keep access.

### Regenerate the secret {#regenerate}

Regenerate the secret if it may have leaked, or when someone who could see it leaves your team.

1. Select **Regenerate secret** and confirm. Reception shows the new secret.
2. Update `RECEPTION_IDENTITY_SECRET` on your server right away and deploy.

Tokens signed with the old secret stop working immediately. ReceptionKit reports them with `identityRejected`, and your app fetches a fresh token on the next launch. Chats that are already verified stay verified. If the secret leaked, also select **Also mark existing users as unverified** in the confirmation. Those users become verified again when your app sends a new valid token.

There is no switch to remove the identity secret. Stopping tokens does not remove verification from existing chats. New chats with app-supplied identity show as unverified without a token; guests without identity show no badge. Registration is rejected when only verified users can chat. To remove existing verification, regenerate the secret with **Also mark existing users as unverified** selected.

## Customer details {#metadata}

### Identity fields {#identify}

`identify(userId:name:email:)` replaces all three staged identity fields. `userId` is required at the call site; `name` and `email` default to `nil`. Values are used exactly as supplied, without trimming. To change only a name, supply the current user ID and email again. Metadata and push registration stay unchanged.

| Field | Limit |
| --- | --- |
| User ID | 255 characters |
| Name | 200 characters |
| Email | 254 characters. Reception does not check the format; the dashboard offers **Send email** only for a valid address. |

Some emoji count as two characters. ReceptionKit leaves out a value that exceeds its limit so it does not block chat.

Passing `nil` omits that field from requests: it stays unset on registration and unchanged on an existing device. Passing all three fields as `nil` does not clear server identity or create a new session. Call [logout before switching users](#sign-out). Once a chat is verified, send a new token to update its identity.

### Reception.shared.setMetadata(_:) {#setmetadata}

The public signature is:

```swift
@MainActor
public func setMetadata(_ metadata: [String: String])
```

The single unlabeled argument is the complete dictionary of custom fields. There is no default argument; a fresh support session starts with an empty dictionary. Calling this method leaves the three identity fields and push registration unchanged.

In your account or subscription update handler, after configuration and on the main actor:

```swift
Reception.shared.setMetadata([
    "plan": "pro",
    "provider": "App Store",
    "subscription_status": "active"
])
```

Metadata keys and values must be strings. Convert numbers or booleans to your intended display strings before passing them.

Metadata can have up to 20 entries. Keys use 1–64 characters and values up to 512 characters; some emoji count as more than one character. Entries outside those length limits are omitted. Supplying more than 20 entries replaces the entire dictionary with an empty one, clearing saved metadata on the next successful synchronization.

These fields are supplied by your app. ReceptionKit does not fetch subscription data from a billing provider just because you set a `provider` field. Keep the fields relevant to support; see [Privacy and data](/docs/privacy-and-data) for data handling.

### Replace or clear metadata {#clear-metadata}

Every call replaces the entire staged dictionary. The next successful device registration or update sends that dictionary, replacing the server’s metadata too. Include every key you want to keep.

For example, in a main-actor subscription update handler, this leaves only `plan` after synchronization:

```swift
Reception.shared.setMetadata(["plan": "free"])
```

In the same handler, clear all custom fields with:

```swift
Reception.shared.setMetadata([:])
```

An empty dictionary clears server metadata on the next successful device update. This differs from `nil` identity fields, which are omitted and leave server values unchanged. An empty string value retains its key; omit a key from the replacement dictionary to remove it.

### When updates reach the server {#when-updates-reach-the-server}

Both setters save the latest values locally. They are synchronous and do not send requests, start support, or provide a completion callback. Multiple calls before a device request leave the latest staged values to be sent.

On a fresh session, registration begins with the first valid send attempt. Merely configuring ReceptionKit, identifying a user, setting metadata, or opening an unused empty chat does not register a device. ReceptionKit may fetch public appearance separately while active.

After support has started, values synchronize during normal chat use and foreground updates. An offline change remains on the device until an update succeeds; check the dashboard to confirm it arrived. There is no separate identity-flush method.

### Where details appear {#dashboard}

Your team sees identity beside conversations and in customer details, and metadata under **Custom fields**. The Users page searches by name, email, or user ID and lists registered devices separately. Team-assigned display names are stored separately; changing the SDK-supplied `name` does not overwrite them. See [Verified users](#verified-users) for the badge meanings.

App language is configured separately. See [Language](/docs/language) for automatic selection, custom language pickers, and supported codes.

## Sign-out and account switches {#sign-out}

ReceptionKit does not authenticate your app’s accounts or select conversations by user ID. Each device session has its own history, even when two devices use the same user ID. Changing an unverified user ID without logout can attach the existing conversation to another person.

Use your existing account coordinator to serialize account transitions. Keep support unavailable while ownership is unresolved. Configure before calling the SDK, and keep calls on the main actor; identity and metadata calls before configuration do nothing.

### Restore a signed-in session {#restore}

Configure ReceptionKit with the same App ID, restore your app’s account, then identify the confirmed user and set its complete metadata dictionary. Forward an available APNs token through your [push registration flow](/docs/push-notifications) before enabling support. With identity verification, also fetch a fresh token on every launch; tokens stay in memory only.

Keep the existing support session when restoring the same account. Calling logout on every launch discards access to its history. Configuration loads the locally stored session; identifying the same user cannot recover another device’s history.

Your app must track whether the restored account matches the previous one; ReceptionKit has no public current-account getter. If restoration confirms sign-out, reset support before enabling guest chat. If it finds a different account, follow [Switch accounts](#switch-accounts). A temporary restoration failure leaves ownership unresolved: preserve the session for retry.

### After successful login {#login}

Wait for login to succeed before applying identity and metadata or fetching a verification token. Failed or canceled login leaves the existing support context intact.

For the same person moving from guest use to a signed-in account, identify the current session without logout if you want to retain the guest conversation. If it belongs to another person, or guest history must stay separate, use the account-switch flow.

### Log out {#logout}

`logout()` is synchronous, takes no arguments, and does not throw. Call it when your app actually transitions to signed out.

Logout cancels active support work and resets the local session, including:

- Reception activation and session credentials.
- Staged user ID, name, email, metadata, identity token, and push token/environment.
- Cached chat history, pending messages, draft text, and unsent action receipts.
- The observable unread count, which becomes zero.

The SDK sheet is dismissed. If your app presents `ReceptionChatView` using its own sheet, cover, or navigation route, your app owns dismissing that container during logout. See [Entry point](/docs/entry-point).

Project configuration remains available, so there is no need to configure again for the same app after logout. Global appearance settings, the language override, badge preference, and event callback also remain; update any account-dependent choices through your own lifecycle code. See [Language](/docs/language).

With default badge handling, resetting the unread count also clears the app icon badge and delivered Reception notifications. If `Reception.shared.updatesAppBadge` is `false`, your app owns those actions. See [Unread messages](/docs/unread-messages).

Logout also asks Reception to end the old session and remove its push registration. It leaves the device and conversation history on the server.

The local reset is immediate; remote sign-out can be delayed while offline or after a failure. Returning from `logout()` does not confirm it has finished, and notifications already being sent can still arrive. ReceptionKit retries remote sign-out while the app is active. Local secure-storage failures can prevent recovery after relaunch. Logout does not end sessions on other devices.

The next valid send attempt starts a fresh support device session. Identifying the previous user again after logout does not recover that user’s old session or conversation history.

### Switch accounts {#switch-accounts}

Complete the following steps as one ordered account transition:

1. Prevent support interaction while switching and use your existing authentication flow to establish the target account.
2. Once the switch succeeds, call `Reception.shared.logout()` before staging the target account’s identity.
3. Call `Reception.shared.identify` with the new account’s user ID, name, and email.
4. Call `Reception.shared.setMetadata` with the new account’s complete metadata dictionary.
5. Forward the APNs token again through your existing registration flow, because logout cleared the staged token.
6. Finish any host-owned chat dismissal and enable support for the new account.

Place this helper in your account coordinator and call it after a successful account switch, before exposing the new account’s support UI:

```swift
import ReceptionKit

@MainActor
func switchSupportAccount(
    userId: String,
    name: String?,
    email: String?,
    metadata: [String: String]
) {
    Reception.shared.logout()
    Reception.shared.identify(userId: userId, name: name, email: email)
    Reception.shared.setMetadata(metadata)
}
```

The helper has no suspension point between reset and staging the new identity. Keep asynchronous authentication outside it, and prevent overlapping account transitions or stale callbacks from applying the previous account’s data afterward.

Do not reset an otherwise valid session just because an attempted switch fails. If your authentication flow already signed out the previous account before attempting the new login, perform the support logout at that actual sign-out boundary; a later failed login then leaves support in its fresh signed-out state.

With identity verification, a token for a different user also resets the old chat. Keep the explicit logout at sign-out and before identifying the next account.

## Delete support data {#delete-data}

Use `Reception.shared.deleteData()` to delete the current device's support conversations, messages, and images, then reset ReceptionKit locally. Your app supplies the confirmation, progress, and failure UI.

### Scope: the current device {#scope-the-current-device}

Deletion uses the session of the currently configured ReceptionKit device. It does not look up other registrations with the same `userId`, name, or email. Another device, another app, or a previous session on the same phone remains separate. Changing labels with `identify(userId:name:email:)` does not select another registration. A token for another user can reset the session, so await deletion before calling `identify(token:)` for another account.

For the current device, Reception removes:

- The device record, including its identity, metadata, push registration, and review-request state.
- All its conversations, including closed ones, with their messages and action-click receipts.
- Its images, including images agents sent in those conversations and unfinished uploads.

Deletion does not remove your app's account, data in your own backend, other devices' support data, or messages already delivered to Telegram. It does not purge backups or copies outside Reception. See [Privacy and data](/docs/privacy-and-data).

### Call the deletion API {#call-the-deletion-api}

The public declaration on the main-actor-isolated `Reception` type is:

```swift
public func deleteData() async throws
```

Call it from a `@MainActor` host-app coordinator or model in your account-deletion flow, and await it. It takes no arguments and returns no receipt. With an existing session, ReceptionKit asks Reception to delete the device. This also works for a blocked device and after a support reset.

> **Important:** When an account is deleted, call `deleteData()` instead of `Reception.shared.logout()`, and await it before switching the configuration or discarding the account. Logout ends the session that deletion needs; afterwards, the SDK can no longer delete that old session’s server data. Do not put logout in a `defer` block or an unconditional failure path.

After completion, the SDK resets locally as logout does: it clears the identity, metadata, staged identity and push tokens, cached chat, drafts, pending sends, action-click receipts, cursor, support activation, and unread count. The SDK sheet is dismissed; if your app presents `ReceptionChatView` itself, your app owns dismissal. Configuration, appearance settings, language override, and the event handler remain. A later send can register a new device, so apply identity, metadata, and push token again if the person keeps using your app.

### When deletion completes {#when-deletion-completes}

| Situation | SDK behavior |
| --- | --- |
| Reception confirms the deletion | Resets locally and emits `.dataDeleted`. |
| No session exists, for example because support was never used on this device | Sends no request, resets locally, and emits `.dataDeleted`. |
| The session has ended or the App ID is unavailable | Completes locally and emits `.dataDeleted`; this does not confirm historical data was erased. |
| Any other failure | Throws `ReceptionError` and keeps the session so you can retry; no reset and no event. |

Before `Reception.configure(appId:)` has run, the call does nothing: it sends no request, resets nothing, and emits no event.

Completion and `.dataDeleted` are not deletion receipts. An ended session may leave historical records that the SDK can no longer delete. An unavailable App ID also does not certify that a service purge has finished.

After a logout, the device has no session left, so `deleteData()` completes locally while the earlier conversation stays on the server. Delete that device in the [dashboard](/docs/dashboard#delete-a-device) if its data must go too.

### Show confirmation, progress, and retry {#show-confirmation-progress-and-retry}

Put the following model and view in your host app's settings feature. This is a complete support-only deletion control for iOS 17 or later; configure ReceptionKit through your normal app setup first. The model performs the request, and the view shows an actionable failure instead of silently logging an error.

```swift
import Observation
import ReceptionKit
import SwiftUI

@MainActor @Observable
final class SupportDeletionModel {
    private(set) var isDeleting = false
    private(set) var completed = false
    private(set) var errorMessage: String?

    func delete() async {
        guard !isDeleting, !completed else { return }
        isDeleting = true
        errorMessage = nil
        defer { isDeleting = false }

        do {
            try await Reception.shared.deleteData()
            completed = true
        } catch let error as ReceptionError {
            if error.code == "delete_failed" {
                errorMessage = "Some support images could not be deleted. Please retry."
            } else if error.code == "connection_failed" {
                errorMessage = "Could not reach support. Check your connection and retry."
            } else {
                errorMessage = "Support deletion could not finish. Please retry."
            }
        } catch {
            errorMessage = "Support deletion could not finish. Please retry."
        }
    }
}

@MainActor
struct DeleteSupportDataView: View {
    @State private var model = SupportDeletionModel()
    @State private var showsConfirmation = false

    var body: some View {
        Form {
            Section {
                Text("Delete support conversations and images for this device registration.")
                if model.completed {
                    Text("Support deletion flow completed for this device. Your app account is unchanged.")
                } else {
                    Button("Delete support data", role: .destructive) {
                        showsConfirmation = true
                    }
                    .disabled(model.isDeleting)
                }
                if model.isDeleting {
                    ProgressView("Deleting support data…")
                }
                if let errorMessage = model.errorMessage {
                    Text(errorMessage)
                        .foregroundStyle(.red)
                        .accessibilityLabel("Deletion failed. " + errorMessage)
                    Button("Retry deletion") {
                        Task { await model.delete() }
                    }
                    .disabled(model.isDeleting)
                }
            }
        }
        .navigationTitle("Support data")
        .confirmationDialog("Delete support data for this device?",
                            isPresented: $showsConfirmation,
                            titleVisibility: .visible) {
            Button("Delete support data", role: .destructive) {
                Task { await model.delete() }
            }
            Button("Cancel", role: .cancel) { }
        } message: {
            Text("This removes this registration's support conversations and images. It cannot be undone. Other devices and your app account are unchanged.")
        }
        .interactiveDismissDisabled(model.isDeleting)
    }
}
```

A further `Reception.shared.logout()` call is unnecessary after completion: deletion has already reset the session.

When embedding this in an account-deletion flow, keep the retryable session until this step completes, then continue your own account operation. This is not an atomic transaction with your account backend: a later host-account failure does not restore deleted support data. Disable account switching, logout, and new support sends in the surrounding host UI while deletion is pending; the example only controls its own buttons and interactive sheet dismissal.

### Handle ReceptionError {#handle-receptionerror}

`code` is a read-only `String` with the backend error code when available, or an SDK request error code. It is not a Swift enum; handle unknown strings with a fallback. `status` is a read-only `Int`: the HTTP status, or `0` when no valid HTTP response was received. Supply your own localized UI text instead of depending on `localizedDescription`.

| Example | Meaning and response |
| --- | --- |
| `connection_failed`, status `0` | Transport failure, such as an unavailable server. Keep the session and offer retry. |
| `cancelled`, status `0` | The request task was canceled. Do not assume the server operation was rolled back. |
| `secure_storage_unavailable`, status `0` | The device's secure storage could not be read, for example before the first unlock after a restart. Retry later. |
| `invalid_configuration`, status `0` | The SDK's service configuration is invalid. Contact Reception. |
| `invalid_response` | The response could not be interpreted. Completion on the server is uncertain; retry. |
| `delete_failed`, status `503` | Deletion could not be completed. Keep the session and retry; some data may already have been removed. |
| Another code | Display a fallback failure and preserve the session. Contact the Reception team if the error persists. |

Retry a failed deletion with the same session. If it continues to fail, contact Reception with what happened and the time of the attempt. Include the error code if available. A lost response can leave the outcome uncertain; retry as described above.

### Session changes and concurrent work {#session-changes-and-concurrent-work}

The SDK captures the current session when deletion starts. If `Reception.shared.logout()` or configuring another app replaces that session before the response arrives, completion does not reset the replacement session, but it still emits `.dataDeleted`.

This protection does not serialize your app's account lifecycle. Do not identify a different user while deletion is pending. Await the operation before moving to the next account, guard your completion handling against stale account transitions, and avoid overlapping first sends, photo uploads, and deletion.

### Observe completion {#observe-completion}

`Reception.shared.onEvent` is a single optional callback, accessed on the main actor. `deleteData()` invokes it synchronously with `.dataDeleted` before returning on a completing path. It does not fire for thrown failures or ordinary logout. The event has no device identifier or server-deletion receipt.

In your host app's existing `@MainActor` event setup, add a `.dataDeleted` branch to refresh host-owned support settings or status. Keep account-deletion sequencing in the awaited call. Installing another handler replaces the previous callback; merge this behavior with existing event handling. See [Events](/docs/events).

### Choose the right operation {#choose-the-right-operation}

| Operation | Result |
| --- | --- |
| `Reception.shared.logout()` | Clears the local SDK session and requests remote sign-out and removal of its push registration; retains server history. Use for ordinary sign-out. See [Login and logout](/docs/accounts#logout). |
| `Reception.shared.deleteData()` | Deletes the current device and its content, then resets locally. Use for account deletion instead of logout. |
| Dashboard **Reset support** | Owner action that deletes the device's conversations and images but keeps the device, its session, identity, metadata, push token, and review-request timestamp. Clears the block and chat activity. |
| Dashboard device deletion | Owner action that removes the selected device and its support content. The app starts a new chat with its next send. |
| Your app's account deletion | Your own workflow, including host data and any other support registrations that must be handled separately. |

## Test your integration {#test}

Use a test app and synthetic accounts and conversations:

1. Sign in as A, stage identity and metadata, and send a message. Check the values in the dashboard. Replace metadata and then send `[:]`; reopen the started chat online and check that removed fields disappear after each update.
2. Relaunch and restore A without logout. Its history should remain. Try a failed login or switch and confirm the account that remains signed in keeps its session.
3. Switch to B. A’s chat should disappear and unread count reset. Send as B and confirm a separate device record; A’s server history should remain.
4. With APNs working, confirm a reply to A arrives before logout. After remote sign-out completes, replies to A should stop notifying that device and replies to B should arrive. Also check recovery after offline logout and relaunch; notifications already in flight can still arrive.
5. For verification, store the test app’s secret on your development server, sign in, and send a message. Check the orange seal. Try a token signed with another secret: expect `identity_token_invalid`; an unverified chat can continue only if the app allows it. Pass B’s token while A’s chat is active and check that the old chat is replaced.
6. Register two devices with the same user ID, send messages and images, then delete from one. Only that device and its conversations should disappear from the dashboard; its cached chat and unread state should clear.
7. Try deletion offline. Show the failure, retain the session, and retry after reconnecting. An unused session sends no deletion request but still emits `.dataDeleted`. A delayed deletion response after a session switch must preserve the replacement session, and host completion handling must not sign out the new account.

## Troubleshooting {#troubleshooting}

| Symptom | What to check |
| --- | --- |
| Identity or metadata is stale | Configure first, run on the main actor, allow a device update to succeed, and inspect the correct app and device. Setters do not synchronize immediately. |
| B sees A’s history | Log out before identifying B, dismiss host-owned chat containers, and prevent stale callbacks from applying A’s data. |
| A signed-in customer shows Unverified | Pass a fresh token after sign-in and on each launch. Check `identityRejected` if the token was rejected. Initial setup supplies labels only. |
| Deletion failed | Keep the session and offer retry. See [Handle ReceptionError](#handle-receptionerror). |
| History remains after logout | Logout retains server data. Use [Delete support data](#delete-data) before ending the session. |

| Code | Meaning | What to do |
| --- | --- | --- |
| `identity_token_invalid` | Reception couldn't verify the token: wrong or old secret, not HS256, missing `exp` or `user_id`, a claim with the wrong type or length, `nbf` in the future, or more than 4,096 characters. | Sign with the current secret exactly as shown, use HS256, and check the [claims](#create-token). |
| `identity_token_expired` | `exp` has passed. | Fetch a fresh token and check your server's clock. |
| `identity_not_configured` | Reception could not find the app’s identity secret. | Every app should already have one. Check the configured App ID and contact Reception if this persists. |
| `identity_mismatch` | The chat on this device belongs to a different user. | ReceptionKit resets the old session and keeps the new token for the next send. No `identityRejected` event is emitted. |
| `verification_required` | Only verified users can chat, and this device session is unverified. | Call `identify(token:)` after sign-in and on every launch. |

The first three codes arrive in the `identityRejected` event. Tokens ignored locally, such as overlong tokens or tokens with an unreadable `user_id`, emit no event. The most common mistakes are `sub` instead of `user_id`, a secret that was decoded or copied incompletely, a secret from another app, and a missing `exp`.

Something not working? Write to us in the Support chat. We're happy to help. Briefly describe what happens and any error message you see.
