Skip to main content

Troubleshooting: RESIDENCY_* data-residency errors

The data-residency pin lives on the compliance surface: PUT /api/v1/compliance/data-residency pins (or re-pins) your organization’s region, POST /api/v1/compliance/data-residency/lock freezes it, POST /api/v1/compliance/data-residency/unlock frees it again for a governed region change, and GET /api/v1/compliance/data-residency reads the posture. Every write needs an owner or admin API key. Four refusal codes gate that surface: The full concept (advisory vs. enforced pins, boundaries, the lock mechanism) is Data placement and residency; this page is the recovery runbook for the refusals.
Residency is a tenant-owned control: you pin the region, you enforce it, you lock it, and you document it. Orbit records and reports the posture; the decision and its documentation are yours.

Symptom decoder — where each code fires

  • PUT /data-residency — the only write that can return RESIDENCY_LOCKED (you tried to change the region on a locked pin) or RESIDENCY_REGION_UNAVAILABLE (you tried to enforce a region the platform has not provisioned).
  • POST /data-residency/lock — returns RESIDENCY_NOT_ENFORCED when the config is missing entirely or pinned without enforcement.
  • POST /data-residency/unlock — returns RESIDENCY_NOT_FOUND when the config is missing (the one 404 the surface uses); a PUT with a changed region succeeds once the pin is unlocked.
  • GET /data-residency — read-only; returns 200 with data.config: null when nothing is pinned, plus the region catalog, resolved bucket, API-region status, and per-plane residency matrix. Use it to read state between every step below — do not assume a step advanced.

RESIDENCY_NOT_FOUND (404) — nothing pinned yet

This fires only on POST /data-residency/unlock. It means your organization has no residency config at all — there is nothing to unlock. The fix is to pin a config with PUT first. Read the state to confirm:
An empty posture comes back with 200 and "config": null — the GET never 404s on the absence. A typical reset trigger is a pasted recipe that runs the unlock before the pin sequence:
Bootstrap the pin, then continue your flow:
First-time pins return 201, updates return 200 with the full view. The 404 only exists because unlock-on-unpinned is a meaningful caller error to flag, not a state to toggle — omitting a PUT from your sequence is the only cause.

RESIDENCY_NOT_ENFORCED (409) — pinned but not enforced

POST /data-residency/lock refuses with this code when there is no enforced pin to lock. Enforcement is what turns a recorded region preference into an actual region-scoped storage boundary — pinning alone does nothing at rest. If your config exists with enforced: false (an advisory pin), it reads as not-enforced; if there is no config at all, that reads the same way. The response names the remediation:
Read the config (GET) and check data.config.enforced. If it is false — an advisory pin — re-issue the PUT with enforced: true against the same region, provided that region’s data.regions[].availability is available:
Then lock:
A 422 VALIDATION_ERROR on the PUT means the body shape failed (region outside the catalog, or a wrong field type) — that is the deterministic validation family, not one of the four codes here. And a lock call on an already-locked pin returns 200 idempotently: re-read locked on the GET instead of assuming a successful response moved the state.

RESIDENCY_REGION_UNAVAILABLE (409) — boundary not provisioned

An enforcing PUT (enforced: true in the request, or carried forward from the existing config) refuses with 409 when the requested region’s storage boundary is not provisioned on the platform — meaning not available in the GET region catalog. The catalog is deliberately honest about procurement posture: the EU boundary ships as available from the platform itself; every other region reads preview until its in-region storage is stood up, at which point it graduates to available. preview does not mean disabled — it means the platform refuses to claim an at-rest guarantee it cannot yet keep. Advisory pinning still works: a PUT with enforced: false (or simply omitting enforced, when nothing carries it forward) against a preview region succeeds and records your intent so you can register where your data needs to land. That is by design — enforcement is the one transition this code gates. List the regions before you enforce:
Each data.regions[] entry carries region, label, jurisdictions, frameworks, and availability — enforce only an entry that reads available.
If the region you need is still preview, open a Devotel support ticket and reference this 409 — region graduation is a platform-provisioning decision recorded per request, not something you toggle on your org.

RESIDENCY_LOCKED (409) — frozen by design

Once POST /data-residency/lock has run, region drift is forbidden: any PUT that names a different region than the locked pin refuses with 409 RESIDENCY_LOCKED. Same-region updates to justification or enforced still succeed on a locked pin — the guard fires only when the region would actually move. The freeze is deliberate. Bit-by-bit data already written under a pinned region must not be silently re-homed by a later PUT (GDPR Art. 44). The only move while locked is unlock — a guarded owner/admin change that itself lands in your audit log. Read the lock state:
data.config.locked is true and data.config.region names the frozen boundary. To perform a governed region migration, unlock first, then re-pin:
Followed by:
Then re-lock. The unlock→PUT arrow lands in the audit log on both sides (the unlock write, then the pin write) — there is no “forgot to re-lock” silent state; re-read locked on the GET so the sequence ends in the state you want. If your organization cannot reach an owner/admin to run the unlock (key rotation, offboarded owner, locked role assignment), escalate to Devotel support with the lock bundle below plus proof of ownership — the platform unlocks nothing outside the API and will flag a support-side unlock to a re-pin as suspicious in the org audit view.

Worked cURL per code

Every command below uses the same header; only the verb changes.

What not to do

  • Do not retry a 409 in a loop — all four codes are deterministic state refusals. A retry that does not change the state reads the same refusal forever.
  • Do not conflate codes — RESIDENCY_NOT_ENFORCED on lock and RESIDENCY_REGION_UNAVAILABLE on an enforcing PUT are siblings of the same gate; RESIDENCY_NOT_FOUND (404, unlock-only) and RESIDENCY_LOCKED (409, PUT-only) are unrelated. Check which verb the refusal came from before assuming the cause.
  • Do not treat an advisory pin as residency — a pinned-but-not- enforced (enforced: false) region records intent only; the region-scoped bucket assignment and the storedAndProcessedInRegion verdict on the messaging read both require enforcement.
  • Do not write a support ticket before reading the GET — the GET names the Config, Region catalog, and lock state precisely; a ticket opened before that read is harder than one pasted with it.

What to send support

Open a ticket (or post into your shared Devotel channel) with the bundle below. It is enough to reconstruct the lock state server-side without exposing your data-plane content:
  1. The refusal code and HTTP status (RESIDENCY_* + 409 or 404).
  2. meta.request_id from the error envelope.
  3. Your organization slug (e.g. org_XXXX) — never the raw tenant id unless support asks.
  4. The target region you attempted (and the region currently locked, if the 409 was on a PUT).
  5. A copy of GET /api/v1/compliance/data-residency so the config, region catalog, and lock state travel with the ticket.
  6. For a RESIDENCY_REGION_UNAVAILABLE consult: your regulatory driver (GDPR/LGPD sector note) and which channel’s plane you need in — region graduation needs per-plane provisioning, not just the flag.
Keep your API key out of the ticket, and paste cURL with the header redacted rather than the key itself.

See also