diff --git a/content/docs/godot/install.mdx b/content/docs/godot/install.mdx
index 507bb5e..3b29235 100644
--- a/content/docs/godot/install.mdx
+++ b/content/docs/godot/install.mdx
@@ -21,6 +21,11 @@ The Godot Asset Library is the recommended way to download Godot plugins. Open t
You can download the latest version of the Godot plugin from our [itch.io page](https://sleepystudios.itch.io/talo-godot). Simply unzip the plugin and copy the `addons/talo` folder into your project.
+
+ Upgrading from an older version of the plugin? Read the [upgrading to 1.0
+ guide](/docs/godot/upgrading-to-1.0) for the breaking changes.
+
+
## Enable the plugin
diff --git a/content/docs/godot/meta.json b/content/docs/godot/meta.json
index 8232a00..08d9cba 100644
--- a/content/docs/godot/meta.json
+++ b/content/docs/godot/meta.json
@@ -1,6 +1,7 @@
{
"pages": [
"install",
+ "upgrading-to-1.0",
"settings-reference",
"exporting",
"request-verification",
diff --git a/content/docs/godot/upgrading-to-1.0.mdx b/content/docs/godot/upgrading-to-1.0.mdx
new file mode 100644
index 0000000..ba9f681
--- /dev/null
+++ b/content/docs/godot/upgrading-to-1.0.mdx
@@ -0,0 +1,269 @@
+---
+description: Upgrade the Talo Godot plugin to 1.0 and migrate code from v0.49.1 and earlier.
+title: Upgrading to 1.0
+---
+## How to upgrade
+
+1. Download the latest release from the [Godot Asset Library](https://godotengine.org/asset-library/asset/2936), [itch.io](https://sleepystudios.itch.io/talo-godot) or [GitHub releases](https://github.com/TaloDev/godot/releases).
+2. Replace your existing `addons/talo` folder with the new one.
+3. Go through each section below and update any code that uses the affected APIs.
+
+## Identifying players
+
+### Player aliases
+
+`identify()`, `identify_steam()`, `identify_google_play_games()`, `identify_game_center()` and `identify_offline()` return a `TaloPlayerAlias` instead of a `TaloPlayer` ([#223](https://github.com/TaloDev/godot/pull/223)).
+
+You can access the underlying player via `alias.player`:
+
+```gdscript
+# before
+var player: TaloPlayer = await Talo.players.identify("username", "bob")
+func _on_identified(_player: TaloPlayer) -> void: ...
+
+# after
+var alias: TaloPlayerAlias = await Talo.players.identify("username", "bob")
+func _on_identified(player_alias: TaloPlayerAlias) -> void: ...
+```
+
+### Identification errors
+
+The `identification_failed` signal now passes a `TaloIdentifyError` with a `code` enum (`UNKNOWN_ERROR`, `IDENTIFIER_PROFANITY`, `IDENTIFIER_TAKEN`) ([#224](https://github.com/TaloDev/godot/pull/224)):
+
+```gdscript
+Talo.players.identification_failed.connect(func (error: TaloIdentifyError):
+ push_error("Identification failed: %s" % error.code)
+ go_to_login()
+)
+```
+
+## Player props
+
+### Awaitable prop updates
+
+All six prop mutators (`set_prop`, `delete_prop`, `set_prop_array`, `delete_prop_array`, `insert_into_prop_array`, `remove_from_prop_array`) now return a signal you can `await`, which resolves with a new `PlayerUpdateResult` (`success`, `rejected_props`) after the debounce settles ([#232](https://github.com/TaloDev/godot/pull/232)):
+
+```gdscript
+# fire-and-forget (still works - the return value can be ignored)
+Talo.current_player.set_prop("xp", "100")
+
+# await the settle result
+var result: PlayersAPI.PlayerUpdateResult = await Talo.current_player.set_prop("xp", "100")
+# result.success - whether the update was successful
+# result.rejected_props - which props Talo rejected (if any)
+
+# a local update (no network request); returns a valid result
+var local := Talo.current_player.set_prop("xp", "100", false)
+```
+
+### Rejected props
+
+The `Talo.players.props_rejected` signal has been removed - rejected props come back on the result ([#233](https://github.com/TaloDev/godot/pull/233)).
+
+### Trailing-only debounce
+
+Debounce timers are now **trailing-only** - the leading-edge mode and its `leading` constructor param were removed ([#234](https://github.com/TaloDev/godot/pull/234)). Pending player and save updates are now flushed when the game quits, so queued/in-flight updates are no longer dropped.
+
+## Player authentication
+
+### Result objects
+
+Every auth method (`register`, `login`, `verify`, `change_password`, `forgot_password`, `migrate_account`, etc.) now returns a **result object** instead of a bare `Error`.
+
+Errors now live inside this result object and `Talo.player_auth.last_error` has been removed ([#228](https://github.com/TaloDev/godot/pull/228)):
+
+```gdscript
+# before
+var res := await Talo.player_auth.register(username.text, password.text, email.text)
+if res != OK:
+ match Talo.player_auth.last_error.get_code():
+ TaloAuthError.ErrorCode.IDENTIFIER_TAKEN:
+ validation_label.text = "Username is already taken"
+ _:
+ validation_label.text = Talo.player_auth.last_error.get_string()
+
+# after
+var res := await Talo.player_auth.register(username.text, password.text, email.text, verification_enabled)
+if not res.success:
+ match res.error.code:
+ TaloPlayerAuthError.ErrorCode.IDENTIFIER_TAKEN:
+ validation_label.text = "Username is already taken"
+ _:
+ validation_label.text = res.error.message
+```
+
+### Login verification
+
+The `LoginResult` enum has been removed. `login()` returns a `PlayerAuthLoginResult` - check `verification_required` instead:
+
+```gdscript
+# before
+var res := await Talo.player_auth.login(username.text, password.text)
+match res:
+ Talo.player_auth.LoginResult.VERIFICATION_REQUIRED:
+ verification_required.emit()
+ Talo.player_auth.LoginResult.FAILED: ...
+
+# after
+var res := await Talo.player_auth.login(username.text, password.text)
+if res.verification_required:
+ verification_required.emit()
+elif not res.success:
+ match res.error.code: ...
+```
+
+## Leaderboards
+
+### Current player entries
+
+The deprecated `get_entries_for_current_player()` and `get_cached_entries_for_current_player()` have been removed ([#220](https://github.com/TaloDev/godot/pull/220), [#221](https://github.com/TaloDev/godot/pull/221)).
+
+Use the options-based methods with `player_id` or `alias_id` filtering instead:
+
+```gdscript
+# before
+var entries := Talo.leaderboards.get_entries_for_current_player(internal_name)
+var cached := Talo.leaderboards.get_cached_entries_for_current_player(internal_name)
+
+# after
+var options := Talo.leaderboards.GetEntriesOptions.new()
+options.player_id = Talo.current_player.id
+var res := await Talo.leaderboards.get_entries(internal_name, options)
+
+var cached_options := Talo.leaderboards.GetCachedEntriesOptions.new()
+cached_options.player_id = Talo.current_player.id
+var cached := Talo.leaderboards.get_cached_entries(internal_name, cached_options)
+```
+
+### Rejected entry props
+
+The `props_rejected` signal has been removed. `add_entry()` now returns an `AddEntryResult` with `success`, `entry`, `updated` and `rejected_props` ([#233](https://github.com/TaloDev/godot/pull/233), [#235](https://github.com/TaloDev/godot/pull/235)):
+
+```gdscript
+# before
+Talo.leaderboards.props_rejected.connect(_on_props_rejected)
+func _on_props_rejected(rejected_props: Array[TaloRejectedProp]):
+ for prop in rejected_props:
+ print("Rejected prop '%s': %s (%s)" % [prop.key, prop.message, prop.error])
+
+# after
+var res := await Talo.leaderboards.add_entry(internal_name, score)
+if not res.success:
+ for prop in res.rejected_props:
+ print("Rejected prop '%s': %s (%s)" % [prop.key, prop.message, prop.code])
+```
+
+## Channels
+
+### Updating channels
+
+`update()` now takes an `UpdateChannelOptions` object instead of positional arguments. `create()` and `update()` now return a `ChannelUpsertResult` (`success`, `channel`, `rejected_props`) ([#231](https://github.com/TaloDev/godot/pull/231)):
+
+```gdscript
+# before
+var channel := await Talo.channels.create(options)
+await Talo.channels.update(channel_id, "new name", 123, { team = "red" })
+
+# after
+var options := Talo.channels.UpdateChannelOptions.new()
+options.name = "new name"
+options.new_owner_alias_id = 123
+options.props = { team = "red" }
+var result := await Talo.channels.update(channel_id, options)
+if not result.success:
+ for prop in result.rejected_props: ...
+ print(result.channel.name)
+```
+
+`UpdateChannelOptions` also adds tri-state toggles (`auto_cleanup`, `private`, `temporary_membership`) so you can leave a field unchanged or explicitly set it to `true`/`false`.
+
+### Rejected channel props
+
+The `channel_props_rejected` signal has been removed - rejected props come back on the result object ([#233](https://github.com/TaloDev/godot/pull/233)).
+
+The `channel_storage_props_failed_to_set` signal still exists but now emits `Array[TaloRejectedProp]` instead of `Array[TaloChannelStoragePropError]` ([#222](https://github.com/TaloDev/godot/pull/222)):
+
+```gdscript
+# before
+func _on_failed(channel: TaloChannel, failed_props: Array[TaloChannelStoragePropError]):
+ for prop in failed_props:
+ print("%s: %s (%s)" % [prop.key, prop.message, prop.error])
+
+# after
+func _on_failed(channel: TaloChannel, failed_props: Array[TaloRejectedProp]):
+ for prop in failed_props:
+ print("%s: %s (%s)" % [prop.key, prop.message, prop.code])
+```
+
+## Saves
+
+### Updating the current save
+
+`update_current_save()` now returns a `SaveUpdateResult` (`success`, `save`) on **both** the debounced content-sync path and the immediate rename path ([#232](https://github.com/TaloDev/godot/pull/232)).
+
+### Updating a save
+
+`update_save(save, new_name)` is unchanged in signature but now returns `null` on a non-200 response (previously it returned the local save, masking failures) ([#232](https://github.com/TaloDev/godot/pull/232)).
+
+## Feedback
+
+### Sending feedback
+
+`send()` now returns a `FeedbackSendResult` (`success`, `rejected_props`) instead of `void`, and the `props_rejected` signal has been removed ([#233](https://github.com/TaloDev/godot/pull/233)):
+
+```gdscript
+# before
+await Talo.feedback.send(category, comment, props)
+
+# after
+var result := await Talo.feedback.send(category, comment, props)
+if not result.success:
+ for prop in result.rejected_props:
+ print("Rejected prop '%s': %s (%s)" % [prop.key, prop.message, prop.code])
+```
+
+## Error classes
+
+### Error class renames
+
+- `TaloAuthError` has been renamed to `TaloPlayerAuthError` ([#225](https://github.com/TaloDev/godot/pull/225)).
+- `get_code()` / `get_string()` have been replaced by the typed `code: ErrorCode` and `message: String` properties ([#226](https://github.com/TaloDev/godot/pull/226)).
+- `.error` has been renamed to `.code` on every error class, and `TaloRejectedProp.RejectionReason` is now `TaloRejectedProp.ErrorCode` ([#227](https://github.com/TaloDev/godot/pull/227)).
+
+```gdscript
+# before
+prop.error # TaloRejectedProp.RejectionReason.PROP_VALUE_TOO_LONG
+Talo.player_auth.last_error.get_string()
+
+# after
+prop.code # TaloRejectedProp.ErrorCode.PROP_VALUE_TOO_LONG
+res.error.message # TaloPlayerAuthError.message
+```
+
+### Channel storage errors
+
+`TaloChannelStoragePropError` has been removed - channel storage failures now use the shared `TaloRejectedProp` class ([#222](https://github.com/TaloDev/godot/pull/222)).
+
+## Quick reference
+
+| Removed / renamed | Replacement |
+| ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
+| `Talo.players.identify()` → `TaloPlayer` | `Talo.players.identify()` → `TaloPlayerAlias` (use `.player`) |
+| `Talo.players.props_rejected` | `PlayerUpdateResult.rejected_props` |
+| Prop editing on players, e.g. `set_prop()`, returns `void` | `Talo.current_player.set_prop()` is awaitable → `PlayerUpdateResult` |
+| `TaloDebounceTimer(..., leading)` | leading mode removed (trailing-only) |
+| `Talo.player_auth.last_error` | `.error` on the returned result |
+| `Talo.player_auth.LoginResult` enum | `PlayerAuthLoginResult` + `verification_required` |
+| `Talo.leaderboards.get_entries_for_current_player()` | `Talo.leaderboards.get_entries()` + `player_id`/`alias_id` options |
+| `Talo.leaderboards.get_cached_entries_for_current_player()` | `Talo.leaderboards.get_cached_entries()` + `player_id`/`alias_id` options |
+| `Talo.leaderboards.props_rejected` | `AddEntryResult.rejected_props` |
+| `Talo.channels.update(id, name, owner, props)` | `Talo.channels.update(id, UpdateChannelOptions)` |
+| `Talo.channels.channel_props_rejected` | `ChannelUpsertResult.rejected_props` |
+| `Talo.saves.update_current_save()` → `TaloGameSave` | `Talo.saves.update_current_save()` → `SaveUpdateResult` |
+| `Talo.saves.update_save()` returns local save on failure | `Talo.saves.update_save()` returns `null` on non-200 |
+| `Talo.feedback.props_rejected` | `FeedbackSendResult.rejected_props` |
+| `TaloAuthError` | `TaloPlayerAuthError` |
+| `get_code()` / `get_string()` on `TaloPlayerAuthError` | `.code` / `.message` |
+| `TaloRejectedProp.RejectionReason` | `TaloRejectedProp.ErrorCode` |
+| `.error` property on error classes (e.g. `TaloRejectedProp`) | `.code` (e.g. `prop.error` → `prop.code`) |
+| `TaloChannelStoragePropError` | `TaloRejectedProp` |
\ No newline at end of file
diff --git a/content/docs/unity/install.mdx b/content/docs/unity/install.mdx
index 323f6f9..8fdf67f 100644
--- a/content/docs/unity/install.mdx
+++ b/content/docs/unity/install.mdx
@@ -27,6 +27,11 @@ You can download the latest version of the Unity package from our [itch.io page]
Once downloaded, you can open the `talo.unitypackage` file to import it into your project.
+
+ Upgrading from an older version of the package? Read the [upgrading to 1.0
+ guide](/docs/unity/upgrading-to-1.0) for the breaking changes.
+
+
## Generate an API key
Visit [the Talo dashboard](https://dashboard.trytalo.com), login or create an account (and confirm your email address), and visit the API Keys page.
diff --git a/content/docs/unity/meta.json b/content/docs/unity/meta.json
index 8b881cd..3d283d0 100644
--- a/content/docs/unity/meta.json
+++ b/content/docs/unity/meta.json
@@ -1,6 +1,7 @@
{
"pages": [
"install",
+ "upgrading-to-1.0",
"settings-reference",
"dev-data",
"request-verification",
diff --git a/content/docs/unity/upgrading-to-1.0.mdx b/content/docs/unity/upgrading-to-1.0.mdx
new file mode 100644
index 0000000..3c07872
--- /dev/null
+++ b/content/docs/unity/upgrading-to-1.0.mdx
@@ -0,0 +1,278 @@
+---
+description: Upgrade the Talo Unity package to 1.0 and migrate code from v0.60.2 and earlier.
+title: Upgrading to 1.0
+---
+## How to upgrade
+
+1. Download the latest release from the [Unity Asset Store](https://assetstore.unity.com/packages/tools/game-toolkits/talo-game-services-292832), [itch.io](https://sleepystudios.itch.io/talo-unity) or [GitHub releases](https://github.com/TaloDev/unity/releases).
+2. Re-import the package over your existing install (Asset Store: re-download from `Window > My Assets`; itch.io: re-open the `.unitypackage`).
+3. Go through each section below and update any code that uses the affected APIs.
+
+## Identifying players
+
+### Player aliases
+
+`Identify()`, `IdentifySteam()`, `IdentifyGooglePlayGames()` and `IdentifyGameCenter()` now return a `PlayerAlias` instead of a `Player`, and the `OnIdentified` event passes a `PlayerAlias` ([#223](https://github.com/TaloDev/unity/pull/223)).
+
+You can access the underlying player via `alias.player`:
+
+```csharp
+// before
+var player = await Talo.Players.Identify("username", "bob");
+Debug.Log(player.id);
+
+// after
+var alias = await Talo.Players.Identify("username", "bob");
+Debug.Log(alias.player.id);
+```
+
+### Identification errors
+
+The `OnIdentificationFailed` event now passes an `IdentifyException` with an `ErrorCode` enum (`UNKNOWN_ERROR`, `IDENTIFIER_PROFANITY`, `IDENTIFIER_TAKEN`) instead of firing with no arguments ([#224](https://github.com/TaloDev/unity/pull/224)):
+
+```csharp
+// before
+Talo.Players.OnIdentificationFailed += () => GoToLogin();
+
+// after
+Talo.Players.OnIdentificationFailed += (ex) =>
+{
+ Debug.LogError($"Identification failed: {ex.ErrorCode}");
+ GoToLogin();
+};
+```
+
+## Player props
+
+### Awaitable prop updates
+
+All six prop mutators (`SetProp`, `DeleteProp`, `SetPropArray`, `DeletePropArray`, `InsertIntoPropArray`, `RemoveFromPropArray`) now return a `Task` that resolves with a new `PlayerUpdateResult` (`Success`, `RejectedProps`) after the debounce settles ([#227](https://github.com/TaloDev/unity/pull/227)):
+
+```csharp
+// fire-and-forget (still works - the return value can be ignored)
+Talo.CurrentPlayer.SetProp("level", "5");
+
+// await the settle result
+var result = await Talo.CurrentPlayer.SetProp("level", "5");
+// result.Success - whether the update was successful
+// result.RejectedProps - which props Talo rejected (if any)
+
+// a local update (no network request); returns a valid result
+var local = await Talo.CurrentPlayer.SetProp("level", "5", false);
+```
+
+### Rejected props
+
+The `Talo.Players.OnPropsRejected` event has been removed - rejected props come back on the result ([#228](https://github.com/TaloDev/unity/pull/228)).
+
+### Trailing-only debounce
+
+The debounce timer is now **trailing-only** - the leading-edge mode has been removed, so the first call no longer fires immediately ([#229](https://github.com/TaloDev/unity/pull/229)). Pending player and save updates are now flushed when the game quits, so queued updates are no longer dropped.
+
+## Leaderboards
+
+### Current player entries
+
+The deprecated `GetEntriesForCurrentPlayer()`, `GetCachedEntriesForCurrentPlayer()` and the `GetEntries(name, page, aliasId, includeArchived)` overloads have been removed ([#219](https://github.com/TaloDev/unity/pull/219), [#220](https://github.com/TaloDev/unity/pull/220), [#221](https://github.com/TaloDev/unity/pull/221)).
+
+Use the options-based methods with `aliasId` or `playerId` filtering instead:
+
+```csharp
+// before
+var res = await Talo.Leaderboards.GetEntriesForCurrentPlayer(internalName);
+var cached = Talo.Leaderboards.GetCachedEntriesForCurrentPlayer(internalName);
+var paged = await Talo.Leaderboards.GetEntries(internalName, 0, aliasId, true);
+
+// after
+var res = await Talo.Leaderboards.GetEntries(internalName, new GetEntriesOptions
+{
+ aliasId = Talo.CurrentAlias.id
+});
+var cached = Talo.Leaderboards.GetCachedEntries(internalName, new GetCachedEntriesOptions
+{
+ aliasId = Talo.CurrentAlias.id
+});
+var paged = await Talo.Leaderboards.GetEntries(internalName, new GetEntriesOptions
+{
+ page = 0,
+ aliasId = aliasId,
+ includeArchived = true
+});
+```
+
+### Rejected entry props
+
+The `OnPropsRejected` event has been removed. `AddEntry()` now returns an `AddEntryResult` (`Success`, `Entry`, `Updated`, `RejectedProps`) instead of a `(LeaderboardEntry, bool)` tuple ([#228](https://github.com/TaloDev/unity/pull/228)):
+
+```csharp
+// before
+(LeaderboardEntry entry, bool updated) = await Talo.Leaderboards.AddEntry(internalName, score, ("team", "red"));
+Debug.Log(entry.position);
+
+// after
+var result = await Talo.Leaderboards.AddEntry(internalName, score, ("team", "red"));
+if (!result.Success)
+{
+ foreach (var prop in result.RejectedProps)
+ {
+ Debug.Log($"Rejected prop '{prop.key}': {prop.message} ({prop.error})");
+ }
+ return;
+}
+Debug.Log(result.Entry.position);
+```
+
+## Stats
+
+### `GetStat()` removed
+
+The deprecated `Talo.Stats.GetStat()` method has been removed. Use `Talo.Stats.Find()` instead ([#215](https://github.com/TaloDev/unity/pull/215)):
+
+```csharp
+// before
+var stat = await Talo.Stats.GetStat("gold-collected");
+
+// after
+var stat = await Talo.Stats.Find("gold-collected");
+```
+
+## Channels
+
+### Deprecated methods removed
+
+The deprecated `GetChannels(int page)`, `CreatePrivate()` and `Create(string name, ...)` overloads have been removed ([#216](https://github.com/TaloDev/unity/pull/216), [#217](https://github.com/TaloDev/unity/pull/217), [#218](https://github.com/TaloDev/unity/pull/218)):
+
+```csharp
+// before
+var res = await Talo.Channels.GetChannels(page);
+var privateChannel = await Talo.Channels.CreatePrivate("secret-guild", autoCleanup: true);
+var guild = await Talo.Channels.Create("guild", true, ("team", "red"));
+
+// after
+var res = await Talo.Channels.GetChannels(new GetChannelsOptions { page = page });
+var privateResult = await Talo.Channels.Create(new CreateChannelOptions
+{
+ name = "secret-guild",
+ autoCleanup = true,
+ isPrivate = true
+});
+var guildResult = await Talo.Channels.Create(new CreateChannelOptions
+{
+ name = "guild",
+ autoCleanup = true,
+ props = new[] { ("team", "red") }
+});
+```
+
+### Updating channels
+
+`Update()` now takes an `UpdateChannelOptions` object instead of positional arguments. `Create()` and `Update()` now return a `ChannelUpsertResult` (`Success`, `Channel`, `RejectedProps`) instead of a `Channel` ([#226](https://github.com/TaloDev/unity/pull/226), [#228](https://github.com/TaloDev/unity/pull/228)):
+
+```csharp
+// before
+await Talo.Channels.Update(channelId, "new name", newOwnerAliasId, ("team", "red"));
+
+// after
+var options = new UpdateChannelOptions
+{
+ name = "new name",
+ newOwnerAliasId = newOwnerAliasId,
+ props = new[] { ("team", "red") }
+};
+var result = await Talo.Channels.Update(channelId, options);
+if (!result.Success)
+{
+ foreach (var prop in result.RejectedProps) { ... }
+ return;
+}
+```
+
+`UpdateChannelOptions` also adds nullable toggles - `autoCleanup`, `isPrivate` and `temporaryMembership` are `bool?` - so you can leave a field unchanged (`null`) or explicitly set it to `true`/`false`. `name` (empty string) and `newOwnerAliasId` (`-1`) work the same way.
+
+### Channel storage errors
+
+`ChannelStoragePropError` has been removed. The `OnChannelStoragePropsFailedToSet` event now passes `RejectedProp[]` instead of `ChannelStoragePropError[]` ([#222](https://github.com/TaloDev/unity/pull/222)):
+
+```csharp
+// before
+Talo.Channels.OnChannelStoragePropsFailedToSet += (Channel channel, ChannelStoragePropError[] errors) =>
+{
+ foreach (var prop in errors) Debug.Log($"{prop.key}: {prop.message} ({prop.error})");
+};
+
+// after
+Talo.Channels.OnChannelStoragePropsFailedToSet += (Channel channel, RejectedProp[] errors) =>
+{
+ foreach (var prop in errors) Debug.Log($"{prop.key}: {prop.message} ({prop.error})");
+};
+```
+
+`RejectedProp` keeps the same shape (`key`, `error`, `message`), so only the type annotation changes.
+
+## Saves
+
+### Updating the current save
+
+`UpdateCurrentSave()` now returns a `SaveUpdateResult` (`Success`, `Save`) on **both** the debounced content-sync path and the immediate rename path (previously the rename path returned a `GameSave`) ([#227](https://github.com/TaloDev/unity/pull/227)):
+
+```csharp
+// rename path (immediate, same shape as the content-sync path):
+var result = await Talo.Saves.UpdateCurrentSave("new name");
+// result.Success - whether the PATCH round-trip OK
+// result.Save - the save that was synced (null on failure)
+```
+
+## Feedback
+
+### Sending feedback
+
+`Send()` now returns a `FeedbackSendResult` (`Success`, `RejectedProps`) instead of `void`, and the `OnPropsRejected` event has been removed ([#228](https://github.com/TaloDev/unity/pull/228)):
+
+```csharp
+// before
+await Talo.Feedback.Send(category, comment, ("version", "1.2"));
+
+// after
+var result = await Talo.Feedback.Send(category, comment, ("version", "1.2"));
+if (!result.Success)
+{
+ foreach (var prop in result.RejectedProps)
+ {
+ Debug.Log($"Rejected prop '{prop.key}': {prop.message} ({prop.error})");
+ }
+}
+```
+
+## Exception handling
+
+`PlayerAuthException.ErrorCode` and `SocketException.ErrorCode` now fall back to `API_ERROR` when the server returns an unrecognised code, instead of throwing ([#225](https://github.com/TaloDev/unity/pull/225)).
+
+## Quick reference
+
+
+| Removed / renamed | Replacement |
+| -------------------------------------------------------------------- | -------------------------------------------------------------------------- |
+| `Talo.Players.Identify*()` → `Player` | `Talo.Players.Identify*()` → `PlayerAlias` (use `.player`) |
+| `Talo.Players.OnIdentified(Player)` | `Talo.Players.OnIdentified(PlayerAlias)` |
+| `Talo.Players.OnIdentificationFailed` (no args) | `Talo.Players.OnIdentificationFailed(IdentifyException)` |
+| `Talo.Players.OnPropsRejected` | `PlayerUpdateResult.RejectedProps` |
+| Prop editing on players, e.g. `SetProp()`, returns `void` | `Talo.CurrentPlayer.SetProp()` is awaitable → `Task` |
+| Leading debounce (first call sends immediately) | Trailing-only: all calls coalesce into one request after the window |
+| `Talo.Leaderboards.GetEntriesForCurrentPlayer()` | `Talo.Leaderboards.GetEntries()` + `aliasId`/`playerId` options |
+| `Talo.Leaderboards.GetCachedEntriesForCurrentPlayer()` | `Talo.Leaderboards.GetCachedEntries()` + `aliasId`/`playerId` options |
+| `Talo.Leaderboards.GetEntries(name, page, aliasId, includeArchived)` | `Talo.Leaderboards.GetEntries(name, GetEntriesOptions)` |
+| `Talo.Leaderboards.AddEntry()` → `(LeaderboardEntry, bool)` tuple | `Talo.Leaderboards.AddEntry()` → `AddEntryResult` (`.Entry`, `.Updated`) |
+| `Talo.Leaderboards.OnPropsRejected` | `AddEntryResult.RejectedProps` |
+| `Talo.Stats.GetStat(name)` | `Talo.Stats.Find(name)` |
+| `Talo.Channels.GetChannels(page)` | `Talo.Channels.GetChannels(new GetChannelsOptions { page = page })` |
+| `Talo.Channels.CreatePrivate(name, ...)` | `Talo.Channels.Create(new CreateChannelOptions { isPrivate = true, ... })` |
+| `Talo.Channels.Create(name, autoCleanup, props)` | `Talo.Channels.Create(new CreateChannelOptions { ... })` |
+| `Talo.Channels.Update(id, name, ownerId, props)` | `Talo.Channels.Update(id, new UpdateChannelOptions { ... })` |
+| `Talo.Channels.Create()`/`Update()` returned `Channel` | `ChannelUpsertResult` (use `.Channel`, check `.Success`) |
+| `Talo.Channels.OnChannelPropsRejected` | `ChannelUpsertResult.RejectedProps` |
+| `ChannelStoragePropError` | `RejectedProp` |
+| `Talo.Saves.UpdateCurrentSave()` → `GameSave` | `Talo.Saves.UpdateCurrentSave()` → `SaveUpdateResult` (both paths) |
+| `Talo.Feedback.Send()` → `void` | `Talo.Feedback.Send()` → `FeedbackSendResult` |
+| `Talo.Feedback.OnPropsRejected` | `FeedbackSendResult.RejectedProps` |
+
+