Automate personal access tokens
Manage the full lifecycle of personal access tokens (PATs) through the API: issue a narrowly scoped token with an expiry, rename or rescope it in place, rotate it without downtime, and revoke it when it is no longer needed. For how tokens, scope categories and access levels work, read Authentication first.
Before you begin
Section titled “Before you begin”- Token management is tenant admin only. The token you call these endpoints with must belong to a tenant admin.
- Every operation acts on the tokens of the requesting user: you can list, update and revoke only tokens you issued yourself.
- These operations are admin-only. The gateway works out your role from the token; if you get a
403, check that the token’s user is a tenant admin.
Scopes are expressed as grants, an array of {category, access} objects. category is one of inventory, security, protection, reporting, integrations or admin; access is read or write (write implies read). Set all_scopes: true instead for universal access.
Issue a scoped token with an expiry
Section titled “Issue a scoped token with an expiry”Create a token that can only read reporting data and expires at the end of the quarter. name is required; grants must be non-empty unless all_scopes is true. Omit expires_at for a token that never expires.
curl -X POST "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "bi-export-reporting-ro", "all_scopes": false, "grants": [{ "category": "reporting", "access": "read" }], "expires_at": "2026-12-31T23:59:59Z" }'import os, requests
BASE = "https://acme.app.elastio.com"HEADERS = {"Authorization": f"Bearer {os.environ['ELASTIO_TOKEN']}"}
body = { "name": "bi-export-reporting-ro", "all_scopes": False, "grants": [{"category": "reporting", "access": "read"}], "expires_at": "2026-12-31T23:59:59Z",}resp = requests.post(f"{BASE}/gateway/shared-services/v1/personal-access-tokens", headers=HEADERS, json=body, timeout=30)resp.raise_for_status()created = resp.json()new_token = created["token"] # shown only once - store it nowtoken_id = created["details"]["id"]$Base = "https://acme.app.elastio.com"$Headers = @{ Authorization = "Bearer $env:ELASTIO_TOKEN" }$Body = @{ name = "bi-export-reporting-ro" all_scopes = $false grants = @(@{ category = "reporting"; access = "read" }) expires_at = "2026-12-31T23:59:59Z"} | ConvertTo-Json -Depth 5
$Created = Invoke-RestMethod -Method Post -Uri "$Base/gateway/shared-services/v1/personal-access-tokens" ` -Headers $Headers -ContentType "application/json" -Body $Body$NewToken = $Created.token # shown only once - store it nowThe response is 201 with the raw token in token and the stored metadata in details:
{ "token": "<raw token value>", "details": { "id": "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44", "name": "bi-export-reporting-ro", "tenant_id": "a1b2c3d4-0000-4000-8000-000000000001", "user_id": "5e8f9a10-1111-4222-8333-444455556666", "all_scopes": false, "grants": [{ "category": "reporting", "access": "read" }], "expires_at": "2026-12-31T23:59:59Z", "created_at": "2026-10-12T09:00:00Z" }}List active tokens
Section titled “List active tokens”List the tokens you have issued, for example to find tokens nearing expiry or tokens that have never been used (last_used_at absent). The list supports offset pagination with limit and offset, and returns a total.
curl "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens?limit=50&offset=0" \ -H "Authorization: Bearer $ELASTIO_TOKEN"from datetime import datetime, timedelta, timezone
tokens, offset = [], 0while True: resp = requests.get(f"{BASE}/gateway/shared-services/v1/personal-access-tokens", headers=HEADERS, params={"limit": 50, "offset": offset}, timeout=30) resp.raise_for_status() page = resp.json() tokens += page["tokens_details"] offset += len(page["tokens_details"]) if not page["tokens_details"] or offset >= page["total"]: break
soon = datetime.now(timezone.utc) + timedelta(days=14)for t in tokens: exp = t.get("expires_at") if exp and datetime.fromisoformat(exp.replace("Z", "+00:00")) < soon: print(f"{t['name']} ({t['id']}) expires {exp}")$Page = Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/personal-access-tokens?limit=50&offset=0" -Headers $Headers$Page.tokens_details | Select-Object name, id, expires_at, last_used_at{ "tokens_details": [ { "id": "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44", "name": "bi-export-reporting-ro", "tenant_id": "a1b2c3d4-0000-4000-8000-000000000001", "user_id": "5e8f9a10-1111-4222-8333-444455556666", "all_scopes": false, "grants": [{ "category": "reporting", "access": "read" }], "expires_at": "2026-12-31T23:59:59Z", "last_used_at": "2026-10-12T10:15:02Z", "created_at": "2026-10-12T09:00:00Z", "updated_at": "2026-10-12T09:00:00Z" } ], "total": 1}Rename or rescope a token
Section titled “Rename or rescope a token”PATCH updates a token in place, so the secret value doesn’t change. Each field is optional:
- Send only
namefor a rename; omitting bothall_scopesandgrantsleaves permissions unchanged. - Send
grants(orall_scopes) to replace the token’s permissions. - Omitting
expires_atleaves the expiry unchanged.
/gateway/shared-services/v1/personal-access-tokens/{token_id}Update a personal access tokencurl -X PATCH "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens/3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "bi-export", "grants": [ { "category": "reporting", "access": "read" }, { "category": "inventory", "access": "read" } ] }'token_id = "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44"resp = requests.patch( f"{BASE}/gateway/shared-services/v1/personal-access-tokens/{token_id}", headers=HEADERS, json={ "name": "bi-export", "grants": [ {"category": "reporting", "access": "read"}, {"category": "inventory", "access": "read"}, ], }, timeout=30,)resp.raise_for_status()$TokenId = "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44"$Body = @{ name = "bi-export" grants = @( @{ category = "reporting"; access = "read" }, @{ category = "inventory"; access = "read" } )} | ConvertTo-Json -Depth 5Invoke-RestMethod -Method Patch -Uri "$Base/gateway/shared-services/v1/personal-access-tokens/$TokenId" ` -Headers $Headers -ContentType "application/json" -Body $BodyThe response is the updated token (no secret):
{ "id": "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44", "name": "bi-export", "tenant_id": "a1b2c3d4-0000-4000-8000-000000000001", "user_id": "5e8f9a10-1111-4222-8333-444455556666", "all_scopes": false, "grants": [ { "category": "reporting", "access": "read" }, { "category": "inventory", "access": "read" } ], "expires_at": "2026-12-31T23:59:59Z", "updated_at": "2026-10-12T11:02:40Z"}Rotate a token without downtime
Section titled “Rotate a token without downtime”The API has no single “rotate” call; rotation is issue, deploy, then revoke:
- Issue a new token with the same
grants(and a freshexpires_at) usingPOST /gateway/shared-services/v1/personal-access-tokens. Copy the old token’s grants from the list response. - Deploy the new
tokenvalue to your secret store and restart or reload the consumers. - Verify the switch: list tokens again and confirm the new token’s
last_used_atis set and the old token’slast_used_athas stopped advancing. - Revoke the old token by ID (next section).
# 1. Issue the replacement (grants copied from the old token)NEW=$(curl -s -X POST "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens" \ -H "Authorization: Bearer $ELASTIO_TOKEN" -H "Content-Type: application/json" \ -d '{"name":"bi-export-2026q4","grants":[{"category":"reporting","access":"read"}],"expires_at":"2027-03-31T23:59:59Z"}')echo "$NEW" | jq -r '.token' # 2. deploy this value, then...
# 4. ...revoke the old tokencurl -X POST "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens/revoke" \ -H "Authorization: Bearer $ELASTIO_TOKEN" -H "Content-Type: application/json" \ -d '{"token_ids":["3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44"]}'PAT = f"{BASE}/gateway/shared-services/v1/personal-access-tokens"
def rotate(old_id, deploy, new_expiry): old = next(t for t in requests.get(PAT, headers=HEADERS, timeout=30).json()["tokens_details"] if t["id"] == old_id) body = {"name": f"{old['name']}-rotated", "all_scopes": old["all_scopes"], "expires_at": new_expiry} if not old["all_scopes"]: body["grants"] = old["grants"] resp = requests.post(PAT, headers=HEADERS, json=body, timeout=30) resp.raise_for_status() created = resp.json()
deploy(created["token"]) # write to your secret store / restart consumers
requests.post(f"{PAT}/revoke", headers=HEADERS, json={"token_ids": [old_id]}, timeout=30).raise_for_status() return created["details"]["id"]$OldId = "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44"$Old = (Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/personal-access-tokens" -Headers $Headers).tokens_details | Where-Object id -eq $OldId
$Body = @{ name = "$($Old.name)-rotated"; grants = $Old.grants; expires_at = "2027-03-31T23:59:59Z" } | ConvertTo-Json -Depth 5$New = Invoke-RestMethod -Method Post -Uri "$Base/gateway/shared-services/v1/personal-access-tokens" ` -Headers $Headers -ContentType "application/json" -Body $Body# Deploy $New.token to your secret store here, then revoke the old token:Invoke-RestMethod -Method Post -Uri "$Base/gateway/shared-services/v1/personal-access-tokens/revoke" ` -Headers $Headers -ContentType "application/json" -Body (@{ token_ids = @($OldId) } | ConvertTo-Json)Revoke tokens and audit revocations
Section titled “Revoke tokens and audit revocations”Revoke one or more of your tokens by ID. The call returns 204 with no body. Revoked tokens appear in the revoked list, which has the same shape and limit/offset pagination as the active list.
curl -X POST "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens/revoke" \ -H "Authorization: Bearer $ELASTIO_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "token_ids": ["3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44"] }'resp = requests.post(f"{BASE}/gateway/shared-services/v1/personal-access-tokens/revoke", headers=HEADERS, json={"token_ids": ["3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44"]}, timeout=30)resp.raise_for_status() # 204 No Content$Body = @{ token_ids = @("3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44") } | ConvertTo-JsonInvoke-RestMethod -Method Post -Uri "$Base/gateway/shared-services/v1/personal-access-tokens/revoke" ` -Headers $Headers -ContentType "application/json" -Body $BodyTo confirm, list revoked tokens:
/gateway/shared-services/v1/personal-access-tokens/revokedList revoked personal access tokenscurl "https://acme.app.elastio.com/gateway/shared-services/v1/personal-access-tokens/revoked?limit=20" \ -H "Authorization: Bearer $ELASTIO_TOKEN"resp = requests.get(f"{BASE}/gateway/shared-services/v1/personal-access-tokens/revoked", headers=HEADERS, params={"limit": 20}, timeout=30)resp.raise_for_status()for t in resp.json()["tokens_details"]: print(t["name"], t["id"])(Invoke-RestMethod -Uri "$Base/gateway/shared-services/v1/personal-access-tokens/revoked?limit=20" -Headers $Headers).tokens_details | Select-Object name, id, last_used_at{ "tokens_details": [ { "id": "3f6c2a1e-8d4b-4c1a-9e2f-7b5d0a9c1e44", "name": "bi-export", "tenant_id": "a1b2c3d4-0000-4000-8000-000000000001", "user_id": "5e8f9a10-1111-4222-8333-444455556666", "all_scopes": false, "grants": [{ "category": "reporting", "access": "read" }], … } ], "total": 1}All endpoints
Section titled “All endpoints”Personal Access Tokens
5 operations in the reference →- GET
/gateway/shared-services/v1/personal-access-tokensList personal access tokens - POST
/gateway/shared-services/v1/personal-access-tokensIssue a personal access token - PATCH
/gateway/shared-services/v1/personal-access-tokens/{token_id}Update a personal access token - POST
/gateway/shared-services/v1/personal-access-tokens/revokeRevoke personal access tokens - GET
/gateway/shared-services/v1/personal-access-tokens/revokedList revoked personal access tokens