# Spec escrita A MANO (estudio §2.5): la fuente de verdad del contrato son los # DTOs de convex/api/v1/mappers.ts y las reglas de convex/api/v1/validate.ts; # esta spec los transcribe. Validar con: npx @redocly/cli lint docs/api-publica/openapi.yaml openapi: 3.1.0 info: title: SecretarIA API version: 1.0.0 summary: API pública de SecretarIA para tareas y proyectos. description: | API REST para consumir SecretarIA desde scripts, backends e integraciones. - **Server-side only:** estas rutas no emiten cabeceras CORS a propósito; no son llamables desde JavaScript de navegador. Una API key en una web es una key filtrada. - **Versionado:** `v1` va en el path. Los cambios aditivos (campos o endpoints nuevos) no cambian la versión; tolera campos desconocidos. - **Errores:** shape único `{"error": {"code", "message"}}`. Programa contra `code` + status HTTP; `message` es para humanos y puede cambiar. - Guía con quickstart, autenticación, errores y límites: https://secretar-ia.org/docs contact: name: SecretarIA url: https://secretar-ia.org license: name: Propietaria url: https://secretar-ia.org externalDocs: description: Documentación de la API de SecretarIA url: https://secretar-ia.org/docs servers: - url: https://system.secretar-ia.org/api/v1 security: - bearerAuth: [] tags: - name: me description: Identidad de la API key. - name: tasks description: Tareas del usuario dueño de la key. - name: projects description: Proyectos del usuario dueño de la key. paths: /me: get: operationId: getMe x-slug: obtener-identidad tags: [me] summary: Identidad de la key description: | Sanity-check de la key: no exige ningún scope, basta que la key sea válida. Devuelve la identidad de la key (nunca su hash ni plaintext). responses: '200': description: Identidad de la key autenticada. content: application/json: schema: type: object required: [data] properties: data: $ref: '#/components/schemas/Me' example: data: userId: k1m8x2c5v9b3n7q0w4e6r1t8y2u5i3o9 keyId: n4r8t2w6y0u3i7o1p5a9s3d7f1g5h9j2 name: Script de informes prefix: sk_live_a3f9 scopes: ['tasks:read', 'tasks:write', 'projects:read'] '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' /tasks: get: operationId: listTasks x-slug: listar-tareas tags: [tasks] summary: Listar tareas description: | Requiere scope `tasks:read`. Incluye las tareas propias y las de proyectos compartidos donde el usuario es miembro activo. Orden estable por `(createdAt, id)` ascendente; el cursor es opaco y sobrevive a altas y bajas entre páginas. Para polling (Zapier/Make/cron), usa `updatedAfter` con el mayor `updatedAt` visto: el filtro es estricto (`updatedAt > updatedAfter`). Un `projectId` con formato de id inválido responde `422` con code `validation_error` (genérico, no `invalid_query_param`). parameters: - name: status in: query description: Filtra por estado de la tarea. schema: $ref: '#/components/schemas/TaskStatus' example: backlog - name: projectId in: query description: Filtra por proyecto (id de proyecto). schema: type: string example: kh72p9d3w1v8m4x6c0b5n2q7r9s3t1a8 # Ejemplo de otra cuenta: fuera de las muestras de /docs para que se puedan copiar tal cual. x-muestra: false - name: updatedAfter in: query description: Solo tareas con `updatedAt` estrictamente mayor (epoch ms). schema: type: integer minimum: 0 example: 1765804800000 - name: limit in: query description: Tamaño de página. schema: type: integer minimum: 1 maximum: 100 default: 50 example: 20 - name: cursor in: query description: Cursor opaco devuelto en `nextCursor` de la página anterior. schema: type: string example: '1765804800000:jd7f3k2m9q8w1x4v6b5n0c2a7s8e3r1t' # Ejemplo de otra cuenta: fuera de las muestras de /docs para que se puedan copiar tal cual. x-muestra: false responses: '200': description: Página de tareas. `nextCursor` `null` = no hay más páginas. content: application/json: schema: type: object required: [data, nextCursor] properties: data: type: array items: $ref: '#/components/schemas/Task' nextCursor: type: ['string', 'null'] description: Cursor opaco para la página siguiente. examples: tareas: $ref: '#/components/examples/ListaTareas' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' post: operationId: createTask x-slug: crear-tarea tags: [tasks] summary: Crear tarea description: | Requiere scope `tasks:write`. Defaults: `status: backlog`, `priority: medium`, `progress: 0`. **Idempotencia:** si se envía la cabecera `Idempotency-Key`, un reintento con la misma clave devuelve la tarea original (también con `201`) sin duplicarla. La clave se compara por usuario y el cuerpo NO se compara: misma clave con cuerpo distinto devuelve la tarea original. parameters: - name: Idempotency-Key in: header description: | Clave de idempotencia opcional (única por operación lógica). Máximo 128 caracteres, ASCII imprimible sin espacios; el prefijo `local:` está reservado. Inválida → `422 invalid_idempotency_key`. schema: type: string maxLength: 128 example: mi-script-2026-10-02-001 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TaskCreate' examples: simple: summary: Solo título y prioridad value: title: Revisar informe mensual priority: high recurrente: summary: Tarea semanal en un proyecto value: title: Revisar informe mensual priority: high notes: Comparar con el trimestre anterior. projectId: kh72p9d3w1v8m4x6c0b5n2q7r9s3t1a8 deadline: 1767225599999 deadlineType: date recurrence: frequency: weekly interval: 1 dayOfWeek: 1 responses: '201': description: Tarea creada (o la original, en un replay idempotente). content: application/json: schema: type: object required: [data] properties: data: $ref: '#/components/schemas/Task' examples: tarea: $ref: '#/components/examples/RespuestaTarea' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: El `projectId` asignado no existe. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: not_found message: Resource not found '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' /tasks/{id}: parameters: - name: id in: path required: true description: Id de la tarea. schema: type: string example: jd7f3k2m9q8w1x4v6b5n0c2a7s8e3r1t get: operationId: getTask x-slug: obtener-tarea tags: [tasks] summary: Obtener tarea description: | Requiere scope `tasks:read`. Las mismas tareas que ve el usuario en la app: las propias y las visibles de los proyectos compartidos donde es propietario/a o miembro activo. Una tarea sin acceso (incluida una privada de otra persona), inexistente o con id malformado responde el mismo `404` (no se filtra existencia). Hasta 2026-09 este endpoint solo servía tareas propias; las compartidas daban `404`. responses: '200': description: La tarea. content: application/json: schema: type: object required: [data] properties: data: $ref: '#/components/schemas/Task' examples: tarea: $ref: '#/components/examples/RespuestaTarea' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' patch: operationId: patchTask x-slug: actualizar-tarea tags: [tasks] summary: Modificar tarea description: | Requiere scope `tasks:write`. Las mismas tareas que GET (mismo `404` opaco): en un proyecto compartido cualquier miembro edita las visibles. En un proyecto archivado responde `409 project_archived`. El cuerpo debe traer al menos un campo (`422 empty_patch` si no). `assigneeId` asigna la tarea a una persona con acceso a su proyecto compartido (`null` la deja sin asignar). Se aplica en la misma operación que el resto de campos: si falla (`422 assignee_not_member`, `task_not_shared`, `task_private`), no se aplica nada. `status: done` fija `completedAt` y `progress: 100` en la primera compleción; si la tarea es recurrente, el servidor crea la siguiente ocurrencia. Volver a `backlog`/`in_progress` limpia `completedAt`. PATCH no borra `notes` ni `deadline`. `projectId: null` deja la tarea sin proyecto y `recurrence: null` hace que deje de repetirse (la serie se para; la tarea se conserva). Un patrón nuevo rige desde la siguiente ocurrencia: la tarea actual conserva su fecha. La recurrencia exige `deadline` y una tarea no completada (`422` si no). requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TaskPatch' examples: avance: summary: Marcar avance y asignar value: status: in_progress progress: 40 assigneeId: p3s7v1y5b9e2h6k0m4q8t2w6z0c4f8j1 completar: summary: Completar la tarea value: status: done dejarDeRepetir: summary: Quitar la recurrencia value: recurrence: null responses: '200': description: La tarea tras aplicar el patch. content: application/json: schema: type: object required: [data] properties: data: $ref: '#/components/schemas/Task' examples: tarea: $ref: '#/components/examples/RespuestaTarea' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/ProjectArchived' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' delete: operationId: deleteTask x-slug: borrar-tarea tags: [tasks] summary: Borrar tarea description: | Requiere scope `tasks:write`. Las mismas tareas que GET (mismo `404` opaco); en un proyecto archivado responde `409 project_archived`. El borrado es definitivo para la API (el undo existe solo dentro de la app). Si la tarea pertenece a una serie recurrente (`recurrence` o `seriesId` de una serie activa), `scope` es obligatorio: sin él responde `409 recurring_scope_required` y no borra nada. parameters: - name: scope in: query required: false description: | Solo para tareas recurrentes. `occurrence` = solo esta (la serie sigue y se genera la siguiente); `series` = toda la serie (deja de repetirse y se borra la pendiente; las completadas se conservan). schema: type: string enum: [occurrence, series] example: occurrence responses: '204': description: Tarea borrada. Sin cuerpo. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' /projects: get: operationId: listProjects x-slug: listar-proyectos tags: [projects] summary: Listar proyectos description: | Requiere scope `projects:read`. Incluye proyectos propios y compartidos como miembro activo, ordenados por nombre. Sin paginación a propósito (la colección por usuario es pequeña): la respuesta no lleva `nextCursor`. parameters: - name: status in: query description: Filtra por estado del proyecto. schema: $ref: '#/components/schemas/ProjectStatus' example: active responses: '200': description: Todos los proyectos accesibles. content: application/json: schema: type: object required: [data] properties: data: type: array items: $ref: '#/components/schemas/Project' examples: proyectos: $ref: '#/components/examples/ListaProyectos' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' post: operationId: createProject x-slug: crear-proyecto tags: [projects] summary: Crear proyecto description: | Requiere scope `projects:write`. **Idempotente por nombre:** si ya existe un proyecto del usuario con ese `name` exacto, devuelve el existente (también con `201`) y el resto de campos enviados se ignoran (no se actualiza nada). `status` es siempre `active` al crear. requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProjectCreate' example: name: Mudanza description: Todo lo de la casa nueva. color: verde icon: casa tipo: personal responses: '201': description: Proyecto creado (o el existente con el mismo nombre). content: application/json: schema: type: object required: [data] properties: data: $ref: '#/components/schemas/Project' examples: proyecto: $ref: '#/components/examples/RespuestaProyecto' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: sk_live_… description: | `Authorization: Bearer sk_live_…` con la API key completa (se crea en Ajustes → API de la app y se muestra una sola vez). Scopes: `tasks:read`, `tasks:write`, `projects:read`, `projects:write`. Cualquier problema con la key (inexistente, revocada, expirada, API deshabilitada) responde el mismo `401 invalid_api_key`. schemas: TaskStatus: type: string enum: [backlog, in_progress, done] TaskPriority: type: string enum: [high, medium, low] DeadlineType: type: string description: Granularidad del deadline (`deadline` = fin o fecha exacta, `deadlineStart` = inicio opcional). enum: [exact, date, week, month, custom] RecurrenceFrequency: type: string enum: [daily, weekly, monthly, yearly] Recurrence: type: object description: Regla de recurrencia de la tarea. Los campos opcionales pueden estar ausentes. required: [frequency] properties: frequency: $ref: '#/components/schemas/RecurrenceFrequency' interval: type: number minimum: 1 maximum: 99 dayOfWeek: type: number minimum: 0 maximum: 6 description: 0 = domingo … 6 = sábado. dayOfMonth: type: number minimum: -1 maximum: 31 description: -1 = último día del mes; si no, 1..31. RecurrenceInput: type: object description: | Regla de recurrencia al crear o en PATCH. Campos no listados → `422 unknown_field`. Los valores deben ser enteros; `dayOfMonth` admite -1 (último día) o 1..31, nunca 0. additionalProperties: false required: [frequency] properties: frequency: $ref: '#/components/schemas/RecurrenceFrequency' interval: type: integer minimum: 1 maximum: 99 dayOfWeek: type: integer minimum: 0 maximum: 6 dayOfMonth: type: integer minimum: -1 maximum: 31 Task: type: object description: | Tarea. Todos los campos están siempre presentes; los sin valor van como `null` (nunca se omiten). Timestamps en epoch milisegundos UTC. required: - id - title - status - priority - progress - notes - projectId - deadline - deadlineStart - deadlineType - recurrence - seriesId - createdAt - updatedAt - completedAt - createdBy - assignee properties: id: type: string title: type: string status: $ref: '#/components/schemas/TaskStatus' priority: $ref: '#/components/schemas/TaskPriority' progress: type: ['number', 'null'] minimum: 0 maximum: 100 notes: type: ['string', 'null'] projectId: type: ['string', 'null'] deadline: type: ['number', 'null'] description: Fin del intervalo o fecha exacta (epoch ms). deadlineStart: type: ['number', 'null'] description: Inicio del intervalo (epoch ms). deadlineType: oneOf: - $ref: '#/components/schemas/DeadlineType' - type: 'null' recurrence: oneOf: - $ref: '#/components/schemas/Recurrence' - type: 'null' description: Solo la lleva la ocurrencia pendiente de una serie; las completadas van con `null`. seriesId: type: ['string', 'null'] description: Serie recurrente a la que pertenece (pendiente o completada); `null` si nunca se repitió. createdAt: type: number updatedAt: type: number completedAt: type: ['number', 'null'] createdBy: $ref: '#/components/schemas/Person' assignee: oneOf: - $ref: '#/components/schemas/Person' - type: 'null' description: Persona asignada en un proyecto compartido; `null` = sin asignar. Person: type: object description: Cuenta de SecretarIA. Sin correo a propósito. required: [id, name] properties: id: type: string name: type: ['string', 'null'] description: Nombre de pila; `null` si la cuenta ya no existe. TaskCreate: type: object description: Campos no listados → `422 unknown_field`. additionalProperties: false required: [title] properties: title: type: string description: No puede ser vacío (tras trim). minLength: 1 priority: $ref: '#/components/schemas/TaskPriority' default: medium status: $ref: '#/components/schemas/TaskStatus' default: backlog notes: type: string projectId: type: ['string', 'null'] description: Id de proyecto propio o compartido; `null` = sin proyecto. Proyecto ajeno → `403`; inexistente → `404`. deadline: type: number minimum: 0 deadlineStart: type: number minimum: 0 deadlineType: $ref: '#/components/schemas/DeadlineType' recurrence: $ref: '#/components/schemas/RecurrenceInput' TaskPatch: type: object description: | Al menos un campo (`422 empty_patch` si no). Campos no listados → `422 unknown_field`. Solo `projectId`, `recurrence` y `assigneeId` aceptan `null` (sin proyecto / deja de repetirse / sin asignar). additionalProperties: false minProperties: 1 properties: title: type: string minLength: 1 notes: type: string projectId: type: ['string', 'null'] deadline: type: number minimum: 0 deadlineStart: type: number minimum: 0 deadlineType: $ref: '#/components/schemas/DeadlineType' priority: $ref: '#/components/schemas/TaskPriority' progress: type: number minimum: 0 maximum: 100 status: $ref: '#/components/schemas/TaskStatus' recurrence: oneOf: - $ref: '#/components/schemas/RecurrenceInput' - type: 'null' assigneeId: type: ['string', 'null'] description: Id de la cuenta (`Person.id`) de alguien con acceso al proyecto compartido de la tarea; `null` = sin asignar. ProjectStatus: type: string enum: [active, archived] ProjectTipo: type: string enum: [trabajo, ocio, personal] Project: type: object description: | Proyecto. Todos los campos están siempre presentes; los sin valor van como `null`. Timestamps en epoch milisegundos UTC. required: - id - name - description - color - icon - tipo - status - shared - role - createdAt - updatedAt properties: id: type: string name: type: string description: type: ['string', 'null'] color: type: ['string', 'null'] icon: type: ['string', 'null'] tipo: oneOf: - $ref: '#/components/schemas/ProjectTipo' - type: 'null' status: $ref: '#/components/schemas/ProjectStatus' shared: type: boolean description: Tiene al menos un miembro activo además del propietario/a. role: type: string enum: [owner, member] description: Rol de la persona dueña de la key en el proyecto. createdAt: type: number updatedAt: type: number ProjectCreate: type: object description: Campos no listados → `422 unknown_field`. additionalProperties: false required: [name] properties: name: type: string description: No puede ser vacío (tras trim). Clave de idempotencia por usuario. minLength: 1 description: type: string color: type: string icon: type: string tipo: $ref: '#/components/schemas/ProjectTipo' Me: type: object required: [userId, keyId, name, prefix, scopes] properties: userId: type: string keyId: type: string name: type: string description: Etiqueta de la key elegida por el usuario al crearla. prefix: type: string description: Primeros caracteres de la key (p. ej. `sk_live_a3f9`), lo único visible tras la creación. scopes: type: array items: type: string enum: ['tasks:read', 'tasks:write', 'projects:read', 'projects:write'] Error: type: object required: [error] properties: error: type: object required: [code, message] properties: code: type: string description: Identificador estable del error (programar contra este + status). enum: - invalid_api_key - insufficient_scope - forbidden - not_found - invalid_json - unknown_field - missing_field - invalid_field - invalid_query_param - empty_patch - body_too_large - invalid_idempotency_key - validation_error - recurring_scope_required - project_archived - calendar_creator_only - assignee_not_member - task_not_shared - task_private - rate_limited - internal_error # Una frase por código: la tabla de /docs/errores la pinta en la columna «Cuándo». x-enum-descriptions: invalid_api_key: Falta la clave, está mal formada, no existe, está revocada o ha caducado. insufficient_scope: La clave no tiene el permiso que pide el endpoint. forbidden: El recurso existe pero no tienes acceso a él. not_found: El recurso no existe, no tienes acceso o el id está mal formado. invalid_json: El cuerpo no es JSON o no es un objeto. unknown_field: El cuerpo trae un campo que el endpoint no acepta. missing_field: Falta un campo obligatorio. invalid_field: Un campo tiene un tipo o un valor no válido. invalid_query_param: Un parámetro de la URL tiene un tipo o un valor no válido. empty_patch: Un `PATCH` sin ningún campo que cambiar. body_too_large: El cuerpo pasa de 64 KB. invalid_idempotency_key: La `Idempotency-Key` es demasiado larga o tiene un formato no válido. validation_error: Validación general, como un `projectId` mal formado o una recurrencia sin `deadline`. recurring_scope_required: '`DELETE` de una tarea recurrente sin `?scope=occurrence|series`. No se borra nada.' project_archived: La tarea está en un proyecto archivado, que es de solo lectura. No se aplica nada. calendar_creator_only: Cambias la sincronización con el calendario de una tarea que creó otra persona. assignee_not_member: El `assigneeId` no es de alguien con acceso al proyecto de la tarea. task_not_shared: Asignas una tarea que no está en un proyecto compartido. task_private: Asignas una tarea privada, creada por una integración. rate_limited: Has superado los límites de uso; espera lo que indica `Retry-After`. internal_error: Error del servidor. Reintenta con espera creciente. message: type: string description: Texto para humanos; NO es estable, puede cambiar sin aviso. examples: RespuestaTarea: summary: Tarea completa value: data: id: jd7f3k2m9q8w1x4v6b5n0c2a7s8e3r1t title: Revisar informe mensual status: in_progress priority: high progress: 40 notes: Comparar con el trimestre anterior. projectId: kh72p9d3w1v8m4x6c0b5n2q7r9s3t1a8 deadline: 1767225599999 deadlineStart: null deadlineType: date recurrence: frequency: weekly interval: 1 dayOfWeek: 1 seriesId: m9c2x6v0b4n8q3w7e1r5t9y3u7i1o5p2 createdAt: 1765804800000 updatedAt: 1766063400000 completedAt: null createdBy: id: k1m8x2c5v9b3n7q0w4e6r1t8y2u5i3o9 name: Ana assignee: id: p3s7v1y5b9e2h6k0m4q8t2w6z0c4f8j1 name: Bruno ListaTareas: summary: Página de tareas value: data: - id: jd7f3k2m9q8w1x4v6b5n0c2a7s8e3r1t title: Revisar informe mensual status: in_progress priority: high progress: 40 notes: Comparar con el trimestre anterior. projectId: kh72p9d3w1v8m4x6c0b5n2q7r9s3t1a8 deadline: 1767225599999 deadlineStart: null deadlineType: date recurrence: frequency: weekly interval: 1 dayOfWeek: 1 seriesId: m9c2x6v0b4n8q3w7e1r5t9y3u7i1o5p2 createdAt: 1765804800000 updatedAt: 1766063400000 completedAt: null createdBy: id: k1m8x2c5v9b3n7q0w4e6r1t8y2u5i3o9 name: Ana assignee: id: p3s7v1y5b9e2h6k0m4q8t2w6z0c4f8j1 name: Bruno nextCursor: '1765804800000:jd7f3k2m9q8w1x4v6b5n0c2a7s8e3r1t' RespuestaProyecto: summary: Proyecto completo value: data: id: kh72p9d3w1v8m4x6c0b5n2q7r9s3t1a8 name: Mudanza description: Todo lo de la casa nueva. color: verde icon: casa tipo: personal status: active shared: true role: owner createdAt: 1765200000000 updatedAt: 1765804800000 ListaProyectos: summary: Proyectos accesibles value: data: - id: kh72p9d3w1v8m4x6c0b5n2q7r9s3t1a8 name: Mudanza description: Todo lo de la casa nueva. color: verde icon: casa tipo: personal status: active shared: true role: owner createdAt: 1765200000000 updatedAt: 1765804800000 - id: q8w2e6r0t4y8u2i6o0p4a8s2d6f0g4h8 name: Equipo de ventas description: null color: null icon: null tipo: trabajo status: active shared: true role: member createdAt: 1764000000000 updatedAt: 1765300000000 responses: Unauthorized: description: Key ausente, malformada, inexistente, revocada, expirada o API deshabilitada (`invalid_api_key`, indistinguibles a propósito). content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: invalid_api_key message: Invalid API key Forbidden: description: Scope insuficiente (`insufficient_scope`), recurso sin acceso (`forbidden`) o sincronización de calendario de una tarea que creó otra persona (`calendar_creator_only`). content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: insufficient_scope message: 'Missing required scope: tasks:write' NotFound: description: Recurso inexistente, de otro usuario o id malformado (`not_found`, indistinguibles a propósito). content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: not_found message: Resource not found Unprocessable: description: Entrada inválida (`invalid_json`, `unknown_field`, `missing_field`, `invalid_field`, `invalid_query_param`, `empty_patch`, `body_too_large` — cuerpos de más de 64 KB —, `invalid_idempotency_key`, `validation_error`, y al asignar `assignee_not_member`, `task_not_shared`, `task_private`). content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: missing_field message: 'Missing field: title' ProjectArchived: description: El proyecto de la tarea está archivado y es de solo lectura (`project_archived`). No se aplicó nada. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: project_archived message: The project is archived and read-only Conflict: description: Tarea recurrente sin `?scope=occurrence|series` (`recurring_scope_required`) o proyecto archivado (`project_archived`). No se borró nada. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: recurring_scope_required message: 'Task is recurring: pass ?scope=occurrence (only this one) or ?scope=series (the whole series)' RateLimited: description: | Límite superado (`rate_limited`) — 60 req/min (ráfagas de 20) y 5.000 req/día por key. Reintentar tras `Retry-After`. headers: Retry-After: description: Segundos (enteros) a esperar antes de reintentar. schema: type: integer minimum: 1 content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: rate_limited message: Rate limit exceeded. Retry later. InternalError: description: Error interno (`internal_error`). Cuerpo opaco a propósito; reintentar con backoff. content: application/json: schema: $ref: '#/components/schemas/Error' example: error: code: internal_error message: Internal server error