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

# Cutover orchestration

> Programmatic cutover from a legacy ERP to Arcus: freeze, snapshot, swap, verify, unfreeze, and rollback via the public API.

The cutover API turns a hard-cutover weekend from a manual SQL playbook into a sequenced run of HTTP calls. Every step writes to an append-only audit log so a future operator can reconstruct exactly what happened.

This guide covers the production cutover flow. For the data-load that runs in the days leading up to cutover, see [Migrating Data into Arcus](/guides/migration-from-versa) and the [bulk import + jobs cluster](/api-reference/migration/submit-a-bulk-import-job-for-a-resource-type).

## When to use this API

Use the cutover cluster when you have already finished:

1. **Migration loads** complete via `POST /v1/entities/{eid}/migration/{resource}/bulk` for accounts, products, orders, journal-entries, vendor-bills, ap-payments.
2. **Reconciliation checks pass.** Trial balance ties; AR aging matches the source; AP aging matches the source; resource counts within tolerance. See [Reconciliation API](/guides/reconciliation-api).
3. **Pilot slices verified.** At minimum one named customer, then a wider slice, then a single fiscal year, then a full dry run into a clean entity. Each slice walked end to end in the Arcus UI.

Once those three gates are green the cutover sequence is a six-call API workflow.

## Required scope

Cutover endpoints are gated on two elevated scopes:

| Scope                                 | Endpoints that require it                  |
| ------------------------------------- | ------------------------------------------ |
| `migration:admin` + `migration:write` | freeze, snapshot, swap, unfreeze, rollback |
| `migration:write`                     | verify (runs the reconciliation suite)     |
| `migration:read`                      | status, log (read-only)                    |

`migration:admin` is intentionally restricted. Grant it only to the operator who will execute the cutover window. Both `migration:admin` and `migration:write` must be present on the same key for the destructive endpoints. Issue the key from **Settings > Developers > API Keys**, tick both elevated-scope checkboxes, acknowledge the elevated-permission warning, and store the secret in your password manager. Revoke the key immediately after cutover is complete.

## The state machine

Every cutover entity is in one of seven states, visible via `GET /v1/entities/{eid}/migration/cutover/status`:

```
idle  ->  frozen  ->  snapshot_in_progress  ->  snapshot_taken
                                                     |
                                                     v
                                            swap_initiated  ->  verified
                                                                   |
                                                                   v
                                                              completed (idle)
```

At any frozen state the operator can transition to `rolling_back` via `POST .../rollback`. Rollback emits operator commands; it does NOT auto-execute a snapshot restore.

## Step 1: freeze the entity

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/freeze \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{"reason":"Production cutover 2026-07-12"}'
```

Response:

```json theme={null}
{
  "object": "entity_freeze_status",
  "entity_id": "65dcd234-53c3-4f54-b772-ee349528d497",
  "frozen": true,
  "frozen_at": "2026-07-12T08:00:00.000Z",
  "frozen_reason": "Production cutover 2026-07-12",
  "freeze_token": "39a2e43f-9b8f-4a41-ae5c-5cc11f2a8edf",
  "log_id": "98bb3084-d219-4945-ae6b-a2c06a31bfaf"
}
```

Capture the `freeze_token`. You need it for every subsequent step until unfreeze.

After freeze, **all non-migration writes return HTTP 423 Locked** with a `migration_in_progress` envelope. Browser users continue to see read-only data; write attempts surface a clear banner. Migration write endpoints (the bulk cluster) continue to function, so a soft-cutover top-up sync is still possible.

If you call freeze on an already-frozen entity, you get the existing `freeze_token` back. The call is idempotent.

## Step 2: take the database snapshot

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/snapshot \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{"freeze_token":"39a2e43f-9b8f-4a41-ae5c-5cc11f2a8edf"}'
```

Response (202 Accepted, async):

```json theme={null}
{
  "object": "database_snapshot",
  "snapshot_id": "arcus-cutover-65dcd234-1689156034",
  "status": "creating",
  "db_instance_id": "arcus-db",
  "percent_progress": 0,
  "started_at": "2026-07-12T08:01:14.522Z"
}
```

Poll the same endpoint or `GET .../cutover/status` until `percent_progress: 100` and `status: "available"`. Snapshots typically complete in 10 to 25 minutes for a small-to-medium database tier.

The snapshot is the rollback target. Do not proceed to swap until the snapshot is `available`. Snapshot retention is unlimited by default; sweep manually post-cutover-window if you no longer need it.

## Step 3: emit swap commands

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/swap \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "freeze_token":"39a2e43f-...",
    "snapshot_id":"arcus-cutover-65dcd234-1689156034",
    "swap_targets": ["lambda_alias","dns","cognito"]
  }'
```

Response:

```json theme={null}
{
  "object": "cutover_swap_initiated",
  "swap_log_id": "...",
  "operator_commands": [
    "<update deployment alias to the new release version>",
    "<repoint DNS to the new origin>",
    "<update identity provider configuration>"
  ]
}
```

The exact command strings are environment-specific. Your operations team configures the swap-target list when the entity is provisioned; common targets are the application deployment alias, DNS records, and identity-provider configuration.

The API does NOT execute these commands. It emits the exact strings to run in your operator shell so you keep a clean audit trail of what changed at the infrastructure layer.

Run them in order. Confirm each succeeds before moving to the next.

## Step 4: verify

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/verify \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{"as_of_date":"2026-07-12"}'
```

Verify runs the reconciliation suite end to end:

* Trial balance debits and credits balance to the penny.
* AR aging total equals the AR control account.
* AP aging total equals the AP control account.
* Inventory valuation snapshot matches the inventory subledger.
* Cash balances match each bank reconciliation row.
* Resource counts are within tolerance (default 0; configurable per call).

Response:

```json theme={null}
{
  "object": "cutover_verify_result",
  "verify_log_id": "41c88501-1b8f-41ff-a557-43abb9029a3d",
  "status": "succeeded",
  "checks": {
    "trial_balance": { "passed": true, "delta_cents": 0 },
    "ar_aging":      { "passed": true, "delta_cents": 0 },
    "ap_aging":      { "passed": true, "delta_cents": 0 },
    "inventory":     { "passed": true, "delta_cents": 0 },
    "cash":          { "passed": true, "delta_cents": 0 },
    "counts":        { "passed": true, "delta": {} }
  },
  "performed_at": "2026-07-12T08:34:41.271Z"
}
```

If any check fails, `status` is `failed`, the response includes per-check deltas, and unfreeze is blocked. Either rollback (Step 6) or fix the data and re-run verify.

`verify_log_id` is valid for **4 hours**. After that it expires and you must re-verify before unfreezing.

## Step 5: unfreeze

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/unfreeze \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "freeze_token":"39a2e43f-...",
    "verify_log_id":"41c88501-1b8f-41ff-a557-43abb9029a3d"
  }'
```

Response:

```json theme={null}
{
  "object": "entity_freeze_status",
  "entity_id": "65dcd234-53c3-4f54-b772-ee349528d497",
  "frozen": false,
  "unfrozen_at": "2026-07-12T08:36:12.001Z",
  "completed_log_id": "..."
}
```

The entity is now live on Arcus. Writes resume. The cutover state machine returns to `idle`. Revoke the API key.

## Step 6 (only if needed): rollback

Rollback emits the operator commands required to restore the pre-cutover snapshot and re-point traffic at the legacy ERP. It does **not** auto-execute anything destructive.

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/rollback \
  -H "Authorization: Bearer ark_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "snapshot_id":"arcus-cutover-65dcd234-1689156034",
    "confirm":"65dcd234-53c3-4f54-b772-ee349528d497:rollback"
  }'
```

The `confirm` field MUST equal `{entity_id}:rollback`. The API rejects any other value with HTTP 422. This is the same pattern Stripe Connect uses for destructive operations.

Response:

```json theme={null}
{
  "object": "cutover_rollback_plan",
  "rollback_log_id": "...",
  "operator_commands": [
    "<restore the database from snapshot arcus-cutover-...>",
    "<repoint DNS back to the legacy source>",
    "<roll the deployment alias back to the previous version>"
  ]
}
```

Run them in order. Confirm each succeeds. Customer-facing communication is your responsibility; the API does not send anything.

## Status and log

At any point during cutover you can read the current state:

```bash theme={null}
curl https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/status \
  -H "Authorization: Bearer ark_live_..."
```

Or page through the full action log:

```bash theme={null}
curl 'https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/log?limit=20' \
  -H "Authorization: Bearer ark_live_..."
```

Both endpoints require only `migration:read`. The log is append-only and includes every action with timestamp, performed-by, status, and the JSON payload that initiated it.

## Webhook events

If you have a webhook endpoint subscribed to the cutover event family, the API emits the following during a cutover:

* `migration.freeze_engaged`
* `migration.snapshot_taken`
* `migration.swap_initiated`
* `migration.cutover_verified`
* `migration.unfreeze_engaged`
* `migration.cutover_completed`
* `migration.cutover_rolled_back` (only on rollback path)

Signing follows the standard HMAC-SHA256 pattern. See [Webhooks](/concepts/webhooks).

## Operational notes

* **Run cutover during a planned freeze window.** Customers and operators see read-only data for the duration. Targets are typically a Friday EOD freeze, Saturday snapshot + swap + verify, Sunday final tests, Monday go-live.
* **Have the rollback playbook open in another tab.** If verify fails on Saturday evening, you want zero ambiguity about how to back out.
* **Snapshots are not free.** Cloud database snapshots are billed by GB-month; keep the snapshot for 30 to 90 days as your safety net, then delete it.
* **Do not skip the verify step.** Unfreeze is gated on a successful verify within the last 4 hours. There is no override.
* **Capture the `freeze_token` on Step 1.** It is the only path through Steps 2 to 5. If you lose it, you must call freeze again (which returns the existing token; safe).
* **Keep the snapshot retention long enough.** Once the snapshot expires, rollback is no longer possible from this snapshot. Take a manual snapshot before deletion if you want a longer safety window.

## Related

* [Migrating Data into Arcus](/guides/migration-from-versa) - the bulk-load pass that populates the entity before cutover.
* [Reconciliation API](/guides/reconciliation-api) - the read endpoints that prove the data is correct before you flip the switch.
* [Webhooks](/concepts/webhooks) - subscribe to cutover events for observability.
* [Idempotency](/concepts/idempotency) - `Idempotency-Key` header semantics across all endpoints.
