Skip to content

Manage users and access with the REST API#

The REST API user and group endpoints invite, modify, and remove team users and manage Access Groups. Use these endpoints to automate onboarding and offboarding, for example to invite new staff into the correct Access Groups or remove departing employees. To control access to a single computer, see Manage computers with the REST API.

Applies to#

  • A Jump Desktop for Teams team, and an API token from a team Admin. Admin (Read-Only) users and read-only tokens are restricted to GET requests. See Get started with the REST API for tokens, team IDs, and error responses.
  • Access Groups: the maximum number of groups depends on your plan. See Control access with Access Groups.
  • Samples use placeholder IDs T-EXAMPLE123 (team), U-EXAMPLE111 (user), G-EXAMPLE789 (Access Group), and D-EXAMPLE456 (computer), and read the token from JUMP_API_TOKEN.
  • Python samples use the requests package. JavaScript samples require Node.js 18 or later and use top-level await, so save them as .mjs files.

Find a user ID#

To look up a user by email, call GET /v1/team/{teamID}/users. The email filter matches the complete address, not a partial address. Without a filter, the endpoint returns every user on the team.

curl --fail-with-body -sS -G https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/users \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  --data-urlencode "email=ana@example.com"
import os

import requests

resp = requests.get(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/users",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    params={"email": "ana@example.com"},
)
if not resp.ok:
    raise SystemExit(f"{resp.status_code} {resp.text}")

users = resp.json()["users"]
print(users[0]["id"] if users else "No user with that email")
const url = new URL("https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/users");
url.searchParams.set("email", "ana@example.com");

const res = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.JUMP_API_TOKEN}` },
});
if (!res.ok) {
  console.error(res.status, await res.text());
  process.exit(1);
}

const { users } = await res.json();
console.log(users.length ? users[0].id : "No user with that email");

Invoke-RestMethod throws on a non-2xx response.

$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$uri = "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/users?email=ana@example.com"

try {
    $resp = Invoke-RestMethod -Uri $uri -Headers $headers
} catch {
    Write-Error "$($_.Exception.Response.StatusCode.value__) $($_.ErrorDetails.Message)"
    exit 1
}

if ($resp.users.Count -gt 0) { $resp.users[0].id } else { "No user with that email" }

An empty users array indicates no match. Each user includes id, email, role, remoteAccess, totpEnabled, and annotations.

The published OpenAPI specification lists role as admin or user; the service also returns admin-read-only for an Admin (Read-Only) user.

Invite users#

To create an invite, call POST /v1/team/{teamID}/invite/user.

Field Use
sendTo Recipient email address for the invite. Omit it to create a public invite link.
singleUse true for email invites. Requires sendTo.
name Invite name shown in the Teams dashboard. Up to 64 characters.
groups IDs of Access Groups the user joins on accepting the invite.
devices IDs of computers the user can connect to after accepting the invite.

The following sample emails a single-use invite that adds the user to one Access Group:

curl --fail-with-body -sS -X POST \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/invite/user \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sendTo": "ana@example.com", "singleUse": true, "groups": ["G-EXAMPLE789"]}'
import os

import requests

resp = requests.post(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/invite/user",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    json={"sendTo": "ana@example.com", "singleUse": True, "groups": ["G-EXAMPLE789"]},
)
resp.raise_for_status()
print(resp.json()["secret"])
const res = await fetch("https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/invite/user", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.JUMP_API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ sendTo: "ana@example.com", singleUse: true, groups: ["G-EXAMPLE789"] }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log((await res.json()).secret);
$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$body = @{ sendTo = "ana@example.com"; singleUse = $true; groups = @("G-EXAMPLE789") } | ConvertTo-Json

$invite = Invoke-RestMethod -Method Post -Uri "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/invite/user" `
    -Headers $headers -ContentType "application/json" -Body $body
$invite.secret

The response contains the invite, including its secret. New members join with the user role. See what invitees see.

Delete an invite#

Call DELETE /v1/team/{teamID}/invite/user/{secret} with the invite's secret, and the invite link stops working. To find pending invites and their secrets, see userInvites in the response from GET /v1/team/{teamID}.

curl --fail-with-body -sS -X DELETE \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/invite/user/INVITE-SECRET \
  -H "Authorization: Bearer $JUMP_API_TOKEN"

Change a user's role or remote access#

Send one or both fields to PATCH /v1/team/{teamID}/user/{userID}. Omitted fields keep their current values. The response contains the updated user.

Field Values
role admin, admin-read-only, or user
remoteAccess enabled or disabled

The published OpenAPI specification lists only admin and user; the service also accepts admin-read-only for Admin (Read-Only).

Turning off remote access disconnects the user's active sessions. For the meaning of roles and remote access, including their effect on billing, see Team roles and remote access.

curl --fail-with-body -sS -X PATCH \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111 \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"remoteAccess": "disabled"}'
import os

import requests

resp = requests.patch(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    json={"remoteAccess": "disabled"},
)
resp.raise_for_status()
print(resp.json()["remoteAccess"])
const res = await fetch("https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.JUMP_API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ remoteAccess: "disabled" }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log((await res.json()).remoteAccess);
$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$body = @{ remoteAccess = "disabled" } | ConvertTo-Json

$user = Invoke-RestMethod -Method Patch -Uri "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111" `
    -Headers $headers -ContentType "application/json" -Body $body
$user.remoteAccess

The request fails with one of the following codes:

  • team-requires-admin or team-requires-active-user if the change would leave the team without an Admin or without a user with remote access turned on.
  • active-users-exceed-licensed-quanity if turning remote access on would exceed the team's licensed users.

Remove a user#

Warning

Removing a user takes effect immediately. The user loses access to all of the team's computers, their active sessions are disconnected, and they are removed from the team's Access Groups. To suspend access without removing the user, turn off remote access.

Call DELETE /v1/team/{teamID}/user/{userID}. The user's Jump Desktop account and their membership in other teams are not affected. You cannot remove the team's last Admin or last user with remote access turned on.

curl --fail-with-body -sS -X DELETE \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111 \
  -H "Authorization: Bearer $JUMP_API_TOKEN"

Add SSO users without an invite#

If your team uses single sign-on, you can add SSO users directly to the team, without an invite or email.

Requirements:

  • The token belongs to an Admin who signs in with SSO and is an Admin of both this team and the team that owns the SSO connection.
  • The user you add signs in with the same SSO connection.

  • To list users who sign in with the same SSO connection, call GET /v1/team/{teamID}/saml/users. The response is an array of users with id, email, firstName, and lastName. Users provisioned with SCIM also have scimUserInfo.

  • To add a user, call POST /v1/team/{teamID}/users/{userID}. No request body is required.
curl --fail-with-body -sS -X POST \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/users/U-EXAMPLE111 \
  -H "Authorization: Bearer $JUMP_API_TOKEN"

The user joins with the user role and remote access turned on. The request returns 409 if the user is already on the team, and active-users-exceed-licensed-quanity if the team has no remaining licensed users.

Check a user's sign-in security#

GET /v1/team/{teamID}/user/{userID}/info returns:

Field Contains
totp.enabled Whether 2-factor authentication is turned on.
authProviders The user's sign-in methods in provider. Provider email addresses and avatars are masked.
tokenLog The user's sign-in tokens, with title, scopes, lastUsed, lastUsedIPAddress, and expiresAt.

For profile details, see Find a user ID.

Ask a user to turn off 2-factor authentication#

This feature requires the Jump Desktop for Teams Enterprise plan. If a user loses their 2-factor authentication device, call POST /v1/team/{teamID}/user/{userID}/removetotp. The request does not turn off 2-factor authentication itself:

  1. Jump Desktop emails the user a link to turn off 2-factor authentication on their account.
  2. The user clicks the link within 24 hours. Until the user clicks the link, nothing changes.
  3. When they confirm, 2-factor authentication is turned off for their Jump Desktop account, not only for your team.

The request fails with totp-not-enabled if the user does not have 2-factor authentication turned on or does not sign in with a Jump Desktop email and password (for example, SSO-only users).

curl --fail-with-body -sS -X POST \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111/removetotp \
  -H "Authorization: Bearer $JUMP_API_TOKEN"

To confirm the change after the user clicks the link, check totp.enabled with Check a user's sign-in security.

Manage Access Groups#

An Access Group is a set of users and computers that grants its users remote access to its computers. See Control access with Access Groups.

To list groups and their IDs, call GET /v1/team/{teamID}. The groups object is keyed by group ID, and each group has name, users, devices, and scimManaged.

Create, rename, or delete a group#

Task Request Body
Create POST /v1/team/{teamID}/group {"name": "..."}, 1 to 64 characters
Rename PATCH /v1/team/{teamID}/group/{groupID} {"name": "..."}, up to 64 characters
Delete DELETE /v1/team/{teamID}/group/{groupID} None

Creating a group returns the group with its new id. The request fails with team-too-many-groups when the team has reached its plan's group limit. Deleting a group revokes the access it granted; its users and computers remain on the team.

curl --fail-with-body -sS -X POST \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Finance"}'
import os

import requests

resp = requests.post(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    json={"name": "Finance"},
)
resp.raise_for_status()
print(resp.json()["id"])
const res = await fetch("https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.JUMP_API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "Finance" }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log((await res.json()).id);
$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$body = @{ name = "Finance" } | ConvertTo-Json

$group = Invoke-RestMethod -Method Post -Uri "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group" `
    -Headers $headers -ContentType "application/json" -Body $body
$group.id

Add or remove members of one group#

Task Request
Add POST /v1/team/{teamID}/group/{groupID}/members
Remove POST /v1/team/{teamID}/group/{groupID}/members/delete

Both accept users (user IDs) and devices (computer IDs), and either field can be omitted. Members not listed are not changed.

curl --fail-with-body -sS -X POST \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/G-EXAMPLE789/members \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"users": ["U-EXAMPLE111"], "devices": ["D-EXAMPLE456"]}'
import os

import requests

resp = requests.post(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/G-EXAMPLE789/members",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    json={"users": ["U-EXAMPLE111"], "devices": ["D-EXAMPLE456"]},
)
resp.raise_for_status()
const res = await fetch(
  "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/G-EXAMPLE789/members",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.JUMP_API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ users: ["U-EXAMPLE111"], devices: ["D-EXAMPLE456"] }),
  },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$body = @{ users = @("U-EXAMPLE111"); devices = @("D-EXAMPLE456") } | ConvertTo-Json

Invoke-RestMethod -Method Post -Uri "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/G-EXAMPLE789/members" `
    -Headers $headers -ContentType "application/json" -Body $body

Add or remove members across several groups#

Task Request
Add PATCH /v1/team/{teamID}/group/members
Remove PATCH /v1/team/{teamID}/group/members/remove

Both accept groups (group IDs), plus the users and devices to add to or remove from every listed group. The request returns 200 even if the change fails for some groups, so check the errors array in the response. Each error has groupID, code, and detail.

curl --fail-with-body -sS -X PATCH \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/members \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"groups": ["G-EXAMPLE789", "G-EXAMPLE790"], "users": ["U-EXAMPLE111"]}'
import os

import requests

resp = requests.patch(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/members",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    json={"groups": ["G-EXAMPLE789", "G-EXAMPLE790"], "users": ["U-EXAMPLE111"]},
)
resp.raise_for_status()
for error in resp.json().get("errors") or []:
    print(error["groupID"], error["code"], error["detail"])
const res = await fetch("https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/members", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.JUMP_API_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ groups: ["G-EXAMPLE789", "G-EXAMPLE790"], users: ["U-EXAMPLE111"] }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);

const { errors = [] } = await res.json();
for (const error of errors) console.error(error.groupID, error.code, error.detail);
$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$body = @{ groups = @("G-EXAMPLE789", "G-EXAMPLE790"); users = @("U-EXAMPLE111") } | ConvertTo-Json

$result = Invoke-RestMethod -Method Patch -Uri "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/group/members" `
    -Headers $headers -ContentType "application/json" -Body $body
$result.errors | ForEach-Object { "$($_.groupID) $($_.code) $($_.detail)" }

SCIM-managed groups#

Groups provisioned from your identity provider with SCIM have scimManaged set to true. The identity provider manages their name and users, so the REST API applies these restrictions:

  • Rejects renaming or deleting the group.
  • Rejects adding or removing users with the single-group endpoints. The bulk endpoints skip users for that group and report the error code group-scim-managed.
  • Allows adding and removing computers in a single-group request that lists no users. A single-group request listing any users is rejected entirely, including its computers.

Add labels with annotations#

Annotations are key-value pairs stored on users and Access Groups, for example an employee number or cost center. Keys are 1 to 64 characters; values are up to 2,048 characters.

Task User Access Group
Get GET .../user/{userID}/annotations GET .../group/{groupID}/annotations
Add or update POST .../user/{userID}/annotations POST .../group/{groupID}/annotations
Remove POST .../user/{userID}/annotations/delete POST .../group/{groupID}/annotations/delete

All paths start with /v1/team/{teamID}. Add or update takes {"annotations": [{"key": "...", "value": "..."}]} and overwrites the value of any key that already exists. Remove takes {"annotationKeys": ["..."]}.

In the Teams dashboard, a user's custom fields are annotations keyed by the field's title, so writing that key changes the field.

curl --fail-with-body -sS -X POST \
  https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111/annotations \
  -H "Authorization: Bearer $JUMP_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"annotations": [{"key": "Cost center", "value": "4410"}]}'
import os

import requests

resp = requests.post(
    "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111/annotations",
    headers={"Authorization": f"Bearer {os.environ['JUMP_API_TOKEN']}"},
    json={"annotations": [{"key": "Cost center", "value": "4410"}]},
)
resp.raise_for_status()
const res = await fetch(
  "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111/annotations",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.JUMP_API_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ annotations: [{ key: "Cost center", value: "4410" }] }),
  },
);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
$headers = @{ Authorization = "Bearer $env:JUMP_API_TOKEN" }
$body = @{ annotations = @(@{ key = "Cost center"; value = "4410" }) } | ConvertTo-Json -Depth 3

Invoke-RestMethod -Method Post -Uri "https://api.jumpdesktop.com/v1/team/T-EXAMPLE123/user/U-EXAMPLE111/annotations" `
    -Headers $headers -ContentType "application/json" -Body $body

Requests over the annotation limit fail with team-too-many-annotations.