Skip to main content

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

ScenarioResponse
First request with a new keyNormal 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 key409 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:
// 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,
});
import uuid

order = arcus.orders.create(
    {"document_type": "sales_order", "account_id": "acc_01H..."},
    idempotency_key=str(uuid.uuid4()),
)
order, err := client.Orders.Create(ctx, &arcus.OrderCreateParams{
    DocumentType: arcus.String("sales_order"),
    AccountID:    arcus.String("acc_01H..."),
}, arcus.WithIdempotencyKey(uuid.New().String()))

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.
# 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.
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 and Reconciliation API guides for end-to-end usage.