> This page is for Developers.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.iot.telia.lt/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.iot.telia.lt/_mcp/server.

# Migrate to the v2 application token API

> What changes between the v1 and v2 application token endpoints, with before and after requests for each breaking change.

The v1 application token endpoints are deprecated.
They keep working until they're removed, and every new application token feature, such as assigning a role to a token, is available in v2 only.

> **Warning**
>
> The v1 endpoints for managing application tokens (`/api/v1/application_token`) stop working on June 1, 2027.
> Move your integration to v2 before then.

This guide lists what you have to change in your integration to move from v1 to v2.

> **Note**
>
> Your existing application tokens keep working.
> You don't have to recreate them, and authenticating with a token through `POST /api/v1/authenticate` doesn't change.
> The migration only affects code that creates, lists, updates, or deletes application tokens.

## Endpoint mapping

| Task                | v1                                                   | v2                                                                                                           |
| :------------------ | :--------------------------------------------------- | :----------------------------------------------------------------------------------------------------------- |
| List tokens         | `GET /api/v1/application_token`                      | [`GET /api/v2/application_token`](/developers/api/application-tokens/list-application-tokens)                |
| Get a token         | Not available                                        | [`GET /api/v2/application_token/{id}`](/developers/api/application-tokens/get-application-token)             |
| List token statuses | Not available                                        | [`GET /api/v2/application_token/status`](/developers/api/application-tokens/list-application-token-statuses) |
| Create a token      | `POST /api/v1/application_token`                     | [`POST /api/v2/application_token`](/developers/api/application-tokens/create-application-token)              |
| Rename a token      | `PATCH /api/v1/application_token/{id}`               | [`PATCH /api/v2/application_token/{id}`](/developers/api/application-tokens/update-application-token)        |
| Revoke a token      | `PATCH /api/v1/application_token/{id}` with a status | [`DELETE /api/v2/application_token/{id}`](/developers/api/application-tokens/delete-application-token)       |

## Summary of breaking changes

| Change                                                                       | What breaks if you don't update                                              |
| :--------------------------------------------------------------------------- | :--------------------------------------------------------------------------- |
| [Status IDs change](#status-ids-change)                                      | Code that compares `status.id` misreads every token                          |
| [Revoking moves to `DELETE`](#revoke-tokens-with-a-delete-request)           | Revoke requests fail or leave the token active                               |
| [`expiry_date` is required](#set-an-expiry-date-on-every-new-token)          | Creating a token without an expiry date fails                                |
| [`roles` is required](#assign-a-role-to-every-new-token)                     | Creating a token without a role fails                                        |
| [Stricter token names](#token-names-are-validated)                           | Names longer than 64 characters or with other characters are rejected        |
| [IP restrictions are an array](#ip-restrictions-are-an-array)                | A token created with `ip` instead of `ip_restrictions` has no IP restriction |
| [Lists return active tokens only](#lists-return-active-tokens-only)          | Revoked tokens are missing from the list                                     |
| [300 active tokens per Workspace](#limit-of-300-active-tokens-per-workspace) | Creating a token beyond the limit fails                                      |
| [Renamed response fields and new status codes](#response-and-error-changes)  | Response parsing and status code checks fail                                 |
| [New timestamp format](#renamed-fields)                                      | Date parsing with a fixed pattern fails                                      |

## Status IDs change

v2 uses different IDs for the same two states.

| State   | v1                   | v2                 |
| :------ | :------------------- | :----------------- |
| Usable  | `0` with `Activated` | `1` with `ACTIVE`  |
| Revoked | `1` with `Revoked`   | `3` with `DELETED` |

A token is never in any other status.
`GET /api/v2/application_token/status` returns both statuses.

Update any code that reads `status.id`.
In v1, a `status.id` of `1` means the token is revoked.
In v2, it means the token is active.

v1:

```json
"status": { "id": 0, "description": "Activated" }
```

v2:

```json
"status": { "id": 1, "description": "ACTIVE" }
```

## Revoke tokens with a delete request

In v1, you revoke a token by setting its status in a `PATCH` request.
In v2, `PATCH` no longer accepts a status.
Send a `DELETE` request instead.

v1:

```http
PATCH /api/v1/application_token/1234
Content-Type: application/json

{
  "status": { "id": 1 }
}
```

v2:

```http
DELETE /api/v2/application_token/1234
```

A successful delete returns `204 No Content`.
The application token can no longer be used to authenticate.
An `auth_token` already generated from it stays valid until it expires, up to 240 minutes later.

> **Warning**
>
> v2 ignores a `status` field in a `PATCH` body instead of rejecting it.
> A body that contains only `status` fails with `400 Bad Request`.
> A body that also contains a `name` renames the token and leaves it active.

## Set an expiry date on every new token

In v1, `expiry_date` is optional, and a token without one never expires.
In v2, `expiry_date` is required.
It must be at least one day and at most 10 years in the future.

Send an ISO 8601 date, such as `2027-10-01T00:00:00Z` or `2027-10-01 00:00:00`.
A value without a time zone is read as UTC.

If you create tokens without an expiry date today, decide how long each token should live, and plan to create a replacement before it expires.
For a rotation procedure, see [Handle token expiration](/developers/auth/application-tokens#handle-token-expiration).

v1, a token that never expires:

```http
POST /api/v1/application_token
Content-Type: application/json

{
  "description": "Customer 4711 integration"
}
```

v2, a token that expires after one year:

```http
POST /api/v2/application_token
Content-Type: application/json

{
  "name": "Customer 4711 integration",
  "roles": [{ "id": 1 }],
  "expiry_date": "2027-10-01T00:00:00Z"
}
```

A missing, past, or out-of-range expiry date returns `400 Bad Request`:

```json
{
  "status_code": 400,
  "message": "expiry_date must be no more than 10 years in the future"
}
```

After you create a token, you can't change its expiry date.

## Assign a role to every new token

v1 tokens always have Administrator permissions.
In v2, you choose the role on creation, and `roles` is required.
Pass exactly one role in the `roles` array.

| Role          | ID  |
| :------------ | :-- |
| Administrator | `1` |
| User          | `2` |
| Observer      | `3` |

Custom roles from your Workspace work too.
The Support role can't be assigned to a token.

To keep the permissions your v1 tokens have, assign the Administrator role:

```json
"roles": [{ "id": 1 }]
```

For least privilege, assign the role with the fewest permissions your integration needs.
To change the role of an existing token, send `roles` in a `PATCH` request.

## Token names are validated

In v1, the token label is called `description` and accepts any character.
v1 accepts up to 256 characters but keeps only the first 64.
In v2, it's called `name`, and it must be 2 to 64 characters long after surrounding spaces are removed.
Only these characters are allowed:

* ASCII letters (`a` to `z` and `A` to `Z`) and digits
* Spaces
* `-` `_` `.` `:` `/` `(` `)` `[` `]` `@` `+` `#`

Accented and non-Latin letters, such as `é` or `ü`, aren't allowed.
A name that doesn't match returns `400 Bad Request`.
If you generate token names, check your naming pattern against these rules before you switch.

v1:

```json
{ "description": "Integration for customer ACME Corporation, EU region, production environment, created by provisioning service" }
```

v2:

```json
{ "name": "ACME Corp EU prod (provisioning)" }
```

## IP restrictions are an array

In v1, a token accepts one IP restriction, sent and returned as the string `ip`.
In v2, you send `ip_restrictions` as an array of up to 100 IPv4 addresses or CIDR ranges.
An address without a mask is stored as `/32`.

> **Warning**
>
> v2 ignores an `ip` field instead of rejecting it.
> A create request that still sends `ip` succeeds, and the new token has no IP restriction.

v1:

```json
{ "ip": "10.203.23.78/32" }
```

v2 request:

```json
{ "ip_restrictions": ["10.203.23.78", "10.10.1.0/24"] }
```

v2 response:

```json
"ip_restrictions": [
  { "id": 42, "ip_address": "10.203.23.78/32" },
  { "id": 43, "ip_address": "10.10.1.0/24" }
]
```

A token without IP restrictions has no `ip_restrictions` field in the response.
You can't change the IP restrictions of an existing token.

## Lists return active tokens only

`GET /api/v1/application_token` returns every token, including revoked ones.
`GET /api/v2/application_token` returns active tokens only.

To include deleted tokens, filter by status with the `q` query parameter.
Separate several statuses with `||`, and percent-encode the pipes as `%7C%7C`.

| To list            | Request                                           |
| :----------------- | :------------------------------------------------ |
| Active tokens      | `GET /api/v2/application_token`                   |
| Deleted tokens     | `GET /api/v2/application_token?q=status:3`        |
| Active and deleted | `GET /api/v2/application_token?q=status:1%7C%7C3` |

`GET /api/v2/application_token/{id}` returns `404 Not Found` for a deleted token.

## Limit of 300 active tokens per Workspace

By default, a Workspace can hold up to 300 active tokens.
Deleted tokens don't count toward the limit.
The v2 create endpoint enforces the limit, and counts every active token in the Workspace, including tokens created with v1.
Creating a token beyond the limit returns `422 Unprocessable Entity`:

```json
{
  "status_code": 422,
  "message": "Cannot create application token: the organisation already has 300 active tokens."
}
```

If your integration needs more active tokens in one Workspace, contact your Telia account manager before you migrate.

## Response and error changes

### Renamed fields

| v1                                   | v2                                              |
| :----------------------------------- | :---------------------------------------------- |
| `description`                        | `name`                                          |
| `created`                            | `created_date`                                  |
| `ip` (string)                        | `ip_restrictions` (array of objects)            |
| `creator` (`id`, `name`, `username`) | `created_by` (`id`, `name`, `username`, `type`) |

v2 also returns `roles`, `last_used_date`, `last_used_ip`, `last_modified_date`, `last_modified_by`, `deleted_date`, and `deleted_by`.
Fields without a value are left out of the response, except `name`, which is `null` for a token created through v1 without a description.

Timestamps, including `expiry_date`, change format from `2027-10-01T00:00:00+0000` to `2027-10-01T00:00:00.000Z`.
If you parse dates with a fixed pattern, update it.

### Create response

v1 returns `200 OK` with the token only:

```json
{
  "application_token": "eyJhbGciOiJIUzUxMiJ9..."
}
```

v2 returns `201 Created` with the full token details, and the secret in `token`:

```json
{
  "id": 1234,
  "name": "Customer 4711 integration",
  "status": { "id": 1, "description": "ACTIVE" },
  "roles": [{ "id": 1, "name": "Administrator" }],
  "expiry_date": "2027-10-01T00:00:00.000Z",
  "created_date": "2026-10-01T09:00:00.000Z",
  "created_by": {
    "id": 10,
    "username": "jane.doe@example.com",
    "name": "Jane Doe",
    "type": { "id": 0, "description": "HUMAN" }
  },
  "token": "eyJhbGciOiJIUzUxMiJ9..."
}
```

Read the token from `token` instead of `application_token`.
As in v1, this is the only response that contains it, so store it immediately.

### Update response

v1 returns `204 No Content`.
v2 returns `200 OK` with the updated token details.
A `PATCH` body must contain `name`, `roles`, or both.

### Errors

v1 validation errors return `422 Unprocessable Entity` with `error_code`, `error_token`, and `errors`.
v2 validation errors return `400 Bad Request` with `status_code` and `message`:

```json
{
  "status_code": 400,
  "message": "name may contain letters, digits, spaces and - _ . : / ( ) [ ] @ + # only, name must be longer than or equal to 2 characters"
}
```

When a request fails several checks, `message` lists every failed check, separated by commas.

If your code branches on `error_code` or `error_token`, branch on the HTTP status code instead.

## Migration checklist

* Replace v1 URLs with the v2 endpoints from the [endpoint mapping](#endpoint-mapping).
* Map status IDs: `0` to `1`, and `1` to `3`.
* Revoke tokens with `DELETE` instead of `PATCH`.
* Send `name`, `roles`, and `expiry_date` on every create request.
* Check generated token names against the new rules.
* Send and read IP restrictions as arrays.
* Add `q=status:1%7C%7C3` where your code expects revoked tokens in the list next to active ones.
* Read the new token from `token` in a `201 Created` response.
* Update response parsing for the renamed fields, timestamp format, and error format.
* Plan token rotation for the expiry dates you choose.