HTTP 409 means the submitted baseVersion is stale.
409 Conflict
Conflict is a normal sync outcome, not an unknown error.
Persistly returns product-shaped local and cloud branches so the client can recover intentionally. Games should design this path instead of pretending every sync is accepted.
Response
HTTP 409 Conflict
The status field tells the SDK exactly what happened, and details.reason explains why.
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"status": "conflict",
"slot": {
"slotId": "autosave",
"slotInfo": {
"label": "Autosave",
"level": 6,
"location": "Reactor"
},
"data": {
"checkpoint": "reactor",
"coins": 612
},
"version": 3,
"status": "active",
"updatedAt": "2026-05-29T12:05:00Z"
},
"version": 3,
"updatedAt": "2026-05-29T12:05:00Z",
"details": {
"reason": "base_version_mismatch",
"serverSlot": {
"slotInfo": {
"label": "Autosave",
"level": 6,
"location": "Reactor"
},
"data": {
"checkpoint": "reactor",
"coins": 612
},
"version": 3,
"updatedAt": "2026-05-29T12:05:00Z"
},
"clientSlot": {
"slotInfo": {
"label": "Autosave",
"level": 6,
"location": "Reactor"
},
"data": {
"checkpoint": "reactor",
"coins": 720
},
"baseVersion": 2
}
}
}Recovery Rules
Build your conflict path explicitly.
Persistly returns the cloud branch and the submitted local branch in the same response.
Clients should compare both branches before deciding whether to accept cloud data, replay local edits, or ask the player.
Do not silently discard either branch or treat conflict as a transport failure.
SDK Pattern
Minimal conflict handling
This is the shape your client logic should follow even if you wrap it inside engine-specific abstractions.
const ConflictRecoveryChoice = {
UseCloud: "use-cloud",
KeepLocal: "keep-local",
} as const;
const result = await client.syncAccountSlot({
accountId,
accountSessionToken,
slotId,
baseVersion: local.version,
slotInfo: local.slotInfo,
data: nextData
});
if (result.status === PersistlySyncStatus.Accepted) {
local = result.slot;
}
if (result.status === PersistlySyncStatus.Conflict) {
const cloudSlot = result.slot;
// Implement this in your game UI. Keep choices in constants instead of ad-hoc strings.
const playerChoice = await askPlayerHowToRecover({ cloudSlot, localDraft });
if (playerChoice === ConflictRecoveryChoice.UseCloud) {
local = cloudSlot;
}
if (playerChoice === ConflictRecoveryChoice.KeepLocal) {
const retry = await client.syncAccountSlot({
accountId,
accountSessionToken,
slotId,
baseVersion: cloudSlot.version,
slotInfo: localDraft.slotInfo,
data: localDraft.data,
});
local = retry.slot;
}
}