Pin, lock, and run a governed data-residency migration
The org-wide data-residency control pins PII, audit logs, and recordings to a geographic boundary, reports the per-plane posture honestly, and freezes the pin behind a deliberate lock. This page walks the full sequence — pin → verify → lock → (governed unlock) → migrate → re-lock — with the four refusal codes you can hit along the way and the cURL to run at each step.The org-wide pin surface, and what it does not cover
The control surface lives under/api/v1/compliance/data-residency:
Every write needs an owner or admin API key, and every change lands
in your audit log.
This org-wide pin is one of two residency surfaces. It covers the
org-wide data planes (PII, audit logs, compliance documents, and
the messaging / CDP planes projected below). It does not move the
voice region — call recordings, voicemail, and live media follow the
workspace voice-region pin documented in
Voice data residency. The
Data residency overview maps
which surface answers which channel’s question; set the voice-region pin
and the org-wide pin deliberately, never assume one covers the other.
Step order: read → pin → verify → lock
The sequence is four deliberate steps. Each one is verifiable from the GET before you move on — do not assume a step landed just because the call returned 200.1
Read the current config
Call
GET /api/v1/compliance/data-residency. A fresh org returns 200
with data.config: null, the region catalog, a resolved bucket, an
API-region status block, and the per-plane data-plane matrix. Check
the catalog before you pin — a region’s
data.regions[].availability must read available before the pin
can be enforced; a preview region pin is advisory-only by design.2
Pin the region (PUT)
Issue a
PUT with { "region": "<code>", "enforced": true, "justification": "…" }. A first-time pin returns 201, an update
returns 200, and the response body is the full GET view (plus the
audit-logged write). Enforcement is what turns the recorded prefer-
ence into the region-scoped storage boundary, so an advisory pin
(enforced: false) registers intent but never becomes “resident.”3
Verify with a GET before you move on
Re-read
GET /api/v1/compliance/data-residency and inspect
data.config.region, data.config.enforced, and the per-plane
matrix in data.data_planes. Read it back before you lock or
migrate — an accidental enforced: false body, or a
preview-region pin you then try to enforce, returns
RESIDENCY_REGION_UNAVAILABLE on the enforcing PUT.4
Lock the pin (POST …/lock)
Issue
POST /api/v1/compliance/data-residency/lock with an optional
{ "notes": "…" } annotation. The lock refuses with
RESIDENCY_NOT_ENFORCED (409) unless an enforced pin exists —
lock-as-a-guard only makes sense on a boundary that actually scopes
storage. Once locked, same-region updates to justification or
enforced still succeed; only a region change is refused.The four refusal codes, walked in the sequence
The surface uses exactly four refusal codes. Each one fires on a specific verb, so check the verb before you diagnose. The dedicated Troubleshooting: RESIDENCY_* errors page expands the recovery for each; the walk below lines them up with the sequence you actually run.A fixation on “region must be enforced before it matters” drives three
of the four codes:
RESIDENCY_NOT_ENFORCED guards the lock,
RESIDENCY_REGION_UNAVAILABLE guards the enforcing PUT, and the
storedAndProcessedInRegion projection on the plane reads treats an
advisory pin as intent-only. An advisory pin is a useful way to
register a region request before its boundary is graduated to GA — it
just is not residency. None of the refusals is a transient error; a
retry that does not change the state reads the same refusal forever.When the per-plane reads need explicit confirmation
The org-wide matrix is one object-wide view — but two data planes also expose their own projection, because an integrating buyer reads residency where it integrates rather than on a generic settings page. Read these before you certify a posture to a buyer or a supervisory authority.
Confirm the messaging read after onboarding a regulated SMS buyer and
the CDP read before you certify an ingested-profile posture. Both
project the same singleton config the compliance surface manages —
they are reads, not per-plane knobs, and they answer with the same
enforced pin the
GET /compliance/data-residency returns. The one
common mistake is treating the messaging/CDP storedAndProcessedInRegion
projection as a write answer: it reports the current posture, and only
PUT /compliance/data-residency changes it.
The governed migration sequence
Region migration is a deliberate governance event, not an edit. Treat it with a two-officer mindset: the officer who unlocks should be the one who re-locks at the end, and thenotes and justification bodies are
the written record of why.
- Announce and document. Record the reason for the migration, the target region, and the sign-off (customer contract, regulatory driver, or procurement change) before any API call. The unlock and re-pin will land in the audit log on both sides; the officer’s written justification is the only narrative between them.
- Unlock the pin.
POST /compliance/data-residency/unlockwith a{ "notes": "…" }body naming the reason. The call records the change only — it performs no data movement and wires no carrier. That distinction is load-bearing for audit: the surface records policy, and the audit log reads as a governance ledger. - Re-pin the target.
PUTthe new region withenforced: trueand ajustificationthat names the migration. Same-region enforcement (enforced: true→enforced: true) is also allowed on a locked pin if you ever need to refresh the justification without unlocking. - Run the data-plane migration out-of-band. Moving bytes between
regions is a service-operations step, not part of this surface —
contact Devotel to schedule it. Until the bytes move, the planes that
do not follow the pin report the gap honestly in the matrix
(
residentRegionstays on the actual store region and the plane’snotesays so). - Re-lock.
POST /compliance/data-residency/lockwith notes referencing the migration ticket. Re-read the GET and confirmdata.config.locked: trueanddata.config.regionnow name the new boundary — the sequence ends in the state you want, not the state the unlock left behind.
Worked cURL for each step
Set the base and header once; only the verb changes.config: null:
data.config.region,
data.config.enforced, and the plane rows in data.data_planes):
RESIDENCY_NOT_ENFORCED if
the config was not enforced:
RESIDENCY_LOCKED — same-region updates to justification are the
exception and succeed.
For a governed migration, unlock with a reason, re-pin the new region,
then re-lock:
See also
- Data residency overview — which surface answers which channel’s residency question, including the messaging subprocessor answer.
- Voice data residency — the workspace voice-region pin that covers recordings, voicemail, and live media. Set beside the org-wide pin; never instead of it.
- Messaging data residency — the
concept behind
GET /messaging/data-residencyand the composedstoredAndProcessedInRegionverdict. - Troubleshooting: RESIDENCY_* errors — the reference for each refusal code and what to bundle for support.
- Data placement and residency — the WHERE-isolation vs. WHERE-geographic vs. region-of-compute concept this pin composes with.