> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting: RESIDENCY_* data-residency errors (409/404)

> Resolve the four data-residency refusal codes on the pin/lock/unlock surface — RESIDENCY_NOT_FOUND, RESIDENCY_NOT_ENFORCED, RESIDENCY_REGION_UNAVAILABLE, and RESIDENCY_LOCKED — with worked cURL for each and the bundle support needs.

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

| Code | HTTP | Fires on | Owner | One-line fix |
| - | - | - | - | - |
| `RESIDENCY_NOT_FOUND` | 404 | the unlock call when no config exists | Tenant | Pin the config with `PUT` first |
| `RESIDENCY_NOT_ENFORCED` | 409 | the lock call when no enforced pin exists | Tenant | Set `enforced: true` on a PUT first |
| `RESIDENCY_REGION_UNAVAILABLE` | 409 | an enforcing PUT to a non-provisioned region | Platform provisioning | Pick a region whose `availability` is `available`, or pin advisory-only |
| `RESIDENCY_LOCKED` | 409 | a PUT that changes region while the pin is locked | Tenant | Call `unlock` first, or accept the pin is frozen by design |

The full concept (advisory vs. enforced pins, boundaries, the lock
mechanism) is [Data placement and residency](/concepts/data-placement-and-residency);
this page is the recovery runbook for the refusals.

<Note>
  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.
</Note>

***

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

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..."
```

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:

```json theme={null}
{
  "error": {
    "code": "RESIDENCY_NOT_FOUND",
    "message": "No data-residency configuration exists for this organization.",
    "status": 404
  },
  "meta": { "request_id": "req_2b1c…" }
}
```

Bootstrap the pin, then continue your flow:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"region": "eu", "enforced": true}'
```

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:

```json theme={null}
{
  "error": {
    "code": "RESIDENCY_NOT_ENFORCED",
    "message": "Pin and enforce a residency region (PUT with enforced=true) before locking it.",
    "status": 409
  }
}
```

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`:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"region": "eu", "enforced": true}'
```

Then lock:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/data-residency/lock \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"notes": "Freeze EU boundary ahead of ISO27001 renewal audit."}'
```

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:

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json"
```

Each `data.regions[]` entry carries `region`, `label`, `jurisdictions`,
`frameworks`, and `availability` — enforce only an entry that reads
`available`.

```bash theme={null}
# Pick an available region first, THEN enforce it:
curl -X PUT https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"region": "eu", "enforced": true}'
```

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:

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..."
```

`data.config.locked` is `true` and `data.config.region` names the frozen
boundary. To perform a governed region migration, unlock first, then
re-pin:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/compliance/data-residency/unlock \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"notes": "Planned migration: uk → eu with customer sign-off"}'
```

Followed by:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/compliance/data-residency \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"region": "eu", "enforced": true, "justification": "Customer contract amendment 2026-03"}'
```

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.

```bash theme={null}
export BASE=https://api.orbit.devotel.io/api/v1
export KEY="X-API-Key: dv_live_sk_..."
```

```bash theme={null}
# RESIDENCY_NOT_FOUND — unpin-then-unlock yields 404; pin first:
curl -H "$KEY" $BASE/compliance/data-residency
curl -X PUT -H "$KEY" -H 'Content-Type: application/json' \
  -d '{"region": "eu"}' $BASE/compliance/data-residency
```

```bash theme={null}
# RESIDENCY_NOT_ENFORCED — lock a non-enforced pin yields 409; enforce then lock:
curl -X PUT -H "$KEY" -H 'Content-Type: application/json' \
  -d '{"region": "eu", "enforced": true}' $BASE/compliance/data-residency
curl -X POST $BASE/compliance/data-residency/lock -H "$KEY" -H 'Content-Type: application/json'
```

```bash theme={null}
# RESIDENCY_REGION_UNAVAILABLE — read catalog, then enforce only an 'available' region:
curl -s -H "$KEY" $BASE/compliance/data-residency  # inspect data.regions[].availability
```

```bash theme={null}
# RESIDENCY_LOCKED — read lock state, unlock deliberately, re-pin:
curl -s -H "$KEY" $BASE/compliance/data-residency     # data.config.locked is true
curl -X POST $BASE/compliance/data-residency/unlock -H "$KEY" -H 'Content-Type: application/json'
curl -X PUT -H "$KEY" -H 'Content-Type: application/json' \
  -d '{"region": "uk"}' $BASE/compliance/data-residency
```

***

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

* [Data placement and residency](/concepts/data-placement-and-residency) —
  the WHERE (tenant isolation) vs. WHERE-geographically (the pin) vs.
  region-of-compute concept.
* [Messaging data residency](/concepts/messaging-data-residency) — the
  messaging projection of the pin and the composed
  `storedAndProcessedInRegion` verdict.
* [Data residency overview](/compliance/data-residency-overview) — which
  surface answers which channel's residency question.
* [Error Code Reference](/reference/error-codes) — the remaining
  codes the API envelopes carry.
