Skip to main content

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.

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:
curl -X POST https://api.amps.ai/battery/dev_abc123 \
  -H "x-api-key: sk_live_abc123xyz" \
  -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.
{
  "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": {
      "conflictingActionIds": ["act_pending_001"],
      "strategies": ["cancel_and_replace", "queue_after"]
    }
  },
  "meta": {
    "requestId": "req_2cB5gNqR",
    "timestamp": "2026-05-08T12:00:00.000Z",
    "path": "/battery/dev_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.
curl -X POST https://api.amps.ai/battery/dev_abc123 \
  -H "x-api-key: sk_live_abc123xyz" \
  -H "Content-Type: application/json" \
  -d '{
    "action": {
      "command": "discharge",
      "parameters": {
        "target": { "value": 30, "unit": "percent" }
      }
    },
    "onConflict": "cancel_and_replace"
  }'
act_pending_001 is cancelled and the new push is accepted.
{
  "actionId": "act_inflight_009",
  "state": "acknowledged",
  "type": "battery:set_operation_mode",
  "createdAt": "2026-05-08T12:00:01.000Z"
}
A follow-up read of the cancelled action confirms the transition:
{
  "id": "act_pending_001",
  "state": "cancelled",
  "updatedAt": "2026-05-08T12:00:01.000Z"
}

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.
curl -X POST https://api.amps.ai/battery/dev_abc123 \
  -H "x-api-key: sk_live_abc123xyz" \
  -H "Content-Type: application/json" \
  -d '{
    "action": {
      "command": "auto.balanced"
    },
    "onConflict": "queue_after"
  }'
The new action is scheduled to fire the moment the colliding one terminates.
{
  "actionId": "act_pending_010",
  "state": "scheduled",
  "type": "battery:set_operation_mode",
  "createdAt": "2026-05-08T12:00:02.000Z",
  "start": "2026-05-09T22:00:00.000Z"
}

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.
{
  "success": false,
  "error": {
    "code": "CONFLICT",
    "message": "The conflicting action is already in progress and cannot be cancelled. Wait for it to complete or fail.",
    "details": {
      "conflictingActionIds": ["act_inflight_002"]
    }
  },
  "meta": {
    "requestId": "req_3dC6hOsT",
    "timestamp": "2026-05-08T12:00:03.000Z",
    "path": "/battery/dev_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.
{
  "id": "act_failed_003",
  "deviceId": "dev_abc123",
  "type": "battery:set_operation_mode",
  "state": "failed",
  "parameters": {
    "mode": "charge",
    "target": { "value": 95, "unit": "percent" }
  },
  "result": null,
  "errorCode": "SCHEDULER_ACTIVE",
  "errorMessage": "Native scheduler is active; clear it before issuing a direct command.",
  "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"
}
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

Cancel an action

The dedicated cancel endpoint and when to reach for it.

Charge overnight

The kind of windowed action that triggers conflicts.

Auto modes

Hand control back to the platform after resolving a conflict.

Subscribe to webhooks

Detect SCHEDULER_ACTIVE failures without polling.