Team & account
Invite teammates, manage members, mint and revoke API keys, and read the audit log of who did what.
Everything on this page is on the account plane and takes a pts_ session token. In the app it lives under Account.
Members and roles
GET /v1/tenants/{tid}/members lists members with their role and its display role_name. A role is a built-in id, owner, admin, member, responder or viewer, or a custom role id such as cr_3f9a0c1d2e4b5a67; see Concepts for what each can do.
PUT /v1/tenants/{tid}/members/{aid} with {"role": "responder"} changes a member’s role. It needs members:write, which owners and admins have. You can grant only a role whose permissions you also hold, only an owner can grant owner, and only an owner can change an owner. You can change only a member whose team role, and the role of every project grant they hold, you could give yourself; anyone else is a 403 cannot change the role of a member with a higher role. Any change to a member’s role, promotion included, revokes the invites they sent that are still pending and expires the pairing codes they minted that are still unused. Demoting the last owner is a 409, as is any change on a self-hosted tenant, whose membership lives on the box. An unknown role is a 400 unknown role. Making a project-limited member an owner is a 409 clear the member's projects before making them owner; lift the limit first with {"projects": null}. Demoting an owner, yourself included, needs {"current_password": "…"} in the body, as removing one does. So does giving anyone owner or a role that holds members:write, built-in admin or custom, so a stolen session alone cannot add another admin. A missing password is a 400 granting the <Role Name> role requires your current password on the field current_password, a wrong one a 401 invalid password, and repeated wrong ones a 429.
GET /v1/roles lists what each role may do. role_defs holds every role of the team, the built-in presets first, then the custom roles, each with its permissions. role, role_name and permissions are the caller’s own, and the caller’s projects are there too (see below), each grant with its role_name. Each rule in its routes names the permission it needs and carries a scope, which says whose role is checked: team checks the team role, project the grant for the project in the path, and object the grant for the project of the alert, log line or incident the route names. A rule with "unrestricted_only": true is refused to project-limited members.
DELETE /v1/tenants/{tid}/members/{aid} removes one and revokes their API keys, the invites they created and their box pairings. As with role changes, you can remove only a member whose team role and every project grant’s role you could give; anyone else is a 403 cannot remove a member with a higher role. Removing the last owner is refused. Removing any owner, yourself included, needs {"current_password": "…"} in the body.
Removal does not wait on what the member was part of. They are taken out of on-call schedules, escalation ladders and current overrides, and every webhook, integration and action they created or last repointed is disabled, not deleted, so you can check the target and re-enable it. That holds even if someone else later changed only its secret or headers. Whoever re-enables one becomes the member it is tied to. The 200 lists what changed under schedules, escalations, overrides and disabled, plus the sources they created under sources for you to review; a removal that touched nothing answers 204. Each change is in the audit log.
Custom roles
A custom role is a named set of permissions that an owner defines and then assigns like a built-in role. What it can hold is in Concepts.
GET /v1/tenants/{tid}/roles is open to any member. It answers {"roles": […], "permissions": […], "limit": 20}. roles lists the built-in presets first, then the custom roles by name, each with its id, name, builtin and permissions; a custom role also carries created_at, updated_at and in_use. permissions is the catalogue of every permission, each with its id, area, level and whether it is grantable.
POST /v1/tenants/{tid}/roles with {"name": "On-call lead", "permissions": ["alerts:write", "oncall:write", …]} creates one and answers 201 with the role. PUT /v1/tenants/{tid}/roles/{rid} replaces its name and permissions and answers 200 with the role. DELETE /v1/tenants/{tid}/roles/{rid} removes it and answers 204. All three are owner only; anyone else gets 403. Editing a custom role so it gains members:write needs {"current_password": "…"}: missing is a 400 giving a role members:write requires your current password on current_password, and wrong or repeated are as for role changes. A change applies on the holders’ next request. Refusals:
- An unknown permission is a
400unknown permission: <id>, and anyownershippermission a400ownership permissions cannot be granted. - A name must be 1 to 40 characters (
400role name must be 1 to 40 characters) and cannot be a built-in role’s name (400role name is reserved). - Writing a built-in role is a
400built-in roles cannot be changed. - A name another role has is a
409role name taken. - The 21st custom role is a
409custom role limit reached, whose body also carries"limit": 20. - A
{rid}that is no custom or built-in role of the tenant is a404unknown role. - On a self-hosted tenant, whose membership lives on the box, every write is a
409membership is managed on the data plane.
A role in use cannot be deleted. The 409 role in use carries in_use, as {"members", "grants", "invites", "pair_codes"} counts of what holds it. To delete it anyway, add ?replace_with= with a built-in role other than owner or another custom role. Everything that held the role moves to the replacement, including roles inside an invite’s projects, and the role is deleted in the same step. A replacement that is admin or holds members:write needs current_password in the body. A replacement that does not exist, is owner or is the role being deleted is a 400 invalid replace_with. Assigning an unknown role id anywhere is a 400 unknown role.
Per-project access
Each member in the list carries projects. null means the team role applies to every project. A list such as [{"project": "web", "role": "member"}] limits the member to those projects, each with its own role. A project-limited caller sees each member’s grants only on projects they also hold: an unrestricted member still shows null, and a member whose grants all lie elsewhere shows []. How that works is in Concepts.
PUT /v1/tenants/{tid}/members/{aid}/projects with {"projects": [{"project": "web", "role": "member"}]} sets a member’s grants, and {"projects": null} lifts the limit. The answer is the member row. It needs members:write, and works only on someone who is not an owner and whose team role and current grants’ roles you could all give; anyone else is a 403 cannot change the projects of a member with a higher role. Each grant’s role is admin, member, responder, viewer or a custom role id, and never one with permissions you lack. Each grant also carries role_name. An unknown project or an empty list is a 422 validation failed whose fields entry for projects names the rule: required, invalid, unknown_project or max. To clear the limit, send null. Limiting an owner is a 409 an owner cannot be limited to projects, as is any change on a self-hosted tenant, whose membership lives on the box. A project-limited admin cannot change grants (403). When the grants actually change, the member’s pending invites are revoked and their unused pairing codes expire, as for a role change; sending the grants they already hold revokes nothing.
Project-limited members also get 403 {"error": "not available to project-limited members"} when they change or remove a member, list, create or revoke invites, create a pairing code, create a project, or change issue trackers, the relay or anything on a status page. They can revoke only their own credentials. Even with members:write, DELETE /v1/tenants/{tid}/keys/{kid} or /v1/tenants/{tid}/pairings/{aid} on another member’s key or pairing is that same 403, and revoking another member’s AI connection is a 404 unknown grant. GET /v1/tenants/{tid}/grants lists only their own AI connections, with can_manage false. Opening an incident (POST /v1/incidents) must name a granted project, or the answer is 404 unknown project.
Invites
POST /v1/tenants/{tid}/invites with {"email": "…", "role": "member"} mints an invite token, valid seven days. It needs members:write, and the role, built-in or custom, must have only permissions you also hold. An invite for owner or for a role that holds members:write also needs your "current_password": missing, wrong and repeated are as for role changes. A tenant holds at most 100 pending invites. An optional projects, shaped as above, invites someone straight into a project limit: it applies as the invite is redeemed, so they are never unrestricted in between. GET lists pending ones with their projects; DELETE /v1/tenants/{tid}/invites/{id} revokes one.
The invitee signs up or in, then POST /v1/invites/{token}/redeem. If the invite’s role is deleted while the invite is being redeemed, the answer is 409 the invite's role no longer exists. An invite lives only as long as its sender’s standing. Removing the sender, or changing their role or project grants, revokes their pending invites, and redeeming one is then a 410 invite revoked. At redemption the sender must also still be a member who is not project-limited, holds members:write and could give the invite’s role and each of its project roles. A change that revokes nothing, such as an owner editing the sender’s custom role, can still break that, and the answer is then 409 the person who sent this invite can no longer grant its role: nobody joins and the invite is not used up. In the app the tenant picker has a Join field for the token.
A self-hosted tenant refuses invites on pingtower.com, because its membership lives on the box. Use pairing seats instead.
API keys
POST /v1/tenants/{tid}/keys mints a ptk_ key, shown once. GET lists key ids and creation times, never secrets. DELETE /v1/tenants/{tid}/keys/{kid} revokes. Keys have ids, not names, so note which id went where.
Any member can mint keys for themselves, up to 100 live ones per tenant, and revoke their own. Revoking someone else’s key needs members:write, and only for a member whose team role and every project grant’s role you could give; anyone else is a 403 cannot revoke a key belonging to a member with a higher role. Owners and admins work as before.
Your account
- GET
/v1/mereturns the account and every membership, each with itsroleandrole_name, which is where “I’m in three companies” becomes visible. - POST
/v1/account/emailand/v1/account/passwordchange them; both re-prove the current password, and wrong answers are rate limited per session. Changing the password signs out your other sessions, revokes your API keys in every tenant except the one a phone got by pairing with a box, and cancels any pending reset tokens. - POST
/v1/logoutends the session. - POST
/v1/account/deletewith your email and password deletes the account. Refused while you are the only owner of a tenant that has other members; hand it over or delete the tenant first. A tenant you are alone in is deleted with you. In every tenant you leave, the account is cleaned up the way removal does it. A tenant that cannot be reached at that moment does not block the deletion; its cleanup is retried hourly until it runs. In the app: Account → Delete account.
Forgotten password
The service sends no email. On the hosted service, write to [email protected] from the account’s address. On your own box, POST /v1/account/password/reset-request writes a reset token to the daemon’s log for you to relay, and POST /v1/account/password/reset redeems it.
Tenant lifecycle
POST /v1/tenants/{tid}/rename is owner or admin, or any role with team_settings:write. POST /v1/tenants/{tid}/delete is owner only, re-proves your current password, and removes every project, alert and log line the tenant holds. GET /v1/tenants/{tid}/plan reads plan and usage.
Audit log
GET /v1/tenants/{tid}/audit returns entries of actor, action, detail, created_at, newest first. Every write on both planes is recorded: tenant.create, tenant.rename, invite.create, invite.redeem, invite.revoke, member.remove, member.projects, role.create, role.update, role.delete, apikey.issue, apikey.revoke, project.*, source.* including source.rotate_token, rule.*, integration.*, action.* including action.fire, oncall.*, report.*, pair.issue, pair.redeem, relay.configure, plan.change, account.delete, and every alert.ack and alert.resolve. For a project-limited caller, entries about a project outside their grants are left out, and team-level entries stay. The app shows it under Account → Audit log.