runlot
ReferenceAPI

Organization

Covers 14 API operations in the orgs category.

MethodPathDescription
GET/v1/orgs
POST/v1/orgsCreate an org
PATCH/v1/orgs/{orgSlug}Rename an org's display name (runlot org rename)
DELETE/v1/orgs/{orgSlug}Delete an org (runlot org delete)
GET/v1/orgs/{orgSlug}/notificationsRecent notifications (docs/limits.md §3.4)
GET/v1/orgs/{orgSlug}/membersOrg member (runlot org members)
POST/v1/orgs/{orgSlug}/membersAdd an existing user as a member (for ops and automation)
GET/v1/orgs/{orgSlug}/invitesPending invites (runlot member invites)
POST/v1/orgs/{orgSlug}/invitesInvite by email (runlot member invite)
DELETE/v1/orgs/{orgSlug}/invites/{inviteId}Revoke an invite (runlot member uninvite)
GET/v1/invites/{token}Preview an invite (no authentication)
POST/v1/invites/{token}/acceptAccept an invite
PUT/v1/orgs/{orgSlug}/members/{userId}Change a role (runlot org set-role)
DELETE/v1/orgs/{orgSlug}/members/{userId}Remove a member or leave on your own (runlot member rm, runlot member leave)

GET /v1/orgs

operationId listOrgs

Status codeDescriptionResponse body
200The owning organizationobject
401

POST /v1/orgs

The creator becomes admin. Personal accounts are also modeled as a one-person organization (docs/mvp-scope.md), so there is no separate path here.

operationId createOrg

Request body: application/json · object

Status codeDescriptionResponse body
201The created organizationOrg
401
409

PATCH /v1/orgs/{orgSlug}

Requires admin or higher. Audited as org.rename.

The slug is not part of this surface. The slug is a name that becomes an address (<project>.<slug>.runlot.app, the certificate SAN, the clone address, the "org" field in the runlot.json committed to the customer's repository), so changing it requires keeping the old name alive for a while — and that alias is exactly what 0027 introduced and 0037 removed (docs/org-settings.md §2).

operationId updateOrg

Request body: application/json · object

Status codeDescriptionResponse body
200The updated orgOrgMembership
400
403
404

DELETE /v1/orgs/{orgSlug}

Requires admin or higher. This cannot be undone.

The confirm field must carry the exact org slug (leading and trailing whitespace is trimmed). The API enforces this check because three callers hit this surface — the dashboard, the CLI, and scripts — and if each implemented it separately, no one would notice if one left it out.

Deletion succeeds only when the org is empty. If projects, domains, repositories, or DNS zones remain, the response is 409 org_not_empty, and the envelope's details.holdings lists counts and a few names per kind. The reason is schema-level (RESTRICT constraints in 0044, 0020, and 0032): deleting the row leaves the node's data in place, and data nothing points to can neither be deleted nor billed for.

Two other rejections: a personal org returns personal_org (the org your first login created, which is never recreated), and a live paid subscription returns active_subscription (this prevents the org from disappearing while charges keep hitting the card).

The org.delete audit event outlives the orgaudit_events.org_id uses ON DELETE SET NULL, and the slug remains in target.

operationId deleteOrg

Request body: application/json · object

Status codeDescriptionResponse body
204deleted
400
403
404
409Not empty (org_not_empty) · personal org (personal_org) · active subscription (active_subscription)Error

GET /v1/orgs/{orgSlug}/notifications

A list of limit, backup, and restore events (notifications). Newest first.

If sentAt is set, an email went out too. Rows without it are either still queued or have no email to receive — for the latter, this list is the only channel: if a GitHub login has a private email, users.email is NULL (§3.3).

viewer or above.

operationId listNotifications

ParameterLocationRequiredTypeDescription
limitqueryNointeger
Status codeDescriptionResponse body
200List notificationsobject
403
404

GET /v1/orgs/{orgSlug}/members

Requires member or higher.

operationId listMembers

Status codeDescriptionResponse body
200Member listobject
403
404

POST /v1/orgs/{orgSlug}/members

This isn't an invite. The user must already have logged in once and exist in users (identified by provider+subject); if not, this returns 404 user_not_found. If they're already a member, it returns 409 already_member — changing a role is a PUT.

The surface for inviting people is POST …/invites, not this one (docs/members.md): GitHub's subject is a numeric id, a value an admin can't know, so this endpoint is for automation that already knows it.

Requires admin or higher. Audited as member.add.

operationId addMember

Request body: application/json · object

Status codeDescriptionResponse body
201Members addedMember
400
403
404The org does not exist, or the user does not exist (user_not_found)Error
409Already a member (already_member)Error

GET /v1/orgs/{orgSlug}/invites

Admin or higher. Member or higher can see the member list, but the invite list is different — it's the emails of people not yet in the org, and that's the state of an admin task, not a roster.

Accepted or revoked invites don't appear (the audit log is where history lives). Expired invites do appear — resending is the admin's job there.

operationId listInvites

Status codeDescriptionResponse body
200List invitesobject
403
404

POST /v1/orgs/{orgSlug}/invites

Admin or higher. Email doesn't go out immediately — a cp-core send cycle sweeps the queue (an empty sentAt means it's still queued).

Calling this again with the same address resends it: instead of a new invite, the same row is updated (a new token and expiry are set, and the role can change), and this returns 200. A new invite returns 201.

An address that's already a member returns 409 already_member. Too many pending invites returns 409 too_many_invites — this is abuse prevention on sending, not a plan limit.

Invites last seven days, are single-use, and can be revoked at any time. Audited as member.invite.

operationId createInvite

Request body: application/json · object

Status codeDescriptionResponse body
200Resent an existing inviteInvite
201New inviteInvite
400
403
404
409Already a member (already_member) or too many pending invites (too_many_invites)Error

DELETE /v1/orgs/{orgSlug}/invites/{inviteId}

Admin or higher. Email that hasn't gone out yet won't go out — revoking also clears the send queue. An invite that's already accepted or revoked returns 404. Audited as member.uninvite.

operationId revokeInvite

Status codeDescriptionResponse body
204Revoked
403
404

GET /v1/invites/{token}

No authentication. Whoever receives the invite link hasn't logged in yet, and they need to know what it is before they log in. The token is a 32-byte random value, so it can't be enumerated, and the cost of opening it ends at "whoever we sent the email to sees the org name."

State is a value, not an error (state). Returning 404 for an expired invite would leave the UI unable to distinguish "the link is wrong" from "the link is old."

An unknown token returns 404 — the same answer as a token revoked long ago.

operationId getInvite

Status codeDescriptionResponse body
200InviteInviteView
404

POST /v1/invites/{token}/accept

Requires login. There is no rule that the logged-in account's email must match the invited address — someone who signs in with a GitHub private email has no users.email at all, and adding that rule would lock them out of every org. Instead, invites live for seven days, are single-use, and can be revoked at any time, and the audit record keeps both the invited address and the accepting account.

If the person is already a member, their role does not change (alreadyMember: true). If an invite became a surface for demoting an admin, that demotion would not pass the last-admin check.

Audited as member.accept.

operationId acceptInvite

Status codeDescriptionResponse body
200Acceptedobject
401
404
409Already accepted (invite_used), revoked (invite_revoked), or expired (invite_expired)Error

PUT /v1/orgs/{orgSlug}/members/{userId}

Requires admin or higher. You cannot demote the last admin (409 last_admin) — this includes yourself. Without this rule, an org could end up with no one able to manage it, and the only way out would be editing the DB directly. Audited as member.role.

operationId setMemberRole

Request body: application/json · object

Status codeDescriptionResponse body
200The member that changedMember
400
403
404
409This is the last admin (last_admin)Error

DELETE /v1/orgs/{orgSlug}/members/{userId}

Removing someone else requires admin or higher; removing yourself is open to anyone (if userId is your own, it's leaving). If only admins could remove members, someone invited as a viewer would have no way to leave the org.

The last admin can't be removed or leave (409 last_admin). The audit log records removal as member.remove and leaving as member.leave — the same statement but different events, so the names differ.

operationId removeMember

Status codeDescriptionResponse body
204removed
403
404
409This is the last admin (last_admin)Error

On this page