-
Notifications
You must be signed in to change notification settings - Fork 0
Groups API
Accessor: server.groups
Reference: https://docs.tak.gov/api/takserver#tag/groups-api
In TAK Server 5.x, "channels" are the classic file/LDAP auth groups exposed
to end users as a selectable subscription mechanism. A client "subscribes to
a channel" by changing its active group set — no new CoT semantics are
involved. The server routes broadcast CoT to a client only when the
sender's active IN groups intersect the receiver's active OUT
groups. Directions are from the group/channel's point of view: an IN
subscription lets you send into the channel (feed producers carry this -
an ADSB feeder holds the matching *_WRITE entitlement), an OUT
subscription means you receive the channel's traffic.
This is where most TAK API confusion happens, so let's sort it out first. There are three distinct scopes, served by three different endpoints:
| Scope | What it tells you | Where it lives |
|---|---|---|
| Entitlements | What a user may subscribe to. Managed via the User Account Management API (groupList, groupListIN, groupListOUT). |
Auth file / user database |
| Available channels | Which of those entitlements materialize as toggleable channels. | GET /Marti/api/groups/all |
| Active subscriptions | What the user is subscribed to right now. | GET /Marti/api/groups/user?username=... |
Note
The active flag in get_all_groups() does not reflect the
current subscription state — treat that endpoint as a discovery/catalog
listing only. For actual subscription state use get_active_groups(),
which reads the subscription endpoint.
Verified live on 5.7-RELEASE-43-HEAD (2026-08-25): querying the subscription endpoint for another user returns the persisted state instantly. Querying it with your own username returns your connection-bound state, which is empty for plain REST clients without a messaging session. If you wonder why your own subscriptions look empty although ATAK shows them fine — that's why.
The subscription helpers need to know whose subscriptions they are dealing with. Pass the username when constructing the server (for certificate auth that's the certificate's CN):
srv = Server("tak.example.com", "client.pem", "client.key", username="myuser")Alternatively, pass username="..." explicitly to each helper call.
Usage: await server.groups.get_all_groups(use_cache=False, send_latest_sa=False)
Returns all channels visible to the authenticated user. Admins see everything; regular users see their entitled channels.
API: GET /Marti/api/groups/all
| Parameter | Type | Description |
|---|---|---|
use_cache |
bool |
Use the server-side group cache. Default False. |
send_latest_sa |
bool |
Ask for latest SA data along with the group list. Default False. |
Returns: (status_code, data) — data is the unwrapped list of group
dicts: name, direction (IN/OUT), created, type, bitpos,
active.
Note
The list may contain duplicate names with different creation
dates — stale entries survive group edits, including variants padded with
leading spaces (" CHANNEL"). The helper functions collapse those for
you and always prefer the unpadded spelling.
Example:
status, groups = await srv.groups.get_all_groups()
for g in groups:
print(g["name"], g["direction"], g["active"])Usage: await server.groups.get_groups_for_user(username)
Returns a user's active channel subscriptions — this is the readback of
set_active_groups() and the source of truth the helpers use. Do not confuse
it with server.user.get_groups_for_user(), which returns raw entitlements.
API: GET /Marti/api/groups/user
| Parameter | Type | Description |
|---|---|---|
username |
str |
User to query |
Returns: (status_code, data) — list of {name, direction, created, type, bitpos, active} dicts. Empty when the user has no subscriptions.
Usage: await server.groups.get_group(name, direction)
Returns a single channel entry by name and direction.
API: GET /Marti/api/groups/{name}/{direction}
| Parameter | Type | Description |
|---|---|---|
name |
str |
Channel/group name |
direction |
str |
"IN" or "OUT" (case-insensitive) |
Returns: (status_code, data) — single group dict.
Usage: await server.groups.set_active_groups(groups, client_uid=None)
Sets the complete active subscription set of the authenticated user. Semantics are absolute, not incremental: whatever you omit becomes unsubscribed. Takes effect immediately on the user's CoT connection.
API: PUT /Marti/api/groups/active
| Parameter | Type | Description |
|---|---|---|
groups |
tuples or dicts | The complete desired set as (name, direction) tuples or plain dicts |
client_uid |
str or None
|
Optional client UID to bind the change to a specific messaging session |
Returns: (status_code, response)
Warning
This will fail you silently if you are not careful. An empty
list is accepted with HTTP 200 but ignored — you cannot unsubscribe
from your last remaining channel this way (verified live). And since the
semantics are absolute, always build your new set from
get_active_groups(), never from scratch.
Example:
# Subscribe to two channels - anything else gets unsubscribed!
status, _ = await srv.groups.set_active_groups([
("WOLF_ADSB", "OUT"),
("WOLF_Family", "OUT"),
])Usage: await server.groups.set_active_groups_bits(bits, client_uid=None)
Bitmask variant of set_active_groups(): instead of names, you pass the
bitpos values of the groups to activate. Same absolute semantics.
API: PUT /Marti/api/groups/activebits
| Parameter | Type | Description |
|---|---|---|
bits |
list[int] |
Bit positions (each group's bitpos field) |
client_uid |
str or None
|
Optional client UID |
Returns: (status_code, response)
Usage: await server.groups.set_active_groups_force(username, groups)
Admin-forced activation of channels for another user — bypasses user opt-out. Requires admin rights. Absolute semantics for the target user's forced set.
API: PUT /Marti/api/groups/activeForce
| Parameter | Type | Description |
|---|---|---|
username |
str |
Target user |
groups |
tuples or dicts | The complete forced set |
Returns: (status_code, response)
async def wait_for_group_update(username: str) -> tuple[int, Any]
async def wait_for_group_update_until(username: str, timeout: float) -> tuple[int, Any]Long-poll: blocks until an admin alters group assignments for the given
user server-side. This is how clients refresh their channel list without
reconnecting. The _until() variant bounds the wait and raises
TimeoutError when nothing happens within timeout seconds — you will
want that variant in any production code.
API: GET /Marti/api/groups/update/{username}
Note
Verified live on 5.7-RELEASE-43-HEAD: a limited (non-admin) client certificate gets HTTP 403 on this endpoint. Poll as an admin or give the client the required role.
Usage: await server.groups.get_group_cache_enabled()
Returns whether the server-side group cache is enabled.
API: GET /Marti/api/groups/groupCacheEnabled
Returns: (status_code, data) — data is a boolean.
LDAP directory queries — only useful on LDAP-backed servers. On a plain file-auth server they answer, but return nothing meaningful.
async def get_ldap_groups(group_name_filter: str) -> tuple[int, Any]
async def get_ldap_group_members(group_name_filter: list[str]) -> tuple[int, Any]These build on the raw wrappers above and save you from the absolute-
semantics foot-gun. All of them accept an optional username= argument;
if omitted, they use the username the Server was constructed with.
async def get_active_groups(username: str | None = None) -> list[dict]Returns [{"name": ..., "direction": ...}, ...] — the current
subscriptions, read from the subscription endpoint. Feed this straight
back into set_active_groups():
current = await srv.groups.get_active_groups()
updated = current + [{"name": "WOLF_ADSB", "direction": "OUT"}]
await srv.groups.set_active_groups(updated)Warning
If the subscription readback fails (non-200 response or an unparsable
error body), get_active_groups() raises ValueError with a clear
message instead of returning bogus data. The same applies to
is_subscribed(), get_channels() and channel_exists().
async def subscribe(name, directions=("IN", "OUT"), username=None)
async def subscribe_many(names, directions=("IN", "OUT"), username=None)Read-modify-write around set_active_groups(): activates the requested
directions of the given channel(s), leaves everything else untouched.
Raises ValueError for channels that don't exist or aren't visible to
the user — before anything is written.
await srv.groups.subscribe("WOLF_ADSB") # both directions
await srv.groups.subscribe("WOLF_Family", directions=["OUT"])
await srv.groups.subscribe_many(["WOLF_ADSB", "WOLF_Family"])Mirror images of subscribe()/subscribe_many(): deactivates the
requested directions, keeps everything else. Unknown channels are ignored.
await srv.groups.unsubscribe("WOLF_ADSB")async def is_subscribed(name, direction=None, username=None) -> boolChecks the subscription state (not just availability). By default any
subscribed direction counts; narrow it with direction="OUT" etc.
if not await srv.groups.is_subscribed("WOLF_ADSB"):
await srv.groups.subscribe("WOLF_ADSB")Dev-friendly catalog view: collapses duplicate/stale entries into one record per channel —
{"name": "channel-a",
"type": "SYSTEM",
"bitpos": 3,
"directions": {"IN": true, "OUT": false}}Newest entry wins on conflicts, unpadded spelling preferred. Remember: availability, not subscription state.
async def channel_exists(name: str) -> boolTrue when the channel is visible to this user. Case-sensitive, but tolerates whitespace-padded duplicates in the server's data.
-
Basic-auth enrollment does not fall back to file users. With
auth default=ldap, password-based operations (e.g. the/Marti/api/tls/signClientenrollment flow) authenticate against LDAP first and there is no fallback to file-based accounts — the log showsLdapAuthenticator ... Invalid Credentialsand the request dies with 401, even though the same credentials work for the User Account Management API. Plan provisioning accordingly: either create your clients in LDAP or mint certificates via the server's CA tooling. - File-user channels materialize as OUT only. Even when a user is entitled to a group as BOTH, the channel layer lists a single OUT entry; explicit IN activations are silently dropped. IN-direction subscriptions matter for LDAP-backed setups.
-
Stale entries never die. Renamed/deleted groups linger in
/groups/all(including whitespace-padded name variants). Always normalize names (.strip()) before comparing — the helpers do. - Freshly created users take a moment. Right after creation, group lookups can briefly fail server-side ("User lookup failed"); writes still persist. If you script user creation + subscription changes, allow a few seconds between steps.