# Getting Started with the Management SDK

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

Managing content from outside the CMS has never been easier. The **Agility Management SDK** lets you create and update content, apply workflow actions, and manage pages, models, containers, and assets through the Agility Management API.

The SDK ships for **JavaScript/TypeScript** and **.NET**. This section documents both — use the tabs in each code sample to switch languages, and your choice is remembered as you read.

## Use cases

- Importing content from external systems
- Implementing custom approval workflows
- Keeping content in sync with other platforms
- Bulk updating or publishing content lists
- Managing pages programmatically

---

## Installation

<div class="code-tabs" data-tabs="JavaScript,.NET">

```bash
npm install @agility/management-sdk
```

```bash
dotnet add package Agility.Management.SDK --prerelease
```

</div>

> **Two things will trip you up on the .NET side.** The assembly is named `management.api.sdk.dll`, but the NuGet **package id** is `Agility.Management.SDK` — asking for `management.api.sdk` fails with a package-not-found error. And every published version is still a prerelease, so without `--prerelease` (or an explicit `--version`) NuGet reports that no installable version exists. Treat the .NET SDK as beta accordingly.

Both SDKs are open source:

- JavaScript/TypeScript — [agility-cms-management-sdk-typescript](https://github.com/agility/agility-cms-management-sdk-typescript)
- .NET — [agility-cms-management-sdk-dotnet](https://github.com/agility/agility-cms-management-sdk-dotnet) (built on .NET 6+ / RestSharp)

---

## Authentication

Every request needs an access token. The SDK supports two ways to get one: **OAuth 2.0** (for interactive apps) and **Personal Access Tokens** (for automation — see below).

### Step 1 — Start the authorization flow

Send the user to the authorize endpoint. Add the `offline_access` scope if you want a refresh token so the integration can run unattended:

```
GET https://mgmt.aglty.io/oauth/authorize
  ?response_type=code
  &redirect_uri=YOUR_REDIRECT_URI
  &state=YOUR_STATE
  &scope=openid profile email offline_access
```

### Step 2 — Exchange the code for a token

Your redirect URI receives an authorization code. Exchange it for an access token:

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
const response = await fetch("https://mgmt.aglty.io/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ code: "YOUR_AUTHORIZATION_CODE" }),
})

const { access_token, refresh_token, expires_in } = await response.json()
```

```csharp
// POST https://mgmt.aglty.io/oauth/token
// Content-Type: application/x-www-form-urlencoded
// code=YOUR_AUTHORIZATION_CODE
using var http = new HttpClient();
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["code"] = "YOUR_AUTHORIZATION_CODE"
});

var response = await http.PostAsync("https://mgmt.aglty.io/oauth/token", body);
var json = await response.Content.ReadAsStringAsync();
```

</div>

The response is the same shape either way: `access_token`, `token_type`, `expires_in`, and — when you asked for `offline_access` — `refresh_token`.

### Step 3 — Refresh an expired token

```
POST https://mgmt.aglty.io/oauth/refresh?refresh_token=YOUR_REFRESH_TOKEN
```

> **Token lifetimes.** Access tokens last **24 hours** — after that, requests return `401 Unauthorized`. Refresh tokens last **30 days**, and only exist if you authorized with the `offline_access` scope. Store both securely and never expose them in client-side code.

---

## Setting up the client

With a token in hand, initialize the client and make your first request:

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
import * as mgmtApi from "@agility/management-sdk"

// Initialize the Options class
const options = new mgmtApi.Options()
options.token = "<<your-access-token>>"

// Initialize the ApiClient
const apiClient = new mgmtApi.ApiClient(options)

const guid = "<<your-instance-guid>>"
const locale = "en-us"

// Get the content item with ID 22
const contentItem = await apiClient.contentMethods.getContentItem(22, guid, locale)
console.log(JSON.stringify(contentItem))
```

```csharp
using management.api.sdk;
using agility.models;

// Initialize Options with your access token
var options = new Options
{
    token = "<<your-access-token>>"
};

// Create the client instance
var client = new ClientInstance(options);

var guid = "<<your-instance-guid>>";
var locale = "en-us";

// Get the content item with ID 22
var contentItem = await client.contentMethods.GetContentItem(22, guid, locale);
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(contentItem));
```

</div>

Note that the .NET `using` statement is `management.api.sdk` — the assembly name — even though you installed the `Agility.Management.SDK` package.

### Options fields

| Field | Purpose |
| --- | --- |
| `token` | OAuth access token or PAT (**required**) |
| `baseUrl` | Override the API base URL (optional) |
| `refresh_token` | OAuth refresh token (optional) |
| `duration` | Retry polling interval in ms (default `3000`) |
| `retryCount` | Max retry attempts for batch polling (default `500`) |

These are SDK-side settings, not API parameters — you won't find them in the Management API spec.
`duration` and `retryCount` control the **batch polling** described in [How writes complete](#how-writes-complete) below.

### Regional endpoints

The SDK picks the right API host from your instance GUID's suffix — you normally don't set `baseUrl` yourself:

| GUID suffix | Region | Endpoint |
| --- | --- | --- |
| `-u` | US | `https://mgmt.aglty.io` |
| `-us2` | US 2 | `https://mgmt-usa2.aglty.io` |
| `-c` | Canada | `https://mgmt-ca.aglty.io` |
| `-e` | Europe | `https://mgmt-eu.aglty.io` |
| `-a` | Australia | `https://mgmt-aus.aglty.io` |
| `-d` | Dev | `https://mgmt-dev.aglty.io` |

Each host serves its own Swagger UI — append `/swagger` to try endpoints interactively against your own instance.

---

## Personal Access Tokens (PAT)

For automation, CI/CD pipelines, and server-side jobs where an OAuth redirect isn't practical, use a **Personal Access Token**.

### Generating a PAT

PATs are created through the Management API. Authenticate with OAuth first, then call `POST /api/v1/tokens/create` — via your region's Swagger UI (append `/swagger` to the endpoints in the table above), with this body:

```json
{
  "name": "my-automation-token",
  "expiryDate": "2028-01-01T00:00:00Z"
}
```

> **`name` is the only required field.** `expiryDate` is optional in the API spec, and the spec doesn't define what happens when you omit it — so always set an explicit future date rather than relying on unspecified behaviour.

A successful call returns `201` with the token in the response's `token` field. **That value is returned only once** — save it immediately, and note that the `notice` field carries an accompanying message about it. Alongside the usual `400`, `401`, and `403` errors, `429` is a documented response, so automation that creates tokens in bulk should back off and retry.

| Response field | What it tells you |
| --- | --- |
| `tokenID` | Identifier for the token — used by the token management endpoints |
| `name` | The name you supplied |
| `token` | The secret value, returned **only** on creation |
| `notice` | Message accompanying the newly created token |
| `expiryDate` | When the token expires |
| `createdDate` | When the token was created |
| `enabled` | Whether the token is currently active |
| `isExpired` | `true` once the expiry date has passed |
| `daysUntilExpiration` | Days of life left — useful for rotating tokens before they lapse |
| `lastUsedDate` | Last time the token authenticated a request |

Companion endpoints let you audit and rotate tokens: `GET /api/v1/tokens/list`, `GET /api/v1/tokens/{tokenId}`, `PUT /api/v1/tokens/{tokenId}/update`, and `DELETE /api/v1/tokens/{tokenId}/delete`.

### Using a PAT

Initialization is identical to OAuth — pass the PAT as `token`:

<div class="code-tabs" data-tabs="JavaScript,.NET">

```ts
import * as mgmtApi from "@agility/management-sdk"

const options = new mgmtApi.Options()
options.token = "<<your-personal-access-token>>"

const apiClient = new mgmtApi.ApiClient(options)
```

```csharp
var options = new Options
{
    token = "<<your-personal-access-token>>"
};

var client = new ClientInstance(options);
```

</div>

**PAT restrictions:** PATs cover content operations but **cannot** touch user management, token management, or admin-level endpoints — those return `403 Forbidden`.

---

## How writes complete

Two things about writing through this API surprise people. Both are worth knowing before your first
save, because each one looks like a bug when you hit it.

### Every write is queued, and returns a batch ID

The Management API does not perform a write during your request. It **queues a batch** and responds
with a **batch ID** — an integer that identifies the queued work, *not* the content ID and not a
result. The actual save happens moments later.

**The SDK hides this for you.** `saveContentItem`, `publishContent` and friends take the batch ID,
poll until the batch completes, and return the real result — which is why `Options` has `duration`
and `retryCount`: they are the polling interval and the attempt ceiling. If a large import times out
waiting, raise `retryCount` rather than assuming the write failed.

If you want the batch ID instead of waiting, pass `returnBatchId: true` (JavaScript only). You are
then responsible for polling it yourself:

```
GET /api/v1/instance/{guid}/batch/{batchID}
```

That URL is **not** locale-scoped — `/{guid}/batch/{id}`, with no `/{locale}` segment. The batch is
finished when `batchState` is `3`. A freshly created batch ID can return `404` for a moment before it
exists, so treat an early `404` as "not yet", not as failure.

> **Calling the REST API directly?** Then this is your problem, not the SDK's. `POST /item` gives you a
> batch ID and nothing else; read the item straight back and you will get the **old** value, which looks
> exactly like a write that silently did nothing. Poll the batch before you trust the result.

### A save lands in Staging, not on your live site

Saving does **not** publish. A save always writes to **Staging**, and if the item was already
Published it drops back to a Staging state — your live site keeps serving the previous version until
you publish explicitly.

So changing one field on a live item is always **two** operations:

```ts
// 1. save — the change now exists, in Staging
const contentID = await apiClient.contentMethods.saveContentItem(item, guid, locale)

// 2. publish — only now is it live
await apiClient.contentMethods.publishContent(contentID, guid, locale)
```

Skip the second step and the change is real, stored, and invisible to your site — the single most
common reason a write "didn't work". The same applies to pages, and to every bulk equivalent: see
[Content Items](/docs/javascript/management-sdk/content-items) and
[Pages](/docs/javascript/management-sdk/pages).

---

## SDK operations

The client exposes method groups for each area of the CMS. The JavaScript client has ten; .NET has the first seven.

| Method group | Covers | In .NET? |
| --- | --- | --- |
| `contentMethods` | Content items — CRUD, workflow, publishing | Yes |
| `containerMethods` | Containers (content lists) | Yes |
| `modelMethods` | Content & component models | Yes |
| `pageMethods` | Pages and page templates | Yes |
| `assetMethods` | Media — upload and manage assets | Yes |
| `instanceUserMethods` | Instance users and permissions | Yes |
| `batchMethods` | Batch status, and batch-level workflow actions | Partly — status only |
| `instanceMethods` | Locales and Fetch API sync status | No |
| `serverUserMethods` | The authenticated user | No |
| `webhookMethods` | Webhook CRUD | No |

---

## Feature differences between the SDKs

The .NET SDK trails the JavaScript SDK, and it's worth being precise about what that means: **every gap below is an SDK gap, not an API gap.** The endpoint exists in all cases, so from .NET you can call the Management API directly and lose nothing but the wrapper.

| JavaScript-only | Call this from .NET instead |
| --- | --- |
| `instanceMethods.getLocales()` | `GET /api/v1/instance/{guid}/locales` |
| `instanceMethods.getFetchApiStatus()` | `GET /api/v1/instance/{guid}/fetch-api-status?mode=fetch` |
| `serverUserMethods.me()` | `GET /api/v1/users/me` |
| `webhookMethods` (webhook CRUD) | `GET/POST /api/v1/instance/{guid}/webhook`, `GET/DELETE .../webhook/{id}` |
| `contentMethods.batchWorkflowContent()` | `POST /api/v1/instance/{guid}/{locale}/item/batch-workflow` |
| `contentMethods.getContentList()` (POST filtering) | `POST /api/v1/instance/{guid}/{locale}/list/{referenceName}` |
| `contentMethods.getContentHistory()` | `GET /api/v1/instance/{guid}/{locale}/item/{contentID}/history` |
| `contentMethods.getContentComments()` | `GET /api/v1/instance/{guid}/{locale}/item/{contentID}/comments` |
| `containerMethods.getContainerListPaged()` | `GET /api/v1/instance/{guid}/container/list/paged` |
| `pageMethods.getPageHistory()` | `GET /api/v1/instance/{guid}/{locale}/page/{id}/history` |
| `pageMethods.getPageComments()` | `GET /api/v1/instance/{guid}/{locale}/page/{id}/comments` |
| `pageMethods.batchWorkflowPages()` | `POST /api/v1/instance/{guid}/{locale}/page/batch-workflow` |
| `batchMethods.publishBatch()` and the other batch workflow actions | `POST /api/v1/instance/{guid}/batch/{id}/publish` (and `/unpublish`, `/approve`, `/decline`, `/request-approval`) |
| `assetMethods.deleteFolder()`, `assetMethods.renameFolder()` | The corresponding asset endpoints |

If you'd rather not hand-roll HTTP calls, the JavaScript SDK is the fuller surface today.

> **Keeping this list honest.** These tables are generated from the published packages — `@agility/management-sdk` on npm and `Agility.Management.SDK` on NuGet — rather than written from memory. If you spot a difference, the packages win; please tell us so we can correct the page.
