# Organizations, projects, members and invitations

An organization is who pays and who may do what. It holds [projects](/concepts/model), and projects hold what you buy. This guide shows the day-to-day calls to set one up and to run a team. The samples use the helper from [Using the Zaroz API](/guides/api/overview).

## Roles

| Role | Can |
|---|---|
| `owner` | Everything, including deleting the organization and managing other owners |
| `admin` | Buy, manage projects and members (not above their own role), create organization API tokens |
| `billing` | Pay invoices and read billing |
| `member` | Use the organization's services; see its projects, members and invoices |

Two rules protect you from locking yourself out: you cannot demote or remove the **last owner**, and you cannot change a member whose role is above yours (`403 member-outranks-caller`). Anyone may remove themselves. A **personal organization** (every account has one) takes no other members: adding one answers `422 member-personal-organization`.

## Create an organization

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/organizations" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "Acme Games", "slug": "acme-games"}'
# {"id": "...", "name": "Acme Games", "slug": "acme-games", "role": "owner", ...}
```

**TypeScript**

```ts
import { call } from "./zaroz";

const org = await call("POST", "/organizations", { name: "Acme Games", slug: "acme-games" });
const ORG = org.body.id;
```

**Python**

```python
from zaroz import call

_, org = call("POST", "/organizations", {"name": "Acme Games", "slug": "acme-games"})
ORG = org["id"]
```

You become its owner. `GET /organizations` lists every organization you belong to, and `GET /organizations/by-slug/{slug}` finds one by its slug. `PATCH /organizations/{id}` renames it.

## Projects

A project groups services. The slug is unique inside the organization.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/projects" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "Production", "slug": "production", "description": "Live servers"}'
curl -sS -X PATCH "$ZAROZ_API/projects/$PROJECT" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"description": null}'                       # null clears it; omitted fields are unchanged
curl -sS "$ZAROZ_API/projects/$PROJECT/stats" -H "$AUTH"
curl -sS -X DELETE "$ZAROZ_API/projects/$PROJECT" -H "$AUTH"   # 204, only when it holds no subscriptions
```

**TypeScript**

```ts
const project = await call("POST", `/organizations/${ORG}/projects`, {
  name: "Production",
  slug: "production",
});
await call("PATCH", `/projects/${project.body.id}`, { name: "Live" });
const del = await call("DELETE", `/projects/${project.body.id}`);
console.log(del.status); // 204, or 4xx while it still holds subscriptions
```

**Python**

```python
_, project = call("POST", f"/organizations/{ORG}/projects", {"name": "Production", "slug": "production"})
call("PATCH", f"/projects/{project['id']}", {"name": "Live"})
status, _ = call("DELETE", f"/projects/{project['id']}")   # 204, or 4xx while it still holds subscriptions
```

In a personal organization the slug `games` is reserved for the gaming dashboard (`422 project-slug-reserved`) and that project cannot be changed here (`403 project-gaming-managed`).

## Invite people

Invite by email address with the role they should get. The answer is the same whether or not the address already has an account, so the API cannot be used to find out who is registered.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/invitations" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"email": "sam@example.com", "role": "member"}'
curl -sS "$ZAROZ_API/organizations/$ORG/invitations" -H "$AUTH"                   # pending, unexpired
curl -sS -X DELETE "$ZAROZ_API/organizations/$ORG/invitations/$INVITATION" -H "$AUTH"   # withdraw one
```

**TypeScript**

```ts
const inv = await call("POST", `/organizations/${ORG}/invitations`, {
  email: "sam@example.com",
  role: "member",
});
console.log(inv.body.status, inv.body.expires_at); // "pending", ...
const pending = await call("GET", `/organizations/${ORG}/invitations`);
```

**Python**

```python
_, inv = call("POST", f"/organizations/{ORG}/invitations", {"email": "sam@example.com", "role": "member"})
print(inv["status"], inv["expires_at"])   # "pending", ...
_, pending = call("GET", f"/organizations/{ORG}/invitations")
```

Your own email must be verified before you can invite. Sending is rate-limited, and an address that declined has a **30-day cool-down** before it can be invited again; both answer `429` with a machine-readable `code`.

The invited person, signed in with the same **verified** email, sees the invitation and answers it:

**curl**

```bash
curl -sS "$ZAROZ_API/me/invitations?status=pending" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/invitations/$INVITATION/accept" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/invitations/$INVITATION/decline" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/invitations/$INVITATION/allow-reinvite" -H "$AUTH"   # lift the cool-down early
```

**TypeScript**

```ts
const mine = await call("GET", "/me/invitations?status=pending");
await call("POST", `/invitations/${mine.body[0].id}/accept`);
```

**Python**

```python
_, mine = call("GET", "/me/invitations?status=pending")
call("POST", f"/invitations/{mine[0]['id']}/accept")
```

Accepting is repeatable (a second accept changes nothing) and also works on a declined, unexpired invitation. If you already belong to that organization the answer is `409` and the invitation is cancelled: accepting never changes an existing member's role.

## Add, change and remove members

If you already know a person's account id you can add them directly, without an invitation.

**curl**

```bash
curl -sS "$ZAROZ_API/organizations/$ORG/members" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/members" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"user_id": "<user uuid>", "role": "member"}'
curl -sS -X PATCH "$ZAROZ_API/organizations/$ORG/members/$USER" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"role": "admin"}'
curl -sS -X DELETE "$ZAROZ_API/organizations/$ORG/members/$USER" -H "$AUTH"     # 204
```

**TypeScript**

```ts
const members = await call("GET", `/organizations/${ORG}/members`);
for (const m of members.body) console.log(m.name, m.email, m.role);
await call("PATCH", `/organizations/${ORG}/members/${USER}`, { role: "admin" });
await call("DELETE", `/organizations/${ORG}/members/${USER}`);
```

**Python**

```python
_, members = call("GET", f"/organizations/{ORG}/members")
for m in members:
    print(m["name"], m["email"], m["role"])
call("PATCH", f"/organizations/{ORG}/members/{USER}", {"role": "admin"})
call("DELETE", f"/organizations/{ORG}/members/{USER}")
```

Re-adding an existing member returns the membership unchanged. Reading the member list of an organization you do not belong to answers `404`, the same as one that does not exist.

## Billing country

A purchase in a shared organization needs the country it is taxed under. A quote answers `billing-country-required` until it is set. Owners and admins set it once; repeating the same value succeeds and a different one is refused with `billing-country-already-set`.

**curl**

```bash
curl -sS -X PUT "$ZAROZ_API/organizations/$ORG/billing-country" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"country_code": "ES"}'
```

**TypeScript**

```ts
await call("PUT", `/organizations/${ORG}/billing-country`, { country_code: "ES" });
```

**Python**

```python
call("PUT", f"/organizations/{ORG}/billing-country", {"country_code": "ES"})
```

## Verify a domain

Proving that the organization controls a domain is done with a DNS TXT record. Start the challenge, publish the record, then ask for a check (the platform also rechecks on its own).

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/domains" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
# {"domain":"example.com","record_type":"TXT","record_name":"...","record_value":"...","status":"...", ...}

# publish the TXT record at record_name with record_value, then:
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/domains/example.com/check" -H "$AUTH"
curl -sS "$ZAROZ_API/organizations/$ORG/domains" -H "$AUTH"
curl -sS -X DELETE "$ZAROZ_API/organizations/$ORG/domains/example.com" -H "$AUTH"
```

**TypeScript**

```ts
const challenge = await call("POST", `/organizations/${ORG}/domains`, { domain: "example.com" });
console.log(`TXT ${challenge.body.record_name} = ${challenge.body.record_value}`);
// after publishing the record:
const checked = await call("POST", `/organizations/${ORG}/domains/example.com/check`);
console.log(checked.body.status, checked.body.last_error);
```

**Python**

```python
_, challenge = call("POST", f"/organizations/{ORG}/domains", {"domain": "example.com"})
print("TXT", challenge["record_name"], "=", challenge["record_value"])
# after publishing the record:
_, checked = call("POST", f"/organizations/{ORG}/domains/example.com/check")
print(checked["status"], checked["last_error"])
```

The check performs a real DNS lookup and is tightly rate-limited, so wait for the record to propagate before calling it again. `last_error` says what the lookup found. (This proves ownership of a domain for the organization; to put a domain in front of a service see [Custom domains](/guides/domains/custom-domains).)

## Activity log

Everything done to the organization is listed, newest first, with who did it (a person, or an API token by name). Narrow it with `project_id`, `service_instance_id` or `action`.

**curl**

```bash
curl -sS "$ZAROZ_API/organizations/$ORG/audit?page=1&page_size=50" -H "$AUTH"
curl -sS "$ZAROZ_API/service-instances/$SERVICE/activity" -H "$AUTH"      # one service only
```

**TypeScript**

```ts
const log = await call("GET", `/organizations/${ORG}/audit?page_size=20`);
for (const e of log.body.data) console.log(e.created_at, e.action, e.principal_name ?? e.api_token_name);
```

**Python**

```python
_, log = call("GET", f"/organizations/{ORG}/audit?page_size=20")
for e in log["data"]:
    print(e["created_at"], e["action"], e["principal_name"] or e["api_token_name"])
```

Platform-internal events never appear here: only what customers did to your organization.

## Delete an organization

Only an owner can, and only when it is empty: no projects (delete them first) and no wallet balance. A personal organization cannot be deleted.

```bash
curl -sS -X DELETE "$ZAROZ_API/organizations/$ORG" -H "$AUTH"      # 204
```
