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.

JAVASCRIPT
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.

HTTP 409 means the submitted baseVersion is stale.

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.

JAVASCRIPT
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;
  }
}