# Claiming Guest Users

A visitor often starts chatting with an AI agent as a [guest user](https://getstream.io/chat/docs/javascript/authless-users/) and signs up later. Claiming the guest moves its conversations to the new account: your backend names the guest and the regular user, and Stream moves the guest's channel memberships, the channels it created, its messages, reactions and threads to that user. The guest is deactivated.

**Claiming guest users is in beta.** Stream enables it per app: [contact support](https://getstream.io/contact/support/) to turn it on. Until it is enabled, the endpoint returns `403`.

## Why it is server-side only

Only your backend, which just authenticated the person, knows which guest session they came from. Chat cannot tell that a sign-up belongs to a given guest, and a guest token is a weak credential: it carries only a user ID and never expires. The endpoint therefore only accepts server-side requests, and your backend decides when a claim may happen, for example only after the email address is verified.

## Claim a guest

| Field           | Required | Description                                                                                                                     |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `guest_user_id` | Yes      | The guest to claim. It must exist, have the `guest` role, and not be deleted, deactivated or already claimed by another user.   |
| `user_id`       | Yes      | The regular user that claims the guest and receives its data. It must exist, not be a guest, and not be deleted or deactivated. |

Create or upsert the regular user first, then claim:

```go label="Go"
resp, err := client.ClaimGuestUser(ctx, &getstream.ClaimGuestUserRequest{
	GuestUserID: guestID,
	UserID:      "alice",
})
if err != nil {
	log.Fatal(err)
}
taskID := resp.Data.TaskID
```

```bash label="cURL"
curl -X POST "https://chat.stream-io-api.com/users/claim_guest?api_key=${API_KEY}" \
  -H "Authorization: ${SERVER_TOKEN}" \
  -H "stream-auth-type: jwt" \
  -H "Content-Type: application/json" \
  -d '{"guest_user_id": "'"${GUEST_ID}"'", "user_id": "alice"}'
```

The response carries the ID of the task that moves the guest's data:

```json
{
  "task_id": "...",
  "duration": "12.31ms"
}
```

## Wait for the move before handing out the token

The claim itself is instant, but the guest's data is moved afterwards by a background task. A client that opens the conversation before the task completes still sees the guest as the author of its messages, and can miss channels that only become readable once their ownership has moved.

So your backend should poll [`GET /tasks/{task_id}`](https://getstream.io/docs/platform/async-operations/) until `status` is `completed`, and only then give the client the new user's token:

```go label="Go"
task, err := client.GetTask(ctx, taskID, &getstream.GetTaskRequest{})
if err != nil {
	log.Fatal(err)
}
fmt.Println(task.Data.Status)
```

When the task is completed, its `result` counts what it moved:

```json
{
  "status": "completed",
  "result": {
    "channels_joined": 3,
    "channels_already_member": 1,
    "channels_ownership_transferred": 4,
    "messages_reassigned": 182,
    "reactions_reassigned": 9,
    "reactions_dropped": 1,
    "drafts_reassigned": 1,
    "pending_messages_reassigned": 0
  }
}
```

A large move runs in several steps, and the counters describe only the last one, so they can be lower than the totals.

A client that is already connected as the claiming user receives `user.claimed` over the WebSocket when the move completes. Moved messages keep their `updated_at` and no `message.updated` is sent, so refetch the user's channels and messages when that event arrives.

## Retrying a claim

A claim is idempotent for the same pair of users. Claiming the same guest again with the same `user_id` returns the same `task_id`:

- While the task is queued or running, no second move starts.
- When the task failed, the repeat claim starts it again, and it resumes where it stopped.
- When the task completed, the repeat claim runs the move again, which finds nothing left to move.

A repeat claim does not send a second `user.claimed` webhook. If two different users claim the same guest at the same time, only one succeeds; the other gets `403`.

## What happens to the guest

The claim deactivates the guest in the same step, which ends its session. The next request with the guest's token fails, and the guest's open connections stop receiving channel events. The socket is not closed, but reconnecting fails.

Claimed guests stay deactivated. A guest that your app deactivated before the claim cannot be claimed.

## What is moved

| Data                     | How it moves                                                                                                                                                                                                                                                                                               |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Channel memberships      | The guest is removed from each of its channels. Where the user is not a member yet, it joins with the guest's member data: channel role, custom data, invite state, archived, pinned, hidden, push preferences and `hide_messages_before`. Where the user is already a member, its own membership is kept. |
| Channel bans             | A guest's active ban in a channel follows the claim, so signing up does not lift it. The user gets the stricter of the two bans: a ban wins over a shadow ban, and the later expiry wins, with a permanent ban being the latest.                                                                           |
| Channels it created      | The user becomes the channel's creator, which keeps owner access on `agent` channels.                                                                                                                                                                                                                      |
| Messages                 | The user becomes the author of all the guest's messages, in every channel. `updated_at` does not change.                                                                                                                                                                                                   |
| Reactions                | Moved to the user. When the user already has a reaction of the same type on the same message, the guest's is dropped.                                                                                                                                                                                      |
| Threads                  | The user takes over the guest's thread participations. Where the user already participates, the user's participation is kept.                                                                                                                                                                              |
| Drafts                   | Moved to the user. Where the user already has a draft for the same channel or thread, the user's draft is kept.                                                                                                                                                                                            |
| Pending messages         | Moved to the user.                                                                                                                                                                                                                                                                                         |
| Pins and thread creation | Moved in the channels where the guest is a member or wrote messages.                                                                                                                                                                                                                                       |

The user starts with no unread messages in the channels it joins through the claim. The guest's unread state is not carried over.

### What is not moved

- Message reminders.
- User mutes, channel mutes, blocks, app-level bans and devices: these belong to the guest identity.
- Hidden-channel state beyond the membership's `hidden` flag.
- Mentions: existing messages that mention the guest keep the guest's ID.
- Edits the guest made to other users' messages keep the guest as their editor.
- Polls and votes, shared locations, scheduled messages, files and message history.
- Pins and threads the guest made in a channel where it has neither a membership nor any message left.

### Things to know

- **Distinct channels keep the guest in their ID.** A [distinct channel](https://getstream.io/chat/docs/go-golang/creating-channels/) encodes its member list in its ID. Its membership moves, but its ID does not change, so a later distinct get-or-create for the user and your agent creates a new channel instead of finding the moved one.
- **A user deactivated during the move receives nothing more.** If you deactivate the claiming user while its task runs, the task stops. What it already moved stays with the user. Reactivate the user and claim again with the same `guest_user_id` and `user_id` to finish the move.
- **Analytics are not rewritten.** Use the `user.claimed` webhook to link the guest's ID to the user's ID in your own data.

## Events

| Event                               | Sent to                                             | When                                                                                                         |
| ----------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `user.claimed`                      | Webhooks                                            | Once, when the guest is first claimed. It carries the guest as `user` and the claiming user as `claimed_by`. |
| `user.claimed`                      | The claiming user's own connections, WebSocket only | When the move completes, including the run of a repeat claim. Refetch channels and messages when it arrives. |
| `member.removed`, `member.added`    | Clients watching each channel, and webhooks         | For each channel the guest is a member of, as the move removes the guest and adds the user.                  |
| `notification.removed_from_channel` | The guest's connections                             | For each channel the guest is removed from.                                                                  |
| `notification.added_to_channel`     | The claiming user's connections                     | For each channel the user joins.                                                                             |

```json
{
  "type": "user.claimed",
  "created_at": "2026-11-03T14:02:11.483Z",
  "user": {
    "id": "guest-1b7c4e2a-9d3f-4a6b-8c5e-2f0a1d9e7b34-visitor",
    "role": "guest",
    "deactivated_at": "2026-11-03T14:02:11.479Z"
  },
  "claimed_by": {
    "id": "alice",
    "role": "user"
  }
}
```

The guest's deactivation sends no `user.deactivated` webhook. Moving messages, reactions, threads and channel ownership sends no events of its own.

## Errors

| HTTP  | When                                                                                        |
| ----- | ------------------------------------------------------------------------------------------- |
| `403` | The request is not server-side.                                                             |
| `403` | Claiming guest users is not enabled for the app.                                            |
| `400` | `guest_user_id` or `user_id` is missing or not a valid user ID, or both name the same user. |
| `404` | The claiming user does not exist, is deleted or is deactivated.                             |
| `403` | The claiming user is a guest.                                                               |
| `404` | The guest does not exist or is deleted.                                                     |
| `400` | The user named by `guest_user_id` is not a guest.                                           |
| `403` | The guest was deactivated by your app, or was already claimed by another user.              |

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/go-golang/claim-guest-users/](https://getstream.io/chat/docs/go-golang/claim-guest-users/).