# Instance & Users

> Source: https://agilitycms.com/docs/javascript/management-sdk/instance

The Management SDK exposes instance-level operations: the users who have access to an instance, the locales configured on it, the currently authenticated user, and whether your recent changes have reached the Fetch API yet.

Instance users are managed through the `instanceUserMethods` group, available in both SDKs. Locales, the current user, and Fetch API status are **JavaScript-only** in the SDK — but every one of them has a REST endpoint, so nothing is out of reach from .NET. Each section below gives the endpoint.

Every method takes the instance `guid` so a single authenticated client can operate across multiple instances.

## Instance users

Access via `apiClient.instanceUserMethods` (JavaScript) or `client.instanceUserMethods` (.NET).

| Operation | JavaScript | .NET |
| --- | --- | --- |
| List all users on the instance | `getUsers(guid)` | `GetUsers(guid)` |
| Create a user or update their roles | `saveUser(...)` | `SaveUser(...)` |
| Remove a user from the instance | `deleteUser(userID, guid)` | `DeleteUser(userID, guid)` |

### Get users

Retrieve all users who have access to the instance.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const users = await apiClient.instanceUserMethods.getUsers(guid);
console.log(users);
```

```csharp
var users = await client.instanceUserMethods.GetUsers(guid);

foreach (var user in users)
{
    Console.WriteLine($"{user.EmailAddress} - {user.FirstName} {user.LastName}");
}
```

</div>

.NET signature: `Task<List<WebsiteUser?>> GetUsers(string guid)`

### Save user

Create a user or update an existing one. If the email address already exists on the instance, the user's roles are updated rather than a new user being created.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const savedUser = await apiClient.instanceUserMethods.saveUser(
  'user@example.com',
  [/* InstanceRole[] */],
  guid,
  'First',
  'Last'
);
```

```csharp
using agility.models;

var roles = new List<InstanceRole>
{
    new InstanceRole { RoleName = "Editor" }
};

var savedUser = await client.instanceUserMethods.SaveUser(
    emailAddress: "user@example.com",
    roles: roles,
    guid: guid,
    firstName: "Jane",
    lastName: "Doe"
);

Console.WriteLine($"Saved user ID: {savedUser?.UserID}");
```

</div>

.NET signature: `Task<InstanceUser?> SaveUser(string? emailAddress, List<InstanceRole> roles, string guid, string? firstName = null, string? lastName = null)`

| Parameter | Type | Description |
| --- | --- | --- |
| `emailAddress` | string | The user's email address. Used as the match key for existing users. |
| `roles` | `InstanceRole[]` / `List<InstanceRole>` | The roles to assign on this instance, e.g. `Editor`. |
| `guid` | string | The instance GUID. |
| `firstName` | string, optional | The user's first name. |
| `lastName` | string, optional | The user's last name. |

### Delete user

Remove a user from the instance by their user ID.

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const deletedUser = await apiClient.instanceUserMethods.deleteUser(1234, guid);
```

```csharp
var result = await client.instanceUserMethods.DeleteUser(userId, guid);
Console.WriteLine($"Delete result: {result}");
```

</div>

.NET signature: `Task<string?> DeleteUser(int? userID, string guid)`

---

## Locales

The SDK gives you a read; the API gives you the full set of locale operations.

```ts
// Get the locales configured for an instance
const locales = await apiClient.instanceMethods.getLocales(guid);
console.log(locales);
```

> JavaScript SDK only. From .NET, call `GET /api/v1/instance/{guid}/locales`.

Each locale looks like this:

| Field | Type | Description |
| --- | --- | --- |
| `localeID` | number | Internal identifier, used by the enable/disable endpoints. |
| `localeName` | string | Display name, e.g. `English (United States)`. |
| `localeCode` | string | The code you pass as `locale` everywhere else, e.g. `en-us`. |
| `enabled` | boolean | Whether content can be authored in this locale. |
| `custom` | boolean | Whether it's a custom locale rather than a standard one. |
| `sortOrder` | number | Position in the CMS locale switcher. |

> The SDK types this return value loosely, so treat the result as the endpoint's own JSON rather than relying on the declared type.

### Managing locales over REST

There's no SDK wrapper for these yet — call them directly with the same bearer token:

| Operation | Endpoint |
| --- | --- |
| List locales | `GET /api/v1/instance/{guid}/locales` |
| List enabled **and** disabled locales | `GET /api/v1/instance/{guid}/locales/all` |
| Get one locale by ID | `GET /api/v1/instance/{guid}/locales/{localeId}` |
| Get one locale by code | `GET /api/v1/instance/{guid}/locales/code/{localeCode}` |
| Add a locale | `POST /api/v1/instance/{guid}/locales` |
| Reorder locales | `POST /api/v1/instance/{guid}/locales/sort-order` |
| Enable a locale | `PATCH /api/v1/instance/{guid}/locales/{localeId}/enable` |
| Disable a locale | `PATCH /api/v1/instance/{guid}/locales/{localeId}/disable` |

`/locales/all` returns two arrays, `enabledLocales` and `disabledLocales`, which is the call you want when building a locale-management UI.

> **There is no delete.** Locales are enabled and disabled, never removed — which is the right design, since deleting one would orphan every piece of content authored in it. Disabling hides a locale from authoring without touching the content already there.

```ts
async function enableLocale(guid: string, localeCode: string, token: string) {
  const base = 'https://mgmt.aglty.io/api/v1/instance';
  const headers = { Authorization: `Bearer ${token}` };

  const all = await fetch(`${base}/${guid}/locales/all`, { headers }).then(r => r.json());
  const match = all.disabledLocales?.find(l => l.localeCode === localeCode);

  if (!match) {
    console.log(`${localeCode} is not in the disabled list — already enabled, or not added yet`);
    return;
  }

  await fetch(`${base}/${guid}/locales/${match.localeID}/enable`, {
    method: 'PATCH',
    headers,
  });
}
```

---

## Current user

Get the currently authenticated user along with all of their instance access details — useful for confirming which instances a token can actually reach.

```ts
const user = await apiClient.serverUserMethods.me(guid);
```

> JavaScript SDK only. From .NET, call `GET /api/v1/users/me`.

> **Pass your real `guid`, even though the endpoint isn't instance-scoped.** The SDK uses the GUID's regional suffix to choose which API host to call. Hand it an empty string and it falls back to the default region, which returns nothing useful if your instance lives in Canada, Europe, or Australia.

---

## Fetch API sync status

This one solves a real timing problem. The Management API writes to the CMS, but content is served from the Fetch API's CDN, and there's a propagation delay between the two. If a deploy script publishes content and then immediately rebuilds a site, it can build against content that hasn't landed yet.

`getFetchApiStatus` tells you where that sync is:

```ts
// mode: 'fetch' for the live CDN, 'preview' for the preview CDN
const status = await apiClient.instanceMethods.getFetchApiStatus(guid, 'fetch');

if (status.inProgress) {
  console.log(`Sync running since ${status.startTime}, ${status.itemsAffected} items affected`);
} else {
  console.log(`Last sync completed ${status.completionTime}`);
}
```

**Signature:** `getFetchApiStatus(guid: string, mode?: FetchApiSyncMode, waitForCompletion?: boolean): Promise<FetchApiStatus>`

Pass `waitForCompletion: true` to have the SDK poll until the sync finishes rather than returning the current snapshot — the polling interval and attempt cap come from the `duration` and `retryCount` fields on `Options`.

| Field | Description |
| --- | --- |
| `inProgress` | Whether a sync is running right now. |
| `itemsAffected` | Number of items in the sync. |
| `startTime` / `completionTime` | When the sync started and finished. |
| `errorMessage` | Set if the sync failed. |
| `pushType` | `1` = Preview, `2` = Fetch/Live. |
| `lastContentVersionID` | Last content version synced — compare against a save's version to confirm it landed. |
| `lastDeletedContentVersionID` | Last deleted content version synced. |
| `lastDeletedPageVersionID` | Last deleted page version synced. |
| `maxChangeDate` | Most recent change date processed. |
| `maxContentModelDate` | Most recent model change processed. |
| `leaseID` | Lease identifier for the sync operation. |
| `websiteName` | The instance's website name. |
| `timestamp` | When this status record was written. |

> JavaScript SDK only. From .NET, call `GET /api/v1/instance/{guid}/fetch-api-status?mode=fetch` (`mode` accepts `fetch` or `preview`).

**Wait for a publish to go live before rebuilding:**

```ts
async function waitForSync(guid: string, timeoutMs = 120_000) {
  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const status = await apiClient.instanceMethods.getFetchApiStatus(guid, 'fetch');

    if (status.errorMessage) {
      throw new Error(`Fetch API sync failed: ${status.errorMessage}`);
    }
    if (!status.inProgress) {
      return status;
    }

    await new Promise(r => setTimeout(r, 3000));
  }

  throw new Error('Timed out waiting for the Fetch API sync');
}
```

---

## Retrieving API keys programmatically

The Fetch and Preview API keys are visible in the Agility UI, but you can also read them with an authenticated Management API call — handy when provisioning environments or seeding CI secrets rather than copying keys by hand.

| Key | Endpoint |
| --- | --- |
| Fetch (live content) | `GET /oauth/getfetchkey?guid={guid}` |
| Preview (staged content) | `GET /oauth/getpreviewkey?guid={guid}` |

Both return the key as a plain string and both require a valid Management API bearer token.

```ts
async function getApiKeys(guid: string, token: string) {
  const headers = { Authorization: `Bearer ${token}` };

  const [fetchKey, previewKey] = await Promise.all([
    fetch(`https://mgmt.aglty.io/oauth/getfetchkey?guid=${guid}`, { headers }).then(r => r.text()),
    fetch(`https://mgmt.aglty.io/oauth/getpreviewkey?guid=${guid}`, { headers }).then(r => r.text()),
  ]);

  return { fetchKey, previewKey };
}
```

> **These are secrets.** The preview key in particular exposes unpublished content. Write them straight into your secret store — never into logs, build output, or anything client-side.

---

## Enum values reference

The Management API's numeric enums — the ones behind `properties.state`, workflow operations, container types — are published at a single endpoint:

```
GET https://mgmt.aglty.io/api/v1/types
```

It needs **no authentication and no instance GUID**, so you can open it in a browser. Each entry gives a `name`, its numeric `value`, and an optional `description`. When a value in this documentation disagrees with that endpoint, the endpoint is right.

The ones you'll reach for most:

**`itemStates`** — what `properties.state` means on a content item or page:

| Name | Value |
| --- | --- |
| `New` | `-1` |
| `None` | `0` |
| `Staging` | `1` |
| `Published` | `2` |
| `Deleted` | `3` |
| `Approved` | `4` |
| `AwaitingApproval` | `5` |
| `Declined` | `6` |
| `Unpublished` | `7` |

**`workflowOperationTypes`** — the `operation` argument for batch workflow calls:

| Name | Value |
| --- | --- |
| `Publish` | `1` |
| `Unpublish` | `2` |
| `Approve` | `3` |
| `Decline` | `4` |
| `RequestApproval` | `5` |

**`contentModelTypes`** — the model type on a container:

| Name | Value |
| --- | --- |
| `Item` | `0` |
| `List` | `1` |
| `Module` | `2` |

**`contentViewTypes`** — the `contentType` filter for paged container listings:

| Name | Value |
| --- | --- |
| `All` | `0` |
| `Shared` | `1` |
| `Linked` | `2` |
| `DynamicPageList` | `3` |

**`batchStates`** — progress of a batch operation:

| Name | Value |
| --- | --- |
| `None` | `0` |
| `Pending` | `1` |
| `InProcess` | `2` |
| `Processed` | `3` |
| `Deleted` | `4` |

The same response also carries `modes`, `pageTypes`, `pageItemTemplateTypes`, `batchItemTypes`, `batchOperationTypes`, `instancePermissions`, `userTypes`, `assetGroupingTypes`, `notificationTypes`, `changeItemTypes`, and `changeTypes`. Rather than hard-coding any of these, read them once at startup:

```ts
const types = await fetch('https://mgmt.aglty.io/api/v1/types').then(r => r.json());

const itemState = Object.fromEntries(
  types.itemStates.map((e: { name: string; value: number }) => [e.name, e.value])
);

console.log(itemState.Published); // 2
```

---

## Error handling

In the .NET SDK, all `instanceUserMethods` calls throw `ApplicationException` on failure.

```csharp
try
{
    var users = await client.instanceUserMethods.GetUsers(guid);
}
catch (ApplicationException ex)
{
    Console.Error.WriteLine($"Failed: {ex.Message}");
}
```
