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

# Worked identity and org-context samples

> The startup introspection chain — call GET /me, read identity + tenant + memberships, decide on the caller's role — plus the authentication failures this probe surfaces first.

## Worked identity and org-context samples

Bootstrap the session, then decide: call `GET /api/v1/me` at startup before touching any resource operation, read `data.user`, `data.tenant`, and `data.organizations` out of the same envelope, and branch on the caller's `role` before you call a write path.

Every signed-in client resolves identity through this probe — the dashboard's org picker and the mobile client both bootstrap from it. Cache the response per session, keep `meta.request_id` for support correlation, and re-fetch only on org switch or role change.

### Read the authenticated caller

<Note>
  `GET /api/v1/me`
</Note>

Returns the caller's identity, the active tenant, and every organization the caller belongs to in one envelope.

**Request**

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "https://api.orbit.devotel.io/api/v1/me" \
      -H "X-API-Key: $ORBIT_API_KEY"
    ```

    ```typescript Node.js theme={null}
    const res = await fetch("https://api.orbit.devotel.io/api/v1/me", {
      headers: { "X-API-Key": process.env.ORBIT_API_KEY! },
    });
    console.log(await res.json());
    ```
  </CodeGroup>
</RequestExample>

**Response**

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "user": {
        "id": "usr_9f2a4c6e1b7d3e8f5012a4b6c8d0e2f4",
        "email": "jane@acme.example",
        "name": "Jane Cooper",
        "image": null,
        "role": "admin",
        "emailVerified": true,
        "twoFactorEnabled": false,
        "softphone_layout": "compact_bottom_bar",
        "voicemail_to_email_enabled": false,
        "voicemail_to_email_address": null,
        "inbox_signature": null,
        "inbox_show_archived_tags": false
      },
      "tenant": {
        "id": "ten_4c8d1e6a9b2f4d8c0e2a4c6e8b0d2f4a",
        "name": "Acme Logistics",
        "slug": "acme-logistics",
        "plan": "scale",
        "logoUrl": null
      },
      "organizations": [
        {
          "id": "org_2b6c8e0a4d6f8b0c2e4a6c8d0e2f4a6c",
          "name": "Acme Logistics",
          "slug": "acme-logistics",
          "logoUrl": null,
          "role": "admin"
        },
        {
          "id": "org_7d0e2a4c6e8b0d2f4a6c8e0a2c4e6a8b",
          "name": "Acme Retail",
          "slug": "acme-retail",
          "logoUrl": "https://cdn.orbit.devotel.io/org-logos/acme-retail.png",
          "role": "member"
        }
      ]
    },
    "meta": {
      "request_id": "req_me_bootstrap_01",
      "timestamp": "2026-09-11T08:12:44.310Z"
    }
  }
  ```
</ResponseExample>

The three `data` keys always arrive together: `user` carries the identity fields above plus the caller's per-user preferences (softphone layout, voicemail-to-email opt-in, inbox signature), `tenant` is the organization context every subsequent request runs in, and `organizations` lists every membership with the caller's `role` in each.

### Preflight: decide on the caller's role

<Note>
  Branch on `data.user.role` / membership `role` before calling a write path
</Note>

The identity probe answers three startup questions in one call: who is calling (`data.user`), which organization the key is bound to (`data.tenant.slug`), and whether the caller's role in that org justifies the call you are about to make (`data.organizations[].role`). A workspace-scoped key resolves to exactly one tenant; multi-org dashboard (`Clerk`-session) callers receive every membership in `data.organizations`. An admin-of-one-org membership appears alongside the rest — treat the per-org `role` as the decision input and the org-scoped endpoint's own verdict as authoritative.

<CodeGroup>
  ```typescript Node.js theme={null}
  const res = await fetch("https://api.orbit.devotel.io/api/v1/me", {
    headers: { "X-API-Key": process.env.ORBIT_API_KEY! },
  });
  const { data } = await res.json();

  // Branch on the caller's role before any write path.
  const membership = data.organizations.find((o) => o.slug === data.tenant.slug);
  const role = membership?.role ?? "member";
  if (role !== "admin" && role !== "owner") {
    console.warn(`Role "${role}" cannot manage this resource — expect 403 on write paths`);
  }
  ```

  ```python Python theme={null}
  import os, requests

  r = requests.get(
      "https://api.orbit.devotel.io/api/v1/me",
      headers={"X-API-Key": os.environ["ORBIT_API_KEY"]},
  )
  data = r.json()["data"]

  # Branch on the caller's role before any write path.
  membership = next(
      (o for o in data["organizations"] if o["slug"] == data["tenant"]["slug"]),
      None,
  )
  role = membership["role"] if membership else "member"
  if role not in ("admin", "owner"):
      print(f'Role "{role}" cannot manage this resource — expect 403 on write paths')
  ```

  ```csharp C# (escape hatch) theme={null}
  using System.Net.Http.Json;

  using var http = new HttpClient();
  http.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("ORBIT_API_KEY"));
  var me = await http.GetFromJsonAsync<JsonElement>("https://api.orbit.devotel.io/api/v1/me");
  var data = me.GetProperty("data");

  // Branch on the caller's role before any write path.
  var role = data.GetProperty("user").GetProperty("role").GetString() ?? "member";
  if (role is not ("admin" or "owner"))
  {
      Console.WriteLine($"Role \"{role}\" cannot manage this resource — expect 403 on write paths");
  }
  ```
</CodeGroup>

The decision table for the role you read:

| Caller state                                          | `GET /me`             | Role-less write that follows                          |
| ----------------------------------------------------- | --------------------- | ----------------------------------------------------- |
| Credential valid; role carries the write path's grant | `200` — full envelope | `200` / `201` — operation succeeds                    |
| Credential valid; role lacks the grant                | `200` — full envelope | `403 FORBIDDEN` — surface, rotating retry never heals |
| Credential missing or invalid                         | `401` — see below     | no downstream call is reached                         |

A `200` on `/me` tells you the credential is good — it never tells you a write will succeed. Read the role, decide, and treat the operation's own `403` as the authoritative verdict when the table disagrees with your role map.

## Errors worth branching on

This probe is where an invalid credential surfaces first — an integration that skips the bootstrap re-discovers the same `401` on every later call. The mechanics of the envelope (`error.code` / `message` / `status` / `details`, the `meta.docs_url` trail) are owned by the [error handling guide](/guides/error-handling-examples); only the /me-specific branch is hers.

### 401 — the credential itself fails

```json theme={null}
{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "The API key is missing, invalid, or has been revoked.",
    "status": 401
  },
  "meta": {
    "request_id": "req_me_auth_01",
    "timestamp": "2026-09-11T08:12:45.120Z",
    "docs_url": "https://docs.orbit.devotel.io/errors/INVALID_API_KEY"
  }
}
```

A `401` on this probe means the `X-API-Key` header value itself fails — it never means the identity payload moved. The failure precedes tenant resolution, so there is no `data` envelope to fall back to. Read `meta.docs_url` for the code's remedy anchor; rotate the key or mint a new one, because retrying the same header re-fails identically.

### Retry matrix

| Class                     | Meaning                                                                   | Branch response                                                                                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **401 `INVALID_API_KEY`** | the credential itself is missing, invalid, or revoked                     | **Surface.** Rotate or mint a key; retrying the same value re-fails identically. Follow `meta.docs_url` to the code's remedy.                                                                                         |
| **403 `FORBIDDEN`**       | the credential is valid but the caller's role lacks the operation's grant | **Surface on the operation, not here.** `/me` itself returns `200`; the write path you preflighted against the role table answers `403`. Fix the role or the scope — a retry with the same credential never heals it. |
| **429 `RATE_LIMITED`**    | the window quota is exhausted                                             | **Retry after `error.details.retry_after`** (or the `Retry-After` header) with the same request. Cache the `200` payload per session so a retry storm is unnecessary.                                                 |
| **5xx transient**         | a backing lookup failed                                                   | **Retry with backoff.** The startup probe is idempotent; serve your last cached envelope while you retry rather than failing the session.                                                                             |

The platform-wide retry-vs-terminal decision table lives in the [error handling guide](/guides/error-handling-examples).
