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

# Handle a 409 Conflict

> Resolve 409 CONFLICT when an action is in flight. Use onConflict cancel_and_replace, queue_after, or wait. Plus SCHEDULER_ACTIVE recovery.

## Overview

A 409 means the device already has an action booked for the requested window. The error response lists the colliding action IDs and the strategies that would resolve it. Two strategies, `cancel_and_replace` and `queue_after`, ride on the request body so you can resolve conflicts in a single round-trip.

A different kind of conflict comes from the device's own scheduler. When an OEM has its native scheduler active, direct writes are rejected until it is cleared. You see this as `SCHEDULER_ACTIVE` on the action result.

## Anatomy of a 409 CONFLICT

Push without an `onConflict` strategy when a colliding action exists:

```bash theme={null}
curl -X POST https://api.amps.ai/battery/device_abc123 \
  -H "x-api-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "action": {
      "command": "discharge",
      "parameters": {
        "target": { "value": 30, "unit": "percent" }
      }
    }
  }'
```

You get back `409` with the conflicting action IDs and the strategies that would resolve the collision.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "CONFLICT",
    "message": "A conflicting action is already pending for this device. Provide onConflict to resolve automatically, or cancel the existing action.",
    "details": {
      "reason": "no_strategy_supplied",
      "conflictingActionIds": ["act_pending_001"],
      "strategies": ["cancel_and_replace", "queue_after"]
    }
  },
  "meta": {
    "requestId": "req_2cB5gNqR",
    "timestamp": "2026-05-08T12:00:00.000Z",
    "path": "/battery/device_abc123",
    "latencyMs": 14
  }
}
```

## Strategy 1: cancel\_and\_replace

Use this when the new intent should win. The colliding action is cancelled and the new one is applied.

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.amps.ai/battery/device_abc123 \
      -H "x-api-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
      -H "Content-Type: application/json" \
      -d '{
        "action": {
          "command": "discharge",
          "parameters": {
            "target": { "value": 30, "unit": "percent" }
          }
        },
        "onConflict": "cancel_and_replace"
      }'
    ```
  </Tab>

  <Tab title="Node">
    ```javascript theme={null}
    await fetch("https://api.amps.ai/battery/device_abc123", {
      method: "POST",
      headers: {
        "x-api-key": process.env.AMPS_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        action: {
          command: "discharge",
          parameters: { target: { value: 30, unit: "percent" } },
        },
        onConflict: "cancel_and_replace",
      }),
    });
    ```
  </Tab>
</Tabs>

`act_pending_001` is cancelled and the new push is accepted.

```json theme={null}
{
  "success": true,
  "data": {
    "id": "act_inflight_009",
    "deviceId": "device_abc123",
    "deviceType": "battery",
    "command": "discharge",
    "parameters": { "target": { "value": 30, "unit": "percent" } },
    "state": "acknowledged",
    "createdAt": "2026-05-08T12:00:01.000Z",
    "links": { "self": "/actions/act_inflight_009" }
  },
  "meta": { "requestId": "req_8bT2pCfS", "environment": "sandbox", "timestamp": "2026-05-08T12:00:01.000Z", "latencyMs": 110 }
}
```

A follow-up read of the cancelled action confirms the transition:

```json theme={null}
{
  "success": true,
  "data": {
    "id": "act_pending_001",
    "deviceId": "device_abc123",
    "deviceType": "battery",
    "command": "charge",
    "parameters": { "target": { "value": 100, "unit": "percent" } },
    "state": "cancelled",
    "result": null,
    "errorCode": null,
    "errorMessage": null,
    "createdAt": "2026-05-08T08:00:00.000Z",
    "updatedAt": "2026-05-08T12:00:01.000Z",
    "acknowledgedAt": null,
    "completedAt": "2026-05-08T12:00:01.000Z",
    "start": "2026-05-09T22:00:00.000Z",
    "end": null,
    "links": { "self": "/actions/act_pending_001" }
  },
  "meta": { "requestId": "req_9cU3qDgT", "environment": "sandbox", "timestamp": "2026-05-08T12:00:02.000Z", "latencyMs": 9 }
}
```

## Strategy 2: queue\_after

Use this when the new intent should run as soon as the device is free, without losing the existing schedule. The new action is deferred until the active one terminates.

```bash theme={null}
curl -X POST https://api.amps.ai/battery/device_abc123 \
  -H "x-api-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "action": {
      "command": "auto.balance"
    },
    "onConflict": "queue_after"
  }'
```

The new action is scheduled to fire the moment the colliding one terminates.

```json theme={null}
{
  "success": true,
  "data": {
    "id": "act_pending_010",
    "deviceId": "device_abc123",
    "deviceType": "battery",
    "command": "auto.balance",
    "parameters": null,
    "state": "scheduled",
    "createdAt": "2026-05-08T12:00:02.000Z",
    "start": "2026-05-09T22:00:00.000Z",
    "links": { "self": "/actions/act_pending_010" }
  },
  "meta": { "requestId": "req_0dV4rEhU", "environment": "sandbox", "timestamp": "2026-05-08T12:00:02.000Z", "latencyMs": 87 }
}
```

## When cancel\_and\_replace cannot help

`cancel_and_replace` only works while the conflicting action is `scheduled`. If the OEM has already accepted the write, you get a 409 with the in-flight IDs and no strategies that would resolve it.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "CONFLICT_IN_EXECUTION",
    "message": "The conflicting action is already in progress and cannot be cancelled. Wait for it to complete or fail.",
    "details": {
      "reason": "conflicting_action_in_progress",
      "conflictingActionIds": ["act_inflight_002"]
    }
  },
  "meta": {
    "requestId": "req_3dC6hOsT",
    "timestamp": "2026-05-08T12:00:03.000Z",
    "path": "/battery/device_abc123",
    "latencyMs": 18
  }
}
```

Poll the in-flight action. Retry the push once it reaches `completed`, `failed`, or `cancelled`.

## SCHEDULER\_ACTIVE: a different kind of conflict

Some OEMs, FoxESS-class devices among them, require their native scheduler to be disabled before they will accept direct mode writes. When the scheduler is active, the action result carries `SCHEDULER_ACTIVE`.

```json theme={null}
{
  "success": true,
  "data": {
    "id": "act_failed_003",
    "deviceId": "device_abc123",
    "deviceType": "battery",
    "command": "charge",
    "parameters": { "target": { "value": 95, "unit": "percent" } },
    "state": "failed",
    "result": null,
    "errorCode": "SCHEDULER_ACTIVE",
    "errorMessage": "A schedule is currently active on the device and must be cleared first.",
    "createdAt": "2026-05-08T12:10:00.000Z",
    "updatedAt": "2026-05-08T12:10:32.000Z",
    "acknowledgedAt": "2026-05-08T12:10:01.000Z",
    "completedAt": "2026-05-08T12:10:32.000Z",
    "start": null,
    "end": null,
    "links": { "self": "/actions/act_failed_003" }
  },
  "meta": { "requestId": "req_1eW5sFiV", "environment": "sandbox", "timestamp": "2026-05-08T12:10:32.000Z", "latencyMs": 13 }
}
```

Resolve by re-pushing the same action with `onConflict: "cancel_and_replace"`. The native scheduler is cleared, the direct command is applied, and the new action's lifecycle reports back as normal. No separate cancel call is required.

## What next

<CardGroup cols={2}>
  <Card title="Cancel an action" icon="x" href="/guides/cookbook/cancel-action">
    The dedicated cancel endpoint and when to reach for it.
  </Card>

  <Card title="Schedule a charge for later" icon="moon" href="/guides/cookbook/schedule-charge-later">
    The kind of windowed action that triggers conflicts.
  </Card>

  <Card title="Auto modes" icon="sparkles" href="/guides/cookbook/auto-balanced">
    Hand control back to the platform after resolving a conflict.
  </Card>

  <Card title="Subscribe to webhooks" icon="webhook" href="/guides/cookbook/subscribe-webhooks">
    Detect SCHEDULER\_ACTIVE failures without polling.
  </Card>
</CardGroup>

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "HowTo",
"name": "Handle a 409 Conflict",
"description": "Resolve 409 CONFLICT when an action is in flight. Use onConflict cancel_and_replace, queue_after, or wait. Plus SCHEDULER_ACTIVE recovery.",
"step": [
{
  "@type": "HowToStep",
  "name": "Overview",
  "position": 1
},
{
  "@type": "HowToStep",
  "name": "Anatomy of a 409 CONFLICT",
  "position": 2
},
{
  "@type": "HowToStep",
  "name": "Strategy 1: cancel_and_replace",
  "position": 3
},
{
  "@type": "HowToStep",
  "name": "Strategy 2: queue_after",
  "position": 4
},
{
  "@type": "HowToStep",
  "name": "When cancel_and_replace cannot help",
  "position": 5
},
{
  "@type": "HowToStep",
  "name": "SCHEDULER_ACTIVE: a different kind of conflict",
  "position": 6
}
]
})}}
/>
