> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alana.shopping/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync & Conflicts

> Apply pending updates from a Hub subscription and resolve field-level conflicts between local edits and publisher changes.

## Endpoints

```
POST /api/hub/subscriptions/{subscriptionId}/sync/apply
GET  /api/hub/subscriptions/{subscriptionId}/conflicts
PUT  /api/hub/subscriptions/{subscriptionId}/conflicts/{conflictId}
```

Manage the sync lifecycle for Hub subscriptions: apply pending publisher updates to your local catalog, list field-level conflicts, and resolve them individually.

***

## Path parameters

| Parameter        | Type   | Required | Description                                    |
| ---------------- | ------ | :------: | ---------------------------------------------- |
| `subscriptionId` | string |    Yes   | Your subscription ID (from subscribe response) |
| `conflictId`     | string |    Yes   | Conflict ID (from list conflicts response)     |

***

## Apply pending updates

`POST /api/hub/subscriptions/{subscriptionId}/sync/apply`

Applies pending publisher version updates to your local catalog. For subscriptions with `syncStrategy: "auto"`, this happens automatically. For `"manual"` subscriptions, call this endpoint when you're ready to sync.

### Request body

```json theme={null}
{
  "targetVersion": 7
}
```

| Field           | Type   | Required | Description                                                       |
| --------------- | ------ | :------: | ----------------------------------------------------------------- |
| `targetVersion` | number |    No    | Specific version to sync to. Defaults to latest published version |

### Response

```json theme={null}
{
  "applied": true,
  "fromVersion": 5,
  "toVersion": 7,
  "productsUpdated": 34,
  "productsAdded": 12,
  "productsRemoved": 3,
  "conflictsCreated": 5,
  "duration_ms": 2100
}
```

### Apply sync examples

<CodeGroup>
  ```bash curl theme={null}
  # Apply latest publisher updates
  curl -X POST "https://app.alana.shopping/api/hub/subscriptions/sub_4k2n8x/sync/apply" \
    -H "Authorization: Bearer sk_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{}'

  # Apply a specific version
  curl -X POST "https://app.alana.shopping/api/hub/subscriptions/sub_4k2n8x/sync/apply" \
    -H "Authorization: Bearer sk_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{"targetVersion": 7}'
  ```

  ```javascript JavaScript theme={null}
  const subscriptionId = 'sub_4k2n8x';

  const response = await fetch(
    `https://app.alana.shopping/api/hub/subscriptions/${subscriptionId}/sync/apply`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.ALANA_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({}), // sync to latest
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error.message);
  }

  const result = await response.json();
  console.log(`Synced from v${result.fromVersion} to v${result.toVersion}`);
  console.log(`${result.productsUpdated} updated, ${result.productsAdded} added, ${result.productsRemoved} removed`);
  if (result.conflictsCreated > 0) {
    console.log(`${result.conflictsCreated} conflicts need resolution`);
  }
  ```

  ```python Python theme={null}
  import requests, os

  subscription_id = "sub_4k2n8x"

  response = requests.post(
      f"https://app.alana.shopping/api/hub/subscriptions/{subscription_id}/sync/apply",
      headers={
          "Authorization": f"Bearer {os.environ['ALANA_API_KEY']}",
          "Content-Type": "application/json",
      },
      json={}
  )

  response.raise_for_status()
  result = response.json()
  print(f"Synced v{result['fromVersion']} → v{result['toVersion']}")
  print(f"Changes: +{result['productsAdded']} added, ~{result['productsUpdated']} updated, -{result['productsRemoved']} removed")
  print(f"Conflicts created: {result['conflictsCreated']}")
  ```
</CodeGroup>

***

## List conflicts

`GET /api/hub/subscriptions/{subscriptionId}/conflicts`

Returns all unresolved conflicts for a subscription — fields where your local edits differ from the publisher's new value.

### Response

```json theme={null}
{
  "conflicts": [
    {
      "id": "conflict_9a3b",
      "productId": "prod_local_789",
      "field": "description",
      "localValue": "Our hand-curated description of this premium blazer...",
      "remoteValue": "Classic linen blazer in sky blue. Machine washable. Available in S-XL.",
      "publisherVersion": 7,
      "createdAt": "2026-03-17T11:00:00Z",
      "status": "pending"
    },
    {
      "id": "conflict_7c1d",
      "productId": "prod_local_456",
      "field": "price",
      "localValue": 89.99,
      "remoteValue": 94.99,
      "publisherVersion": 7,
      "createdAt": "2026-03-17T11:00:00Z",
      "status": "pending"
    }
  ],
  "total": 2
}
```

### Conflict fields

| Field              | Type   | Description                                                               |
| ------------------ | ------ | ------------------------------------------------------------------------- |
| `id`               | string | Conflict ID                                                               |
| `productId`        | string | Local product ID with the conflict                                        |
| `field`            | string | Field name where the conflict exists                                      |
| `localValue`       | any    | Your current local value                                                  |
| `remoteValue`      | any    | Publisher's new value                                                     |
| `publisherVersion` | number | Publisher version that introduced this change                             |
| `createdAt`        | string | When the conflict was created                                             |
| `status`           | string | `"pending"`, `"resolved_local"`, `"resolved_remote"`, `"resolved_merged"` |

### List conflicts examples

<CodeGroup>
  ```bash curl theme={null}
  curl "https://app.alana.shopping/api/hub/subscriptions/sub_4k2n8x/conflicts" \
    -H "Authorization: Bearer sk_live_your_api_key_here"
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    `https://app.alana.shopping/api/hub/subscriptions/${subscriptionId}/conflicts`,
    { headers: { 'Authorization': `Bearer ${process.env.ALANA_API_KEY}` } }
  );

  const { conflicts, total } = await response.json();
  console.log(`${total} unresolved conflicts`);
  conflicts.forEach(c => {
    console.log(`${c.productId}.${c.field}: local="${c.localValue}" vs remote="${c.remoteValue}"`);
  });
  ```

  ```python Python theme={null}
  import requests, os

  response = requests.get(
      f"https://app.alana.shopping/api/hub/subscriptions/{subscription_id}/conflicts",
      headers={"Authorization": f"Bearer {os.environ['ALANA_API_KEY']}"}
  )

  data = response.json()
  print(f"{data['total']} unresolved conflicts")
  for c in data["conflicts"]:
      print(f"  {c['productId']}.{c['field']}: local={c['localValue']!r} vs remote={c['remoteValue']!r}")
  ```
</CodeGroup>

***

## Resolve a conflict

`PUT /api/hub/subscriptions/{subscriptionId}/conflicts/{conflictId}`

Resolves a single field conflict by choosing local, remote, or a custom merged value.

### Request body

```json theme={null}
{
  "resolution": "keep_local"
}
```

Or with a custom merged value:

```json theme={null}
{
  "resolution": "merge",
  "mergedData": "Classic sky blue linen blazer — our premium pick. Machine washable. Available in S-XL."
}
```

| Field        | Type   | Required | Description                                                   |
| ------------ | ------ | :------: | ------------------------------------------------------------- |
| `resolution` | string |    Yes   | `"keep_local"`, `"accept_remote"`, or `"merge"`               |
| `mergedData` | any    |    No    | Custom merged value (required when `resolution` is `"merge"`) |

### Response

```json theme={null}
{
  "conflictId": "conflict_9a3b",
  "status": "resolved_local",
  "resolvedAt": "2026-03-17T11:30:00Z",
  "resolvedBy": "user_abc"
}
```

### Resolve conflict examples

<CodeGroup>
  ```bash curl theme={null}
  # Keep your local value
  curl -X PUT "https://app.alana.shopping/api/hub/subscriptions/sub_4k2n8x/conflicts/conflict_9a3b" \
    -H "Authorization: Bearer sk_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{"resolution": "keep_local"}'

  # Accept the publisher's value
  curl -X PUT "https://app.alana.shopping/api/hub/subscriptions/sub_4k2n8x/conflicts/conflict_7c1d" \
    -H "Authorization: Bearer sk_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{"resolution": "accept_remote"}'

  # Use a custom merged value
  curl -X PUT "https://app.alana.shopping/api/hub/subscriptions/sub_4k2n8x/conflicts/conflict_9a3b" \
    -H "Authorization: Bearer sk_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "resolution": "merge",
      "mergedData": "Classic sky blue linen blazer — our premium pick."
    }'
  ```

  ```javascript JavaScript theme={null}
  // Keep local value
  const response = await fetch(
    `https://app.alana.shopping/api/hub/subscriptions/${subscriptionId}/conflicts/${conflictId}`,
    {
      method: 'PUT',
      headers: {
        'Authorization': `Bearer ${process.env.ALANA_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ resolution: 'keep_local' }),
    }
  );
  const result = await response.json();
  console.log(`Resolved: ${result.status}`);
  ```

  ```python Python theme={null}
  import requests, os

  # Accept remote value
  response = requests.put(
      f"https://app.alana.shopping/api/hub/subscriptions/{subscription_id}/conflicts/{conflict_id}",
      headers={
          "Authorization": f"Bearer {os.environ['ALANA_API_KEY']}",
          "Content-Type": "application/json",
      },
      json={"resolution": "accept_remote"}
  )
  result = response.json()
  print(f"Resolved as: {result['status']}")
  ```
</CodeGroup>

***

## Conflict workflow

```mermaid theme={null}
graph TD
    A["Publisher releases v7"] --> B["Sync applied\nPOST /sync/apply"]
    B --> C{Conflicts created?}
    C -- no --> D["All done — catalog up to date"]
    C -- yes --> E["List conflicts\nGET /conflicts"]
    E --> F["Review each conflict"]
    F --> G{Resolution?}
    G -- keep local --> H["PUT /conflicts/{id}\nresolution: keep_local"]
    G -- accept remote --> I["PUT /conflicts/{id}\nresolution: accept_remote"]
    G -- custom merge --> J["PUT /conflicts/{id}\nresolution: merge + mergedData"]
    H --> K{More conflicts?}
    I --> K
    J --> K
    K -- yes --> F
    K -- no --> D
```

***

## Error responses

| HTTP status | Code                       | Description                                             |
| ----------- | -------------------------- | ------------------------------------------------------- |
| 403         | `INSUFFICIENT_PERMISSIONS` | Not your subscription                                   |
| 404         | `SUBSCRIPTION_NOT_FOUND`   | Subscription ID not found                               |
| 404         | `CONFLICT_NOT_FOUND`       | Conflict ID not found or already resolved               |
| 409         | `SYNC_ALREADY_RUNNING`     | A sync is already in progress                           |
| 422         | `NO_PENDING_UPDATES`       | Already on the latest version (apply only)              |
| 422         | `MERGE_DATA_REQUIRED`      | `mergedData` is required when `resolution` is `"merge"` |
