
# Establecer el CSS personalizado

`PUT /v1/branding/css` · permiso `branding:write`

Escribe el CSS personalizado del borrador del entorno (plan Startup o superior, función `branding_custom_css`). El CSS pasa por un saneador de lista permitida: solo clases de `ui_catalog` (`idp-*`) y propiedades y valores permitidos; la capa protegida (texto legal, consentimiento y elementos de accesibilidad) no se puede ocultar ni cubrir. Todo o nada: con una sola infracción no se escribe nada. El borrador guarda la salida canónica del saneador, no el texto enviado; `css` en la respuesta es esa salida. Una cadena vacía lo borra y no está sujeta al plan.

## Solicitud

| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
| `If-Match` | encabezado | sí | El `ETag` del borrador leído por última vez (`"0"` si nunca se escribió). Ausente: `428`; obsoleto: `412`. |
| `css` | cadena | sí | Como máximo 51200 bytes, UTF-8 válido; `""` borra. |

## Respuesta

`200 OK` con el borrador del entorno y su `ETag`. `theme` contiene solo los tokens definidos; los demás heredan el valor predeterminado de la plataforma.

```json
{
  "theme": {"mode": "light", "light": {"color": {"primary": "#0a3d91"}}, "logo_height": 40},
  "css": ".idp-c .idp-title{color:#0a3d91}\n",
  "page_template": null,
  "updated_at": "2026-10-05T09:30:00.123456Z"
}
```

## Errores

| HTTP | código | Cuándo |
|---|---|---|
| 400 | `unknown_field` | El cuerpo tiene un campo desconocido o no es un objeto JSON. |
| 403 | `plan_feature_unavailable` | El plan no incluye la función (`plan_feature_unavailable`, extensiones `feature` y `required_plan`). Los entornos de desarrollo no están restringidos. |
| 412 | `precondition_failed` | `If-Match` no coincide con la versión actual del borrador. |
| 422 | `validation_failed` | `css` falta o es `null` (`required`), es demasiado largo (`too_long`) o no es UTF-8 válido (`invalid_utf8`). |
| 422 | `branding_validation_failed` | El CSS no superó el saneador. La extensión `violations` enumera todas las infracciones: `line`, `column` (desde 1), `rule` (por ejemplo `class_not_in_catalog`, `protected_property`, `property_not_allowed`, `syntax_error`) y, si existe, `name` (nombre de la propiedad, la clase o la función). Los valores CSS nunca se devuelven. Tras 100 infracciones el saneador se detiene; el último elemento tiene la regla `too_many_issues`. La salida canónica es más larga que la entrada (cada regla recibe un prefijo de ámbito); si supera los 51200 bytes, la única infracción es la regla `too_large` en la línea 1, columna 1. |
| 428 | `precondition_required` | Falta el encabezado `If-Match`. |
| 401 | `unauthenticated` | La credencial falta, no es válida o se ha revocado. |
| 403 | `insufficient_permissions` | La credencial no tiene el permiso necesario. |
| 429 | `rate_limited` | Se superó el límite de frecuencia; vuelva a intentarlo tras el periodo `Retry-After`. |
| 503 | `service_unavailable` | Una dependencia no está disponible temporalmente; vuelva a intentarlo tras el periodo `Retry-After`. |

Ejemplo de rechazo del saneador:

```css
.idp-button{position:fixed}
.my-banner{color:red}
```

```json
{
  "type": "https://docs.keenauth.com/errors/branding_validation_failed",
  "title": "Branding validation failed",
  "status": 422,
  "code": "branding_validation_failed",
  "violations": [
    {"line": 1, "column": 13, "rule": "property_not_allowed", "name": "position"},
    {"line": 2, "column": 1, "rule": "class_not_in_catalog", "name": "my-banner"}
  ],
  "request_id": "req_01J9Z3N5W8Q2F6V4K7T1M0XB3C"
}
```

## Ejemplo

```bash
curl -X PUT https://api.eu.keenauth.com/v1/branding/css \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "3"' \
  -d '{"css":".idp-title { color: #0a3d91 }"}'
```
