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

# CLI/SDK marker

> Why some operations succeed from the webapp but 403 from the SDK — and how the gate works.

## What it is

Every authenticated request to Ekso carries a **client marker** that distinguishes how the call arrived:

| Client | How the request arrived                                                            |
| ------ | ---------------------------------------------------------------------------------- |
| `Web`  | Browser session, webapp authorization-code flow. Default when no claim is present. |
| `Cli`  | CLI device-flow login (`ekso auth login`).                                         |
| `Sdk`  | API-key auth — every `ApiKeyAuth` call.                                            |

The backend exposes this as `UserContext.IsCliSdk` (true for `Cli` or `Sdk`; false for `Web`). Controllers can read it to gate operations that should not be scriptable.

## Why it exists

The webapp and the API are the same surface — there's one set of HTTP routes, used by both browsers and SDK clients. That's good for symmetry but creates a problem: some operations are safe in a UI but dangerous from a script.

Concrete example — **system field updates**. An admin in the webapp legitimately renames the "Priority" field's prompt or tweaks its description. The same request from a renegade script could brick a tenant's field setup. Same route, different threat surface.

The client marker lets the controller distinguish them: if the request arrived via SDK or CLI, apply the lockdown; otherwise let the webapp through.

## What's currently gated

| Operation                                                                  | Webapp | CLI / SDK                          |
| -------------------------------------------------------------------------- | ------ | ---------------------------------- |
| Create a custom field (any type)                                           | ✅      | ✅                                  |
| Update a custom field (`IsCore=false`)                                     | ✅      | ✅                                  |
| Update a system field's metadata (name, description, …)                    | ✅      | ❌ — 403                            |
| Update a system list field's data values (e.g. add `Critical` to Priority) | ✅      | ✅ — via `PUT /api/field/list/data` |

The list-data escape hatch is important: even an automation needs to be able to add `Critical` to `Priority` or new statuses to the workflow. That's a legitimate scripted operation and stays open. What's blocked is scripted *renaming* or deletion of system fields.

## How it gets stamped

Three points stamp the marker:

1. **Webapp authorization-code grant** — issues a JWT with no `EksoClient` claim. Validation maps absent → `Web`.
2. **CLI device-flow grant** — issues a JWT with `EksoClient=Cli`. The marker is preserved through token refreshes via `DataOAuthRefreshToken.ClientType`, so a refreshed CLI token doesn't silently downgrade to `Web`.
3. **API-key validation** — sets `IsCliSdk=true` unconditionally. API keys are by definition non-interactive, so `Sdk` is the right marker.

The flag lives on `UserContext.IsCliSdk` and is set per request by the auth middleware after token validation. Controllers never need to parse JWT claims directly.

## How to react in your code

If you're calling the SDK and you hit a 403 on a system-field update, you've found this gate:

```csharp theme={null}
try
{
    var systemField = await GetField("priority");  // IsCore=true
    systemField.Description = "Updated description";
    await client.Api.Field.List.PutAsync(systemField);
}
catch (EksoApiException ex) when (ex.StatusCode == 403)
{
    // Two options:
    // 1. Don't try this from CLI/SDK at all — change the field via the webapp.
    // 2. If you only need to change list values, use the dedicated endpoint:
    await client.Api.Field.List.Data.PutAsync(new UpdateFieldListDataRequest
    {
        Id = systemField.Id,
        Data = systemField.Data,           // mutated values
        DefaultValue = systemField.DefaultValue,
    });
}
```

The CLI surfaces this directly — `ekso field update-list <id>` 403s on system lists, but `ekso field update-list-data <id>` works for both system and custom lists.

## When to expect more gates

The CLI/SDK marker is a structural primitive — any operation we don't want scripted gets the same `if (helper.UserContext.IsCliSdk) ...` guard. Today only system fields are gated. Likely future additions:

* Bulk delete operations (already require `--confirm` flags; may also gate from SDK).
* Tenant-shape-changing operations (workspace renames, admin email changes).
* Anything where "someone left an API key in a Lambda" is a credible blast radius.

Each new gate will be called out in the operation's API Reference page when it lands.

## See also

* **[Authentication](/sdk/authentication)** — how `ApiKeyAuth` and `RefreshableBearerAuth` produce different markers.
* **[Error handling](/sdk/error-handling)** — handling 403s in your code.
