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

# Idempotency

> Use the Idempotency-Key header to safely retry failed requests without creating duplicates.

## What is idempotency?

An idempotent operation can be repeated multiple times without changing the result beyond the first execution. On the Arcus API, idempotency protects you from creating duplicate orders, payments, or other records when a request fails partway through (network timeout, server restart, etc.).

## How to use it

Include the `Idempotency-Key` header on any POST, PATCH, or DELETE request. The value must be unique per operation -- a UUID v4 is the recommended format.

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/$ARCUS_ENTITY_ID/orders \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Idempotency-Key: 7f3b2c4d-1e5a-4f9b-8c2d-0a1b3c5d7e9f" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
```

## Replay behavior

| Scenario                                    | Response                                                |
| ------------------------------------------- | ------------------------------------------------------- |
| First request with a new key                | Normal execution; response stored                       |
| Retry with the same key (within 24 hours)   | Original response returned immediately, no re-execution |
| Retry with the same key (after 24 hours)    | Key expired; request treated as new                     |
| Different request body with an existing key | `409 Conflict` with `code: idempotency_key_mismatch`    |

## 24-hour replay window

Arcus stores idempotency keys and their responses for **24 hours**. After that window, the key is expired and a new request with the same key is treated as a fresh operation. If your retry strategy spans more than 24 hours, generate a new key.

## Key format

The key can be any string up to 255 characters. Best practices:

```
# UUID v4 (simplest)
7f3b2c4d-1e5a-4f9b-8c2d-0a1b3c5d7e9f

# Prefixed (easier to debug in logs)
order-create-acc_01H-2024-01-15T10:30:00Z

# Client transaction ID (if you have one)
txn_import_batch_42_row_1337
```

## SDK usage

All SDKs handle idempotency keys automatically for safe retries:

<CodeGroup>
  ```javascript Node theme={null}
  // Pass explicitly
  const order = await arcus.orders.create(
    { document_type: 'sales_order', account_id: 'acc_01H...' },
    { idempotencyKey: crypto.randomUUID() }
  );

  // Or enable auto-retry (SDK generates and manages keys)
  const arcus = new Arcus({
    apiKey: process.env.ARCUS_API_KEY,
    entityId: process.env.ARCUS_ENTITY_ID,
    maxRetries: 3,
  });
  ```

  ```python Python theme={null}
  import uuid

  order = arcus.orders.create(
      {"document_type": "sales_order", "account_id": "acc_01H..."},
      idempotency_key=str(uuid.uuid4()),
  )
  ```

  ```go Go theme={null}
  order, err := client.Orders.Create(ctx, &arcus.OrderCreateParams{
      DocumentType: arcus.String("sales_order"),
      AccountID:    arcus.String("acc_01H..."),
  }, arcus.WithIdempotencyKey(uuid.New().String()))
  ```
</CodeGroup>

## When to use idempotency keys

Use them on **every** POST, PATCH, or DELETE that mutates data. The most critical cases:

* Creating orders, invoices, or payments
* Processing refunds
* Purchasing shipping labels
* Posting journal entries
* Any financial or inventory-affecting operation

GET requests are inherently idempotent and do not need a key.

## Bulk imports and async jobs

The migration cluster endpoint `POST /v1/entities/{eid}/migration/{resource}/bulk` accepts `Idempotency-Key` and gives you two distinct safety nets:

1. **Request-level idempotency** (this header). If your loader retries the same bulk request after a network timeout, you get the original `migration_job` row back with the same `id` and status. No duplicate jobs queued.

2. **Per-record provenance idempotency** (`external_source` + `external_id`). Even if you submit the same record across multiple jobs over time, the canonical handlers use the `(entity_id, external_source, external_id)` partial unique index to upsert. Re-runs against the same source data are safe.

Together this means a migration loader can crash midway through a 10,000-record batch, restart with the same `Idempotency-Key`, and pick up where it left off without creating duplicates.

```bash theme={null}
# Submit a bulk import job
curl -X POST 'https://api.arcuserp.com/v1/entities/{eid}/migration/accounts/bulk' \
  -H "Authorization: Bearer ark_live_..." \
  -H "Idempotency-Key: wss-accounts-batch-0142" \
  -H "Content-Type: application/json" \
  -d '{
    "external_source": "versa",
    "conflict_mode": "skip",
    "records": [ ... ]
  }'
# 202 Accepted -> { object: "migration_job", id: "...", status: "pending" }

# Network failure; loader retries with the SAME Idempotency-Key
curl -X POST 'https://api.arcuserp.com/v1/entities/{eid}/migration/accounts/bulk' \
  -H "Authorization: Bearer ark_live_..." \
  -H "Idempotency-Key: wss-accounts-batch-0142" \
  ...
# 202 Accepted -> same { id: "...", status: "pending" } (no new row)
```

### `conflict_mode` semantics

The bulk endpoint takes a `conflict_mode` body field that controls behavior when a record already exists at the provenance key `(external_source, external_id)`:

* `skip` (default) - leave the existing row untouched, count it as `skipped`.
* `update` - PATCH the existing row with the new field values; preserves any Arcus-side fields not in the payload.
* `replace` - DELETE the existing row and INSERT the new payload as if it were a fresh record. Use with care.
* `error` - return an error envelope per-record and continue processing the rest.

### Dry-run mode

Add `?dry_run=true` to validate a payload without writing anything. The endpoint returns a synchronous 200 with the result envelope, no `migration_job` row is created, and zero DB writes occur. Use this in CI to catch schema errors before the real run.

```bash theme={null}
curl -X POST 'https://api.arcuserp.com/v1/entities/{eid}/migration/accounts/bulk?dry_run=true' \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{ ... }'
# 200 -> { object: "migration_dry_run_result", total_records: 500, ... }
```

See the [Migrating Data into Arcus](/guides/migration-from-versa) and [Reconciliation API](/guides/reconciliation-api) guides for end-to-end usage.
