When to use this API
Use the cutover cluster when you have already finished:- Migration loads complete via
POST /v1/entities/{eid}/migration/{resource}/bulkfor accounts, products, orders, journal-entries, vendor-bills, ap-payments and the other supported resources. Each call takes up to 1,000 records (some resources have a lower per-call limit, and a call over it returns400 batch_too_large), needs anexternal_sourcenaming the system the records came from, takes aconflict_modeofskip(the default),update,replaceorerrorfor records that already exist, and returns202with a job to poll. Send?dry_run=truefirst to validate a batch without writing anything. - Reconciliation checks pass. Trial balance ties; AR aging matches the source; AP aging matches the source; resource counts within tolerance. See Reconciliation API.
- 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.
Required scope
Cutover endpoints are gated on two elevated scopes: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 > System > Developers (the API Keys tab), 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
GET /v1/entities/{eid}/migration/cutover/status reports one of six current_state values:
POST .../rollback. Rollback emits operator commands; it does NOT auto-execute a snapshot restore.
Step 1: freeze the entity
reason is optional and is shown in the audit log; without it the reason is “Cutover initiated via API”. Response:
freeze_token. You need it for the snapshot and swap steps.
After freeze, every write from a caller without the freeze token returns HTTP 423 Locked with error: "entity_frozen" and type: "cutover_error". Browser users continue to see read-only data. A caller whose key has the migration:write scope and who sends the token in an X-Freeze-Token header is let through, which is how the bulk migration calls keep working during the freeze, 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 with idempotent: true. The call is idempotent.
Step 2: take the database snapshot
freeze_token is required (a missing one returns 422 missing_freeze_token); description is an optional note stored in the cutover log. Response (202 Accepted, async):
estimated_completion_at is the estimate to plan around (about 15 minutes in the live environment). The status and log endpoints record that the snapshot was requested and keep the snapshot_id; they do not report when the snapshot finishes, so allow for the estimate (or confirm with your Arcus contact) before you swap. In the development environment the call returns a stub snapshot (dev_stub: true) and no real snapshot is taken. In the live environment the call is refused with 403 prod_gate_required until Arcus has enabled cutover snapshots for the cutover window.
The snapshot is the rollback target. Do not proceed to swap until the snapshot should be complete. Snapshot retention is unlimited by default; sweep manually post-cutover-window if you no longer need it.
Step 3: emit swap commands
snapshot_id and freeze_token are both required (a missing one returns 422 missing_snapshot_id or 422 missing_freeze_token). You can also name the application version the commands should target; the API reference lists the optional field for it. Response:
instructions holds one operator command or note per swap target: the application alias, the DNS flip and the identity provider swap. The exact strings are environment-specific; for a non-live environment the DNS and identity provider entries say that nothing needs to change.
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, and it records the swap in the cutover log.
Run them in order. Confirm each succeeds before moving to the next.
Step 4: verify
as_of_date is optional and defaults to today. Verify runs four reconciliation reads against the entity:
- Trial balance: total debits and total credits, and whether they balance.
- AR aging: the total receivable.
- AP aging: the total payable.
- Resource counts: the counts of migrated records by resource.
passed: true) only when all four reads ran without error and the trial balance is in balance. The AR aging, AP aging and count figures are returned for you to compare with the source system; the API does not compare them for you. Response:
results.errors[] as { "check": "...", "error": "..." } and passed is false. A trial balance that is out of balance also gives passed: false. A failed verification cannot be used to unfreeze. 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
verify_log_id is the only field. Unfreeze is refused with 422 missing_verify_log_id when it is left out, and with 422 verify_required when it does not name a passed verification of this entity from the last 4 hours. Response:
cutover_completed action, and the status 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.confirm field MUST equal {entity_id}:rollback. The API rejects any other value with HTTP 422 and the code rollback_confirmation_required, and a missing snapshot_id with 422 missing_snapshot_id. This is the same pattern Stripe Connect uses for destructive operations. You can also name the application version to roll back to (the API reference lists the optional field); without it the commands roll back to the previous version.
Response:
warnings list is part of the plan and is worth reading before you run anything. It says that the restore creates a NEW database instance and does not delete the existing one, that DNS must be moved by hand once the restored instance is available, and that any transaction committed after the snapshot was taken is lost on rollback.
Run the commands 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:migration:read (a key with migration:write or migration:admin also works). Status returns frozen, current_state, frozen_at, frozen_reason, snapshot_id, the latest_action and the ten most recent entries in recent_log. The log is newest first and append-only; each entry has an action (freeze_engaged, snapshot_taken, swap_initiated, verify_result, unfreeze_engaged, rollback_initiated or cutover_completed), a status (in_progress, succeeded, failed or reverted), performed_at, any snapshot id, the verification_result, error_text and notes. Page it with limit (up to 100, default 20) and starting_after, set to the id of the last entry you received; has_more says whether another page exists.
Webhook events
If you have a webhook endpoint subscribed to the cutover event family, the API emits the following during a cutover:migration.snapshot_takenmigration.cutover_initiated(when the swap commands are issued)migration.cutover_verifiedmigration.cutover_completedmigration.cutover_rolled_back(only on rollback path)
migration.freeze_engaged and migration.unfreeze_engaged are registered but not sent yet; freezing and unfreezing show up in the cutover log only.
Signing follows the standard HMAC-SHA256 pattern. See 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_tokenon 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 - the bulk-load pass that populates the entity before cutover.
- Reconciliation API - the read endpoints that prove the data is correct before you flip the switch.
- Webhooks - subscribe to cutover events for observability.
- Idempotency -
Idempotency-Keyheader semantics across all endpoints.

