Saltar al contenido principal

API REST

Acceso programático a sol2flow con claves de API personales. Una clave actúa como la persona que la creó, con exactamente sus permisos: la API es otra forma de entrar con las mismas comprobaciones de permisos que la app, nunca una forma de saltárselas.

  • URL base: <APP_URL>/api/v1
  • Referencia: cada instancia tiene su propia Swagger UI en <APP_URL>/api/docs y el documento OpenAPI 3.1 en <APP_URL>/api/v1/openapi.json, siempre acordes con la versión de la instancia (un administrador puede desactivarlos, consulta Ajustes de la instancia). En la Swagger UI, Authorize acepta una clave de API y Try it out llama a la API desde tu navegador.
  • Esta página explica los conceptos; la Swagger UI muestra cada endpoint con sus parámetros y esquemas de respuesta.
  • Para usar sol2flow desde un asistente de IA (Claude, Cursor, VS Code, …), consulta el servidor MCP.

Autenticación​

  1. En la app: menú de la cuenta (abajo a la izquierda) → Claves de API → Crear clave de API. Dale un nombre, una caducidad (30 días, 90 días (por defecto), 1 año, nunca) y su acceso: Acceso completo (por defecto) o Solo lectura (Ámbitos). Para crear una clave necesitas haber iniciado sesión en los últimos 10 minutos (si no, «Confirma que eres tú»). La clave (sf_<prefix>_<secret>) se muestra una sola vez: cópiala en tu gestor de secretos.
  2. Envíala en cada petición:
curl -H "Authorization: Bearer $SOL2FLOW_KEY" https://flow.example.com/api/v1/me
Cambio: roles de staff

GET /me devuelve staff_role ("OWNER", "ADMIN", "SUPPORT", "AUDITOR" o null) en lugar del booleano is_instance_admin, que ya no existe. Los scripts que comprobaban is_instance_admin deben comprobar staff_role !== null, o el rol concreto.

  • Solo se acepta la cabecera Authorization: Bearer. Las cookies se ignoran: una petición que solo lleva la cookie de sesión del navegador recibe 401. Así no hay superficie para CSRF, y un script no puede aprovechar una sesión del navegador.
  • Una clave deja de funcionar cuando caduca, cuando se revoca (tú, o un Propietario / Admin de la instancia), cuando tu cuenta se desactiva o se elimina, mientras tu cuenta no pueda iniciar sesión (por ejemplo, un registro cuya dirección de correo aún no está confirmada) y mientras la instancia tenga las claves de API desactivadas (403 api_disabled).
  • Hasta 25 claves activas por cuenta. Cada creación y revocación de una clave envía un correo de seguridad a la cuenta.
  • La lista muestra de cada clave su nombre, sf_<prefix>…<last 4>, su acceso (Acceso completo / Solo lectura), su creación, su caducidad y su último uso (se actualiza como máximo una vez por minuto).

Ámbitos​

Una clave tiene acceso completo (todo lo que puede hacer su propietario: es el valor por defecto, y el de todas las claves creadas antes de que existieran los ámbitos) o es de solo lectura:

  • Una clave de solo lectura solo puede usar GET y HEAD: listar, leer, buscar, descargar adjuntos (GET /files/{id}). Cualquier otro método responde 403 insufficient_scope, antes de que pase nada más.
  • Se comprueba de forma centralizada en todos los endpoints.
  • Algunas lecturas también necesitan acceso completo: los endpoints de webhooks (sus URL y registros de entrega son configuración de administración).
  • El documento OpenAPI indica el ámbito que necesita cada operación en x-required-scope (read / full), y la descripción del esquema bearer explica la regla.
  • Un ámbito nunca amplía nada: una clave de solo lectura sigue viendo solo lo que puede ver su propietario.

Convenciones​

  • JSON de entrada y de salida, en snake_case. Las marcas de tiempo son ISO 8601 en UTC (2026-09-25T09:30:00.000Z); los días naturales son YYYY-MM-DD. Los ids son UUID; los espacios de trabajo se identifican por su slug (como en /w/<slug>).
  • Éxito: 200 con el recurso, 201 al crear, 202 para invitaciones (enviadas por correo), 204 (sin cuerpo) al eliminar. Las escrituras devuelven el recurso actualizado, así que rara vez necesitas un segundo GET.
  • Las personas son { id, name, username }, nunca su correo (solo GET /me devuelve el tuyo), igual que en los selectores de la app. Excepción, como en la app: el staff que ve correos (Propietario, Admin, Soporte: Roles de staff) recibe email en los miembros y en las personas del tablero; las direcciones de las invitaciones siguen la regla de enmascarado que se explica más abajo.
  • El texto enriquecido (descripciones de tareas, comentarios) se acepta como texto plano (un párrafo por línea) o como documento Tiptap JSON ({"type":"doc","content":[…]}, el formato del editor). Ambos se sanean exactamente igual que lo que se introduce en la app; las menciones ({"type":"mention","attrs":{"id":"<user id>","label":"Name"}}) solo se conservan para personas que pueden abrir el tablero, y se les notifica. Las respuestas incluyen el JSON (description, doc) y el texto plano (description_text, text). Markdown funciona en los dos sentidos: envía "format": "markdown" con una cadena, y añade ?format=markdown para recibirlo; consulta Markdown.
  • Posiciones: las listas, las tareas y los elementos de checklist se mueven entre vecinos: previous_id (el elemento que queda justo antes) y next_id (justo después). Mover una tarea sin ninguno de los dos la coloca al final de la lista.
  • Las escrituras crean las mismas entradas de actividad y notificaciones que la app, contigo como autor.

Paginación​

Los endpoints de listas aceptan limit (1–100, por defecto 50) y cursor, y responden

{ "data": [ … ], "next_cursor": "eyJ1Ijoi…" }

Devuelve next_cursor como cursor hasta que sea null. Los cursores son opacos (no los construyas); uno mal formado da un 400. GET /tasks se ordena por el último cambio, del más antiguo al más reciente, así que updated_since=<timestamp> junto con el cursor permite una sincronización incremental.

Errores​

{ "error": { "code": "title_required", "message": "Title is required.", "field": "title" } }

code es estable: básate en él; message está en inglés y es para personas; field indica el campo de entrada cuando se conoce; max es el límite para too_long / file_too_large / api_key_limit; retry_after (segundos) acompaña al 429; plan_limit viene con kind (lo que se ha agotado, p. ej. seats o guests), limit y used.

EstadoCuándoCódigos habituales
400El cuerpo no es JSON; un parámetro de ruta o de consulta no es válido (UUID o cursor incorrecto)invalid_input
401Sin clave, o clave mal formada, desconocida, caducada o revocada; cuenta desactivadaunauthenticated
403Tu rol no lo permite; la clave es de solo lectura; claves de API, webhooks o enlaces públicos desactivados; no incluido en tu planforbidden, insufficient_scope, api_disabled, webhooks_disabled, public_links_disabled_workspace, plan_feature, guest_viewer_only
404No existe o no puedes abrirlo (la API nunca dice cuál de las dos)not_found
405Ruta conocida, otro métodomethod_not_allowed
409El estado actual no lo permiteboard_archived, label_scope, last_board_admin, last_admin_leave, last_private_admin, oidc_managed, instance_admin_leave, conflict, upload_incomplete, webhook_limit, plan_limit
413Archivo (o cuerpo JSON > 1 MB) demasiado grandefile_too_large
422Falló la validacióntitle_required, name_required, too_long, dates_order, duration_invalid, invalid_input, webhook_url_blocked, webhook_url_unresolved, webhook_url_invalid, events_required, …
429Límite de peticiones: espera Retry-After segundosrate_limited
500Un bug; no se filtra nada internointernal

Límites de peticiones​

Por clave: 600 peticiones / 10 minutos, de las cuales como máximo 120 escrituras (POST, PUT, PATCH, DELETE). Las peticiones sin una clave válida se limitan por IP del cliente (30 / 10 minutos; después, 429 en lugar de 401). Las llamadas a la API también cuentan para los mismos límites por usuario que la app (comentarios 30 / min, búsqueda 240 / min, subidas 300 / h, invitaciones 100 direcciones / h por quien invita y 200 / h por espacio de trabajo, revocaciones 100 / h). Al superar un límite: 429 con una cabecera Retry-After (segundos) y retry_after en el cuerpo.

Endpoints​

La referencia completa y siempre actualizada (parámetros, cuerpos, esquemas de respuesta, ejemplos) es la Swagger UI de tu instancia en /api/docs. Resumen:

ÁreaEndpoints
CuentaGET /me: tu perfil, incluidos staff_role (OWNER · ADMIN · SUPPORT · AUDITOR · null) y time_zone (tu zona IANA guardada en Ajustes → Preferencias, p. ej. Europe/Berlin; null = automática, la del navegador; solo lectura: las marcas de tiempo de la API siempre son UTC) y, desde la API 1.6.0, email_verified (true cuando tu dirección de correo está confirmada; false sin dirección de correo) y, a partir de la API 1.8, api_key: la clave usada en la petición (name, prefix, scope full / read, expires_at; nunca su secreto)
Espacios de trabajoGET /workspaces · GET /workspaces/{slug} · GET /workspaces/{slug}/search?q=
TablerosGET /workspaces/{slug}/boards (archived, q) · POST /workspaces/{slug}/boards · GET /boards/{id} (con las listas) · PATCH /boards/{id} (nombre, acceso, rol por defecto) · POST /boards/{id}/archive · POST /boards/{id}/restore
ListasPOST /boards/{id}/lists · PATCH /boards/{id}/lists/{listId} · POST …/lists/{listId}/move · DELETE …/lists/{listId}
EtiquetasGET /workspaces/{slug}/labels · GET /boards/{id}/labels · POST /boards/{id}/labels
TareasGET /tasks (board_id o workspace; list_id, assignee, label, q, due_from, due_to, updated_since, archived=true para incluir tareas archivadas) · GET /tasks/{id} · POST /tasks · PATCH /tasks/{id} · POST /tasks/{id}/move · POST /tasks/{id}/move-board (board_id, list_id, position: otro tablero del mismo espacio de trabajo; clave nueva, la antigua redirige en la app) · POST /tasks/{id}/archive · tareas: estimate_minutes + estimate_display (tiempo de trabajo, Accept-Language), escribe estimate: "3d" o estimate_minutes · POST /tasks/{id}/unarchive (una tarea archivada es de solo lectura: 409 task_archived) · DELETE /tasks/{id} · POST /tasks/{id}/restore · PUT /tasks/{id}/labels · PUT/POST /tasks/{id}/assignees, DELETE /tasks/{id}/assignees/{userId} · lo mismo para members
ChecklistPOST /tasks/{id}/checklist · PATCH /checklist/{itemId} (texto, hecho) · POST /checklist/{itemId}/move · DELETE /checklist/{itemId}
Vínculos entre tareasGET /tasks/{id}/links (solo vínculos con tareas que puedes abrir) · POST /tasks/{id}/links (kind: relates_to, blocks, blocked_by, duplicates, duplicated_by, visto desde esta tarea; task_id: la otra tarea, del mismo espacio de trabajo; 409 link_exists, 422 link_self) · DELETE /tasks/{id}/links/{linkId}
ComentariosGET /tasks/{id}/comments (con respuestas; `order=newest
TiempoGET /time-entries?workspace= (board_id, task_id, user_id, from, to) · POST /time-entries · PATCH /time-entries/{id} · DELETE /time-entries/{id} · GET /timer · POST /timer/start · POST /timer/stop
AdjuntosGET /tasks/{id}/attachments · GET /files/{fileId} · subidas, más abajo
NotificacionesGET /notifications (unread) · PATCH /notifications/{id} (read) · POST /notifications/read-all
CompartirGET /boards/{id}/sharing · PATCH /boards/{id}/sharing (acceso, rol por defecto, enlace público) · POST /boards/{id}/members · PATCH/DELETE /boards/{id}/members/{userId} · GET /boards/{id}/invitations
MiembrosGET /workspaces/{slug}/members (role, q, include=invitations) · POST /workspaces/{slug}/members · PATCH/DELETE /workspaces/{slug}/members/{userId} · POST /workspaces/{slug}/leave
InvitacionesPOST /workspaces/{slug}/invitations (202) · GET /workspaces/{slug}/invitations · DELETE /invitations/{id} · GET /me/invitations · POST /me/invitations/{id}/accept · POST /me/invitations/{id}/decline
WebhooksGET/POST /workspaces/{slug}/webhooks · GET/PATCH/DELETE …/webhooks/{id} · POST …/{id}/rotate-secret · POST …/{id}/ping (202) · GET …/{id}/deliveries (status) · GET …/deliveries/{deliveryId} · POST …/deliveries/{deliveryId}/redeliver (202)

Registros de tiempo: started_at es un instante; el inicio se guarda al minuto, como los registros hechos en la app. Indica duration_sec (60 … 86 400) o ended_at. La visibilidad sigue las reglas de la app (Registro de tiempo). Los invitados reciben 404 en GET /time-entries, porque no tienen hojas de horas.

Comentarios: los invitados solo pueden usar los endpoints de comentarios (escribir, responder, editar y eliminar los suyos) donde los comentarios de invitados están activados; si no, 403.

Compartir, miembros e invitaciones​

Las mismas reglas que el diálogo Compartir, la página Miembros y el aviso de invitación (Miembros y compartir).

EndpointQuiénNotas
GET /boards/{id}/sharingcualquiera que pueda abrir el tableroacceso, rol por defecto, public_link.enabled; para los admins del tablero, también public_link.url, expires_at, password (true si hay una), visibility, allowed y blocked_by (plan o workspace). people: miembros explícitos del tablero y admins del espacio de trabajo (locked), con los invitados marcados
PATCH /boards/{id}/sharingadmins del tableroaccess, default_role, public_link: true crea / mantiene el enlace, false lo quita, o un objeto con la configuración (abajo)
POST /boards/{id}/membersadmins del tablero{user_id, role?, workspace_role?}; sin role: VIEWER para invitados, EDITOR para los demás. Los invitados reciben como máximo VIEWER (403 guest_viewer_only). Alguien del espacio de trabajo, o una cuenta de fuera que podrías elegir en la app (comparte un espacio de trabajo contigo): se añade al espacio de trabajo como workspace_role (GUEST; MEMBER requiere un admin del espacio de trabajo). Cualquier otra persona: 404, invítala por correo
PATCH / DELETE /boards/{id}/members/{userId}admins del tablerosolo miembros explícitos (si no, 404); el último admin del tablero se queda (409 last_board_admin); un invitado recibe como máximo VIEWER (403 guest_viewer_only)
GET /workspaces/{slug}/membersadmins y miembros del espacio de trabajo (invitados: 404)paginado con cursor, primero los admins; role, q, include=invitations (entonces siguen las invitaciones pendientes). managed = sincronización de grupos OIDC (solo lectura), automatic = concedido por ser admin de la instancia
POST /workspaces/{slug}/membersadmins del espacio de trabajo{user_ids, role}: solo personas que puedes ver (espacio de trabajo compartido; staff Propietario / Admin: todo el mundo); el resto se omite → {added}
PATCH / DELETE /workspaces/{slug}/members/{userId}admins del espacio de trabajono a ti mismo (403, usa leave), no filas gestionadas por OIDC (403), no el último admin explícito de un espacio de trabajo privado (409 last_private_admin)
POST /workspaces/{slug}/leavecualquier miembro409 last_admin_leave, last_private_admin, oidc_managed, instance_admin_leave (reglas)
POST /workspaces/{slug}/invitationsadmins del espacio de trabajo; admins del tablero para invitados con board_id{emails, role, board_id?, board_role?} → 202 {sent}. Los invitados reciben como máximo VIEWER (EDITOR / ADMIN: 403 guest_viewer_only); por encima del límite de invitados del plan: 409 plan_limit con kind guests
GET /workspaces/{slug}/invitations, GET /boards/{id}/invitationsadmins y miembros del espacio de trabajo (invitados: 404) / cualquiera del tableroinvitaciones pendientes con can_revoke, un elemento por invitación (alguien invitado al espacio y a dos tableros tiene tres)
DELETE /invitations/{id}admins del espacio de trabajo, quien la envió, admins de su tablero404 si no está pendiente
GET /me/invitations, POST /me/invitations/{id}/accept, …/declinetúse buscan solo por tu correo verificado; aceptar y rechazar responden solo a esa invitación (aceptar te une al espacio si hace falta y añade el tablero de esa invitación; tus otras invitaciones siguen pendientes); aceptar devuelve el espacio de trabajo

La configuración del enlace público (Compartir un tablero públicamente) va en un objeto public_link; todos los campos son opcionales:

CampoSignificado
enabledtrue / false: activar o desactivar el enlace
expires_atuna fecha y hora futuras (ISO 8601 con desfase), o null para que no caduque; una pasada da 422 expiry_past
passwordde 6 a 128 caracteres, o null para quitarla; una contraseña nueva o quitada cierra la sesión de todos los visitantes. Solo escritura: nunca se devuelve
regeneratetrue: un enlace nuevo; el anterior deja de funcionar al instante
visibilityqué ven los visitantes, p. ej. {"assignees": true, "time": false}; las claves que no incluyas se quedan como están. Claves: labels, dates, checklists, descriptions (activadas en enlaces nuevos), assignees, members, comments, attachments, time (desactivadas)

Todo lo que no sea desactivar un enlace se rechaza con 403 plan_feature si el plan no incluye enlaces públicos, y con 403 public_links_disabled_workspace si el espacio tiene Permitir enlaces públicos desactivado.

Privacidad, exactamente como en la app:

  • Correos: las direcciones de otras personas solo para el staff que ve correos (si no, email: null). Las direcciones de las invitaciones solo se muestran completas a ese staff, a los admins del espacio de trabajo y a quien envió la invitación; los demás reciben j•••@client.io (email_masked: true), y q solo busca en lo que ves, así que no sirve para sondear una dirección.
  • Cuentas: invitar responde siempre 202 {"sent": n}, tenga o no cuenta una dirección: no se añade a nadie directamente, cada dirección recibe el correo (y una notificación en la app si una cuenta verificada la tiene). POST /boards/{id}/members y POST /workspaces/{slug}/members solo llegan a personas que ya podrías elegir en la app.
  • Límites de peticiones: las invitaciones cuentan para invite (por quien invita) y invite-workspace (por espacio de trabajo), cada dirección una vez; las revocaciones, para invite-revoke. Son los mismos contadores que la app, además de api / api-write.
curl -H "$H" -H 'Content-Type: application/json' -X PATCH "$BASE/boards/$BOARD/sharing" -d '{"access":"RESTRICTED","public_link":false}'
curl -H "$H" -H 'Content-Type: application/json' -X PATCH "$BASE/boards/$BOARD/sharing" \
-d '{"public_link":{"enabled":true,"expires_at":"2026-12-31T23:59:59Z","visibility":{"assignees":true}}}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/boards/$BOARD/members" -d '{"user_id":"…","role":"EDITOR"}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/invitations" \
-d '{"emails":["[email protected]"],"role":"GUEST","board_id":"…","board_role":"VIEWER"}' # → 202 {"sent":1}
curl -H "$H" "$BASE/workspaces/northwind/members?include=invitations&limit=100"
curl -H "$H" -X POST "$BASE/me/invitations/$INVITE/accept"

Planes​

Donde se aplican planes, GET /api/v1/public/plans (sin clave) lista los planes públicos, y GET /organizations/{slug}/plan muestra a los propietarios y admins de una organización su plan, sus límites y su uso.

  • features indica lo que incluye un plan, entre otros guest_comments (los espacios pueden activar Los invitados pueden comentar) y email_notifications (desactivado: las notificaciones de los espacios de la organización son solo en la app).
  • limits.guests es el límite de invitados opcional (null = ilimitado). Los invitados nunca ocupan puesto.
  • Desde la API 1.5.0, guests_free y limits.guests_free están obsoletos: siempre true, se eliminarán en una versión posterior. No te bases en ellos.
  • highlights ya no incluyen "Guests don't use a seat".

Webhooks​

Los webhooks del espacio de trabajo (Webhooks: eventos, payloads, firmas, reintentos, reglas de direcciones) a través de la API: el mismo servicio que Ajustes → Espacio de trabajo → Webhooks. Solo admins del espacio de trabajo con una clave de acceso completo, también para GET (claves de solo lectura: 403 insufficient_scope; otros miembros: 403 forbidden); 403 webhooks_disabled mientras la instancia los tenga desactivados.

curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/webhooks" \
-d '{"url":"https://hooks.example.com/sol2flow","description":"CI","events":["task.created","comment.created"]}'
# → 201 {"id":"…","state":"active",…,"secret":"whsec_…"} (the secret is shown only here and by rotate-secret)
curl -H "$H" -H 'Content-Type: application/json' -X PATCH "$BASE/workspaces/northwind/webhooks/$HOOK" -d '{"active":false}'
curl -H "$H" -X POST "$BASE/workspaces/northwind/webhooks/$HOOK/ping" # → 202, the queued delivery
curl -H "$H" "$BASE/workspaces/northwind/webhooks/$HOOK/deliveries?status=failed"
curl -H "$H" -X POST "$BASE/workspaces/northwind/webhooks/$HOOK/deliveries/$D/redeliver"

ping y redeliver cuentan para webhook-manual (60 / 10 minutos por espacio de trabajo).

Importaciones​

Importación de tableros desde kan.bn y Trello (Importaciones: qué se importa y cómo), igual que en Ajustes → Espacio de trabajo → Importar y exportar. Solo admins del espacio de trabajo: POST necesita una clave de acceso completo, GET una clave de lectura. La ejecución es una tarea en segundo plano: POST responde 202 con la importación; consulta GET hasta que status sea DONE o FAILED. Una sola importación a la vez por espacio de trabajo (409 import_running). Las credenciales de origen viajan una vez en el cuerpo, se guardan cifradas solo mientras la ejecución las necesita y nunca se devuelven.

# Trello API (key + token) or kan.bn (api_key, base_url for a self-hosted Kan); boards = source board ids
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/imports" \
-d '{"source":"trello","api_key":"…","token":"…","boards":["5f1b…"],"access":"RESTRICTED"}' # → 202 {"id":"…","status":"PENDING",…}
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/imports" \
-d '{"source":"kanbn","base_url":"https://kan.example.com","api_key":"…","boards":["brd8Kq2mZx1a"],"people":{"mem1aB2cD3eF":"<user id>"}}'
# a Trello board JSON export (up to 50 MB) in `board`
jq -n --slurpfile b board.json '{source:"trello_file", board:$b[0]}' | curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/workspaces/northwind/imports" -d @-
curl -H "$H" "$BASE/workspaces/northwind/imports/$IMPORT" # status, counts, errors, results[].board_key

people asocia las claves de personas del origen con miembros del espacio de trabajo (null = dejar sin asignar); las personas que no aparecen se asocian automáticamente (correo → nombre de usuario → nombre completo único). No se invita a nadie. Volver a ejecutar una importación solo añade lo nuevo.

Subir un adjunto​

La misma subida por partes y reanudable que en la app:

# 1. start: the answer says how to split the file
curl -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"board_id":"…","task_id":"…","name":"report.pdf","size":1048576,"mime_type":"application/pdf"}' \
$BASE/uploads # → {"upload_id":"…","chunk_size":8388608,"part_count":1,"size":1048576}
# 2. each part n = 1…part_count: exactly chunk_size bytes (the last one: the rest)
curl -X PUT -H "Authorization: Bearer $KEY" -H 'Content-Type: application/octet-stream' \
--data-binary @report.pdf $BASE/uploads/$UPLOAD_ID/parts/1
# 3. finish: the file is attached to the task
curl -X POST -H "Authorization: Bearer $KEY" $BASE/uploads/$UPLOAD_ID/complete

GET /uploads/{id} muestra las partes que han llegado (para reanudar), DELETE /uploads/{id} cancela. Límites de tamaño: Administración → General → Subidas (413 file_too_large, max en MB).

Markdown​

Las descripciones de tareas, los comentarios y las respuestas se pueden escribir y leer en Markdown (GitHub-flavoured): la misma conversión que usa el editor al pegar Markdown, seguida de la misma limpieza que cualquier otra entrada.

  • Escritura: POST /tasks, PATCH /tasks/{id} (description) y POST /tasks/{id}/comments, PATCH /comments/{id} (text) aceptan "format": "markdown". Sin él ("text", el valor por defecto) una cadena es texto plano, un párrafo por línea, así que los clientes existentes no se ven afectados. Una description / doc en Tiptap JSON ignora format.
  • Lectura: ?format=markdown en GET /tasks/{id}, POST /tasks, PATCH /tasks/{id}, GET /tasks/{id}/comments (respuestas incluidas), POST /tasks/{id}/comments y PATCH /comments/{id} añade description_markdown (tareas; null sin descripción) o markdown (comentarios; null si está eliminado). description / doc y el texto plano no cambian.
  • Sintaxis: #/## → título grande, ###+ → título; **bold**, *italic*/_italic_, ~~strike~~, `code`, bloques de código delimitados con lenguaje, citas con >, listas -/*/1. (anidadas), listas de tareas - [ ]/- [x], ---, enlaces y enlaces automáticos, saltos de línea conservados (un solo salto de línea es un salto de línea). Las tablas se convierten en un párrafo por fila.
  • Menciones: @[Name](user:<user id>) de entrada y de salida; las mismas reglas que las menciones en JSON (solo las personas que pueden abrir el tablero siguen siendo menciones y reciben notificación; cualquier otra se convierte en el texto «@Name»).
  • Seguridad: el HTML sin procesar nunca se interpreta: <b>, <script>, <img onerror=…> se quedan como texto literal. Los enlaces deben ser http(s), mailto o relativos a la app (javascript: y similares conservan solo su texto); las imágenes solo como /api/files/<id> (las demás se convierten en enlaces). El subrayado no existe en Markdown y se descarta en la salida.
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/tasks?format=markdown" \
-d '{"board_id":"…","list_id":"…","title":"Release 1.2","format":"markdown","description":"## Steps\n- [ ] Changelog\n- [x] Tag **v1.2**"}'
# → { …, "description": {"type":"doc",…}, "description_text": "Steps\nChangelog\nTag v1.2", "description_markdown": "## Steps\n\n- [ ] Changelog\n- [x] Tag **v1.2**" }
curl -H "$H" "$BASE/tasks/$TASK/comments?format=markdown"

Ejemplos​

BASE=https://flow.example.com/api/v1; KEY=sf_…
H="Authorization: Bearer $KEY"

curl -H "$H" "$BASE/workspaces/northwind/boards?q=website"
curl -H "$H" "$BASE/tasks?workspace=northwind&assignee=$ME&due_to=2026-10-31&limit=100"
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/tasks" \
-d '{"board_id":"…","list_id":"…","title":"Write release notes","description":"Cover the API.\nLink the docs.","due_date":"2026-10-01"}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/tasks/$TASK/comments" -d '{"text":"Shipped 🎉"}'
curl -H "$H" -H 'Content-Type: application/json' -X POST "$BASE/time-entries" \
-d '{"task_id":"…","started_at":"2026-09-25T08:00:00Z","duration_sec":5400,"note":"Pairing"}'

Ajustes de la instancia​

Administración → General → API:

  • Permitir claves de API (activado por defecto). Desactivado: el elemento de menú «Claves de API» desaparece y todas las claves existentes reciben 403 api_disabled; al volver a activarlo, se recuperan (no se eliminan).
  • Permitir webhooks (activado por defecto) y Permitir webhooks a direcciones de red privadas (desactivado por defecto): Webhooks.
  • Documentación de la API (activado por defecto). Desactivado: /api/docs (Swagger UI y sus recursos) y /api/v1/openapi.json responden 404, por ejemplo para una instancia de producción que no debe anunciar su API. La API sigue funcionando.

La propia instancia sirve la Swagger UI (sin CDN externa), que sigue el tema oscuro / claro de la app. «Authorize» acepta una clave; después, «Try it out» llama a la API desde el navegador con ella.

Versionado​

  • La versión va en la ruta (/api/v1). Dentro de v1, los cambios son solo aditivos: nuevos endpoints, nuevos campos opcionales en las peticiones, nuevos campos en las respuestas, nuevos códigos de error para situaciones nuevas. Los clientes deben ignorar los campos desconocidos.
  • Quitar o renombrar un campo, cambiar un tipo o un código de estado, o endurecer la validación de un campo existente requiere /api/v2; v1 sigue funcionando entonces al menos 6 meses, con aviso en el changelog.
  • info.version en el documento OpenAPI sigue el versionado semántico para el contrato v1.
  • A partir de la API 1.8, cada respuesta bajo /api/v1 lleva la cabecera Sol2flow-Api-Version (por ejemplo 1.8.0), la misma versión que info.version, para que un cliente sepa qué admite la instancia incluso con la documentación de la API desactivada. Las instancias anteriores no envían esa cabecera.