> ## Documentation Index
> Fetch the complete documentation index at: https://docs.doconda.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Exportar en tu servidor

> Convierte una sala de coedición en un .docx, .xlsx o .pptx en Node, quédate conectado para conservar su estado y convierte Markdown en .docx, sin navegador ni React.

La **exportación en el servidor** convierte documentos en archivos en Node, sin navegador ni React. Importas
`@doconda/editor/server` en tu propio servidor y llamas a una función. Está incluida en la [licencia del editor](https://docs.doconda.com/es/editor/license.md).

Sirve para:

* Guardar una [sala de coedición](https://docs.doconda.com/es/editor/collaboration.md) desde tu backend, en lugar de elegir un navegador que guarde por todos.
* Convertir Markdown en un archivo de Word sin montar un editor oculto.

Necesita Node 22 o posterior, que ya trae `WebSocket`.

## Tus documentos se quedan en tu servidor

La conversión y la exportación corren dentro de tu proceso. El contenido de los documentos nunca pasa por los servidores
de Doconda por esta función:

* **La única llamada a Doconda es el permiso de licencia.** Envía tu clave de API y la marca de tu copia, y nada del
  documento: ni bytes, ni nombre de archivo, ni contenido de la sala.
* **Leer una sala va solo por el transporte que elijas.** Con `url` o `provider`, Doconda no interviene. Con `token`, los
  cambios pasan por el servidor de Doconda, que los reenvía y no guarda nada, como en cualquier sesión de coedición.

## Convertir un archivo

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { readFile, writeFile } from "node:fs/promises"
import { convert } from "@doconda/editor/server"

const { bytes, warnings } = await convert(await readFile("notas.md"), {
  name: "notas.md",
  to: "docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})
await writeFile("notas.docx", bytes)
```

De `name` solo se usa la extensión, que dice qué son los bytes. `warnings` lista lo que el motor tuvo que avisar al abrir o
al guardar.

| De | A |
| - | - |
| `.md` | `docx`, `md`, `txt` |
| `.docx`, `.txt` | `docx`, `md`, `txt` |
| `.xlsx`, `.csv` | `xlsx`, `csv` |
| `.pptx` | `pptx` |

PDF y PNG se rechazan con `format_unsupported`: PNG necesita un lienzo, y un PDF se paginaría sin medidas reales de las
fuentes. Para un PDF, usa `ref.save("pdf")` en el navegador.

## Exportar una sala

`exportRoom` entra en la sala, espera lo que tienen los demás, devuelve el archivo y se va:

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { exportRoom } from "@doconda/editor/server"

const { bytes } = await exportRoom({
  url: `wss://collab.acme.com/?room=doc-${id}`,
  file: await leerArchivoOriginal(id),
  name: "contrato.docx",
  to: "docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})
```

| Opción | Qué es |
| - | - |
| `url`, `token` o `provider` | Exactamente una, como en [`collab`](https://docs.doconda.com/es/editor/collaboration). `token` puede ser una función. |
| `file` | El archivo que abrió la sala. `null` solo si la sala abrió un documento en blanco. |
| `name` | El nombre del archivo. Su extensión dice el tipo de documento. |
| `to` | El formato que quieres recibir. |
| `apiKey` | Tu [clave de API](https://docs.doconda.com/es/quickstart.md), no la clave de licencia `ek_…`: este código corre en tu servidor, así que puede guardar un secreto. Nunca la uses en una página. |
| `timeoutMs` | Cuánto esperar una respuesta de la sala. 10 segundos por defecto. |

Conviene saber:

* **Pasa el archivo que abrieron todos.** Una sala no contiene el documento entero: lo que nadie ha tocado sale de `file`.
  Un archivo distinto no se detecta y da un resultado incorrecto.
* **Solo lee.** No escribe nada en la sala, sea cual sea el archivo que pases, y no aparece como participante.
* **Solo lee lo que hay en la sala en ese momento.** El servidor de Doconda no guarda nada, así que cuando se va el último no
  hay nada que leer y recibes `collab_empty_room`. Para conservarlo, [quédate en la sala](#quedarse-en-una-sala).
  También sirve un servidor propio que guarde el estado, si contesta con ese estado al saludo (mensaje de tipo `2`).
* **Una sala vacía es un error**: nunca recibes el archivo viejo ni un documento en blanco.
* Un `provider` que pases sigue abierto; cerrarlo es cosa tuya.

### En el servidor de Doconda

Pide un token con `audience: "server"`, con tu clave de API. Un token normal se rechaza sin el `Origin` de un navegador.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { Doconda } from "@doconda/sdk"
import { exportRoom } from "@doconda/editor/server"

const doconda = new Doconda() // lee DOCONDA_API_KEY

const { bytes } = await exportRoom({
  token: async () => (await doconda.editor.collabToken(`doc-${id}`, { audience: "server" })).token,
  file: await leerArchivoOriginal(id),
  name: "contrato.docx",
  to: "docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})
```

En Python, `doconda.editor.collab_token(room, audience="server")`. Sin el SDK, envía `POST /editor/collab/token` con
`{ "room": "…", "audience": "server" }`. Guarda este token en tu servidor.

## Quedarse en una sala

`joinRoom` mantiene una conexión abierta y guarda el documento en memoria, así que sigue teniendo el estado cuando se han ido todos:

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { joinRoom } from "@doconda/editor/server"

const sala = await joinRoom({
  url: `wss://collab.acme.com/?room=doc-${id}`,
  file: await leerArchivoOriginal(id),
  name: "contrato.docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})

// más tarde, tantas veces como quieras, también cuando ya no queda nadie:
const { bytes } = await sala.save("docx")

await sala.close()
```

| Miembro | Qué hace |
| - | - |
| `save(to?)` | Devuelve el archivo con todo lo recibido hasta ahora, por defecto en el formato del documento. |
| `contentVersion()` | Un número que sube con cada cambio recibido. Si no se ha movido, no hay nada nuevo que guardar. |
| `peers()` | Cuántas personas hay ahora en la sala. Cuando llega a 0, es un buen momento para guardar. |
| `status()` | `"connecting"`, `"open"` o `"closed"`. |
| `close()` | Desconecta y libera el documento. No queda nada en marcha. |

Admite las mismas opciones que `exportRoom`, sin `to`. Conviene saber:

* **Nadie lo ve.** No envía cursor ni presencia y no cuenta en los `peers` de los demás. No escribe nada en la sala. Solo
  envía el saludo que pide su estado a los demás y, al cerrar una conexión que abrió, el mensaje normal de «esta conexión se
  fue» con su propio ID, que no cambia nada para los demás. En el servidor de Doconda sí ocupa una de las 50 plazas de la sala.
* **Entra antes de que la gente pueda irse.** Solo sabe lo que recibió mientras estaba conectado. Entra cuando se abre la sala.
* **Si se corta la conexión, reconecta sola** y vuelve a pedir el estado, así recupera lo editado entretanto si sigue conectado
  alguien que lo tenga. Pasa `token` como función: un token fijo se rechaza pasados 10 minutos.
* **Si la sala cierra la conexión para siempre** (un código de `4000` a `4999`), conserva lo que tiene. `save()` sigue
  funcionando y añade un aviso, porque pueden faltar cambios posteriores.
* `save()` sin nada recibido espera hasta `timeoutMs` y luego falla con `collab_empty_room`.
* Si `joinRoom` no conecta dentro de `timeoutMs`, o la sala la rechaza, falla en el acto con `collab_unreachable` o
  `collab_closed`.
* **Una sesión por archivo.** Cuando guardes una versión nueva y la gente vaya a abrir esa, haz `close()` y vuelve a entrar
  con ella como `file`: los cambios de una sala solo tienen sentido sobre el archivo que abrió.
* El documento vive en la memoria de tu proceso: si el proceso se reinicia, lo no guardado se pierde.

## Cuál usar

| | `exportRoom` | `joinRoom` |
| - | - | - |
| Lee | Solo lo que hay en la sala en ese momento. | Todo desde que entró. |
| Con la sala vacía | Falla con `collab_empty_room`: no hay nada que devolver. | Sigue devolviendo lo que recibió. |
| Conexión | Abre, lee y cierra. | Sigue abierta hasta `close()`. |
| Úsala para | Un guardado o una descarga mientras alguien edita. | No perder nunca el último cambio antes de que se vacíe la sala. |

Un `exportRoom` que llega cuando ya se ha ido el último no recibe nada, porque el servidor no guarda nada. Si necesitas lo que
había cuando se fueron todos, usa `joinRoom`.

## La licencia en tu servidor

1. La función pide a Doconda un permiso con tu clave de API (`POST /v1/editor/server/permit`). No se comprueba ningún dominio,
   porque un servidor no tiene.
2. Doconda comprueba la clave y que tu organización tiene la licencia activa, y devuelve un permiso **firmado** válido durante **1 hora**.
3. La función comprueba la firma y guarda el permiso en memoria, y lo renueva cuando quedan menos de 15 minutos.

Un permiso de servidor no abre el editor en un navegador ni deja entrar en ninguna sala. No hay modo de desarrollo: en
`localhost` también hace falta la clave.

**Si la API de Doconda no responde**, un proceso que ya tiene un permiso válido sigue funcionando hasta que caduque, o sea,
hasta 1 hora. Un proceso sin permiso válido falla con `editor_license_unreachable` y puede volver a intentarlo más tarde. Si
Doconda rechaza la clave (baja, clave cambiada), la función se detiene en la siguiente comprobación.

Una sesión de `joinRoom` comprueba su permiso en cada `save()` y lo renueva ahí, así que puede seguir conectada más de lo que
dura un permiso. Una sesión sin actividad no hace ninguna petición. **Si se revoca la licencia durante una sesión**, la conexión
y el documento siguen, y `save()` falla con `editor_unlicensed` desde la siguiente renovación, o sea, en menos de 1 hora; si la
licencia vuelve, `save()` funciona otra vez sin perder nada.

## Errores

Cada fallo lanza un `DocondaServerError` con un `code`:

| Código | Qué significa | Qué hacer |
| - | - | - |
| `editor_unlicensed` | Sin clave, una clave que Doconda rechaza o un permiso no válido. | Revisa la clave y que la licencia esté activa. |
| `editor_license_unreachable` | No se pudo contactar con Doconda y no hay permiso válido. | Vuelve a intentarlo más tarde. |
| `format_unsupported` | Ese tipo de archivo, o esa salida, no está disponible en un servidor. | Mira la tabla de arriba. |
| `file_invalid` | No se pudo abrir el archivo. | Revisa el archivo y su extensión. |
| `file_required` | `exportRoom` no recibió `file`. | Pasa el archivo, o `null` para un documento en blanco. |
| `collab_empty_room` | No se recibió nada de la sala dentro de `timeoutMs`. | Quédate con tu copia guardada, o usa `joinRoom` para tenerlo cuando lo necesites. |
| `collab_closed` | La sala rechazó la conexión; `closeCode` dice por qué. | Mira los [códigos de cierre](https://docs.doconda.com/es/editor/collaboration.md#problemas-de-conexión). |
| `collab_unreachable` | La conexión no se abrió dentro de `timeoutMs`. | Revisa la dirección y tu red. |

## Relacionado

> [card] [Coedición en tiempo real](https://docs.doconda.com/es/editor/collaboration.md)
>
> Salas, tokens y cómo montar tu propio servidor.

> [card] [Licencia y dominios](https://docs.doconda.com/es/editor/license.md)
>
> Tu clave, y qué pasa cuando termina la suscripción.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.