# Organizaciones, proyectos, miembros e invitaciones

Una organización es quien paga y quien puede hacer qué. Contiene [proyectos](/es/concepts/model), y los proyectos contienen lo que compras. Esta guía muestra las llamadas del día a día para montar una y para llevar un equipo. Los ejemplos usan el ayudante de [Usar la API de Zaroz](/es/guides/api/overview).

## Roles

| Rol | Puede |
|---|---|
| `owner` | Todo, incluido borrar la organización y gestionar a otros propietarios |
| `admin` | Comprar, gestionar proyectos y miembros (no por encima de su propio rol), crear tokens de API de la organización |
| `billing` | Pagar facturas y leer la facturación |
| `member` | Usar los servicios de la organización; ver sus proyectos, miembros y facturas |

Dos reglas te protegen de quedarte fuera: no puedes degradar ni quitar al **último propietario**, y no puedes cambiar a un miembro cuyo rol esté por encima del tuyo (`403 member-outranks-caller`). Cualquiera puede quitarse a sí mismo. Una **organización personal** (toda cuenta tiene una) no admite otros miembros: añadir uno responde `422 member-personal-organization`.

## Crear una organización

**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"]
```

Te conviertes en su propietario. `GET /organizations` lista todas las organizaciones a las que perteneces y `GET /organizations/by-slug/{slug}` busca una por su slug. `PATCH /organizations/{id}` la renombra.

## Proyectos

Un proyecto agrupa servicios. El slug es único dentro de la organización.

**curl**

```bash
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/projects" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"name": "Production", "slug": "production", "description": "Servidores en vivo"}'
curl -sS -X PATCH "$ZAROZ_API/projects/$PROJECT" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"description": null}'                       # null lo borra; los campos omitidos no cambian
curl -sS "$ZAROZ_API/projects/$PROJECT/stats" -H "$AUTH"
curl -sS -X DELETE "$ZAROZ_API/projects/$PROJECT" -H "$AUTH"   # 204, solo si no tiene suscripciones
```

**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, o 4xx mientras tenga suscripciones
```

**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, o 4xx mientras tenga suscripciones
```

En una organización personal el slug `games` está reservado al panel de juegos (`422 project-slug-reserved`) y ese proyecto no se puede cambiar aquí (`403 project-gaming-managed`).

## Invitar a personas

Invita por correo con el rol que deben recibir. La respuesta es la misma tenga o no cuenta esa dirección, así que la API no sirve para averiguar quién está registrado.

**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"                   # pendientes, sin caducar
curl -sS -X DELETE "$ZAROZ_API/organizations/$ORG/invitations/$INVITATION" -H "$AUTH"   # retirar una
```

**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")
```

Tu propio correo debe estar verificado para poder invitar. El envío tiene límite de frecuencia, y una dirección que rechazó tiene un **periodo de espera de 30 días** antes de poder ser invitada de nuevo; ambos casos responden `429` con un `code` legible por máquina.

La persona invitada, con sesión iniciada con el mismo correo **verificado**, ve la invitación y la responde:

**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"   # levantar antes la espera
```

**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")
```

Aceptar se puede repetir (una segunda aceptación no cambia nada) y también funciona sobre una invitación rechazada que no haya caducado. Si ya perteneces a esa organización la respuesta es `409` y la invitación se cancela: aceptar nunca cambia el rol de un miembro existente.

## Añadir, cambiar y quitar miembros

Si ya conoces el id de cuenta de una persona puedes añadirla directamente, sin invitación.

**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": "<uuid del usuario>", "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}")
```

Volver a añadir a un miembro existente devuelve la membresía sin cambios. Leer la lista de miembros de una organización a la que no perteneces responde `404`, igual que una que no existe.

## País de facturación

Una compra en una organización compartida necesita el país en el que tributa. Un presupuesto responde `billing-country-required` hasta que se fija. Los propietarios y administradores lo fijan una vez; repetir el mismo valor funciona y uno distinto se rechaza con `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"})
```

## Verificar un dominio

Demostrar que la organización controla un dominio se hace con un registro DNS TXT. Inicia el desafío, publica el registro y pide una comprobación (la plataforma también vuelve a comprobar por su cuenta).

**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":"...", ...}

# publica el registro TXT en record_name con record_value y entonces:
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}`);
// tras publicar el registro:
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"])
# tras publicar el registro:
_, checked = call("POST", f"/organizations/{ORG}/domains/example.com/check")
print(checked["status"], checked["last_error"])
```

La comprobación hace una consulta DNS real y tiene un límite de frecuencia estricto, así que espera a que el registro se propague antes de volver a llamarla. `last_error` indica lo que encontró la consulta. (Esto demuestra la propiedad de un dominio para la organización; para poner un dominio delante de un servicio consulta [Dominios personalizados](/es/guides/domains/custom-domains).)

## Registro de actividad

Todo lo hecho en la organización se lista, lo más reciente primero, con quién lo hizo (una persona, o un token de API por su nombre). Acótalo con `project_id`, `service_instance_id` o `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"      # solo un servicio
```

**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"])
```

Los eventos internos de la plataforma nunca aparecen aquí: solo lo que los clientes hicieron en tu organización.

## Borrar una organización

Solo un propietario puede, y solo cuando está vacía: sin proyectos (bórralos antes) y sin saldo en el monedero. Una organización personal no se puede borrar.

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