Skip to main content

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.

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 and the bulk import + jobs cluster.

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.
  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:
ScopeEndpoints that require it
migration:admin + migration:writefreeze, snapshot, swap, unfreeze, rollback
migration:writeverify (runs the reconciliation suite)
migration:readstatus, 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

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:
{
  "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

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):
{
  "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

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:
{
  "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

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:
{
  "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

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:
{
  "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.
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:
{
  "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:
curl https://api.arcuserp.com/v1/entities/{ENTITY_ID}/migration/cutover/status \
  -H "Authorization: Bearer ark_live_..."
Or page through the full action log:
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.

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.
  • 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-Key header semantics across all endpoints.