PJT AI REST API
Una API REST estándar para integrar los datos de PJT AI con sistemas externos. Todas las solicitudes y respuestas son JSON, y la URL base es https://api.pjt.ai/api/external/v1.
- Autenticación — cabecera X-API-Key (API key de cuenta, prefijo pjt_)
- Ámbitos de la clave — read (solo lectura, por defecto) / write. POST·PUT·DELETE requieren una clave con ámbito write — de lo contrario 403 SCOPE_FORBIDDEN
- Límite — 60 req/min + 10 000 req/mes por clave (Enterprise a convenir)
- Códigos de respuesta — 2xx éxito, 4xx error del cliente, 5xx error del servidor
- Fechas — todas las marcas de tiempo son ISO 8601 (UTC)
Autenticación
Cada solicitud requiere la cabecera X-API-Key: <API_KEY>. Emite claves en Configuración de cuenta > Claves API (se muestran solo una vez al crearlas).
/meMi API key (prueba de conexión)
Devuelve la validez y las capacidades (scopes) de la API key. Al ser un GET, no necesita scope write — incluso una key de solo lectura puede consultar sus capacidades. Una key ausente/inválida/caducada devuelve 401, que es en sí el resultado de la prueba de conexión. Usa canProvision/canWrite/canRead de la respuesta para comprobar la capacidad de antemano (p. ej. si canProvision=false, avisa antes de una llamada de aprovisionamiento → evita un falso semáforo verde). Es distinto del liveness de infraestructura (si el servidor está activo).
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
UNAUTHORIZEDX-API-Key ausente, inválida o caducada — este 401 es en sí la señal de 'sin conexión' (200 = conectado).{
"timestamp": "2026-07-14T09:00:00Z",
"message": "API key is missing, invalid, or expired."
}/provisioning/workspacesSolo EnterpriseAprovisionar workspace (idempotente)
Aprovisiona un tenant y un workspace en una llamada con una API key de alcance provisioning. Envía tenantId para usar ese tenant (la cuenta de la key debe ser OWNER/ADMIN); si no, tenantExternalRef busca/crea de forma idempotente un tenant en el espacio de nombres de la cuenta (400 si no envías ninguno). workspaceExternalRef es la clave de idempotencia obligatoria — las repeticiones devuelven el recurso existente (200) en vez de crear (201); createdTenant/createdWorkspace los distinguen. Jerarquía de alcance: provisioning ⊃ write (no hace falta write aparte). ⚠️ businessType aún no afecta el resultado (solo auditoría); defaultLocale/accentHue/template se validan de forma laxa (envía valores exactos; un template desconocido se ignora); tenant.myRole en la respuesta puede ser null — no lo uses para autorización.
Este endpoint solo está disponible en el plan Enterprise. La emisión y el uso de keys con alcance de aprovisionamiento se incluyen en un contrato Enterprise.
| Nombre | Tipo | Descripción |
|---|---|---|
| tenantId | integer | ID de tenant existente (Long, opcional). Si se envía, se ignoran tenant·tenantExternalRef. La cuenta de la key debe ser OWNER/ADMIN de ese tenant |
| tenantExternalRef | string | Clave de idempotencia del nuevo tenant (≤100, cuando no hay tenantId). El mismo (cuenta, valor) reutiliza el tenant existente |
| tenant.name | string | Nombre del nuevo tenant (≤100, usa workspace.name si se omite) |
| tenant.slug | string | Slug del nuevo tenant (≤50, único global·autogenerado si se omite) |
| tenant.description | string | Descripción del nuevo tenant (≤500) |
| workspaceExternalRefREQUIRED | string | Clave de idempotencia del workspace (≤100, obligatoria). El mismo (tenantId, valor) reutiliza el workspace existente |
| workspace.nameREQUIRED | string | Nombre del workspace (≤100, obligatorio) |
| workspace.slug | string | Slug del workspace (≤50, único dentro del tenant·derivado del nombre si se omite) |
| workspace.description | string | Descripción del workspace (≤500) |
| workspace.defaultLocale | string | Locale por defecto — ko|en|ja|zh|zh-TW|es|vi|th (sin validar; envía el valor exacto) |
| workspace.accentHue | integer | Tono de acento (0–360, sin validar) |
| workspace.template | string | Preset — BLANK|DEV|AGENCY|OPS (los valores desconocidos se ignoran) |
| workspace.businessType | string | Tipo de negocio (a futuro) — solo auditoría; no se refleja en el workspace creado |
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
SCOPE_FORBIDDENEscritura intentada con una key de solo lectura (sin alcance write) — bloqueada por el filtro del gateway{
"error": "SCOPE_FORBIDDEN",
"message": "This API key is read-only. A 'write' scope is required for this operation."
}FORBIDDENKey sin el alcance provisioning — este endpoint requiere provisioning (write por sí solo no basta){
"timestamp": "2026-07-14T09:00:00Z",
"message": "The 'provisioning' scope is required."
}FORBIDDENCuando se indica un tenantId existente, la cuenta que llama no es OWNER/ADMIN (o no es miembro) de ese tenant{
"timestamp": "2026-07-14T09:00:00Z",
"message": "OWNER or ADMIN role on the tenant is required."
}BAD_REQUESTNo se envió ni tenantId ni tenantExternalRef{
"timestamp": "2026-07-14T09:00:00Z",
"message": "One of tenantId or tenantExternalRef is required."
}VALIDATION_ERRORValidación fallida (obligatorio/longitud, etc.) — detalles por campo en errors[]{
"timestamp": "2026-07-14T09:00:00Z",
"code": "VALIDATION_ERROR",
"message": "workspaceExternalRef: must not be blank",
"errors": [
{ "field": "workspaceExternalRef", "code": "NotBlank", "message": "must not be blank" }
]
}UNAUTHORIZEDX-API-Key ausente, inválida o caducada (común a todos los endpoints External){
"timestamp": "2026-07-14T09:00:00Z",
"message": "API key is missing, invalid, or expired."
}/tenantsMis tenants
Devuelve todos los tenants a los que pertenece la cuenta de la API key (miembros directos del tenant + organizaciones alcanzables solo a través de un workspace = MEMBER). Al ser un GET, basta con un alcance read. Determina los permisos con el myRole de cada elemento (OWNER|ADMIN|MEMBER) — crear un workspace (POST /workspaces) solo es posible en tenants donde myRole ∈ [OWNER, ADMIN], por lo que un cliente SI elige aquí el id de un elemento OWNER/ADMIN y lo usa como tenantId.
| Nombre | Tipo | Descripción |
|---|---|---|
| id | Long | ID del tenant — úsalo como tenantId en POST /workspaces |
| myRole | String (enum) | OWNER | ADMIN | MEMBER (null si no eres miembro). La creación de workspaces (POST /workspaces) solo se permite en tenants donde eres OWNER o ADMIN |
| status | String (enum) | Estado del tenant — ACTIVE | ARCHIVED | DELETE (Tenant.Status) |
| slug | String | Slug de la organización (para el enrutado) |
| mfaSetupRequired | boolean | true si la organización exige 2FA y aún no está configurado |
| slugSet | boolean | Si el usuario definió el slug manualmente (false para org-xxxx autogenerado) |
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
UNAUTHORIZEDX-API-Key ausente, inválida o caducada (común a todos los endpoints External){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/tenants/{tenantId}Tenant individual
Devuelve un tenant con la misma shape que la lista. Si no perteneces a él, la verificación de acceso falla (403); un tenant inexistente devuelve 400 tenant.not_found. Basta con un alcance read.
| Nombre | Tipo | Descripción |
|---|---|---|
| tenantIdREQUIRED | integer | ID del tenant a consultar (Long, obligatorio) |
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
FORBIDDENLa cuenta que llama no es miembro de ese tenant (verificación de acceso al tenant fallida){
"timestamp": "2026-07-14T20:00:00Z",
"message": "테넌트 멤버가 아닙니다"
}BAD_REQUESTTenant inexistente (tenant.not_found){
"timestamp": "2026-07-14T20:00:00Z",
"message": "테넌트를 찾을 수 없습니다"
}UNAUTHORIZEDX-API-Key ausente, inválida o caducada (común a todos los endpoints External){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspacesCrear workspace (tenant existente)
Un camino ligero separado del aprovisionamiento — NO crea un tenant; crea solo un workspace bajo un tenant existente (tenantId). Basta con un alcance write (no requiere provisioning; las keys de solo lectura reciben 403 SCOPE_FORBIDDEN). La cuenta de la key debe ser OWNER/ADMIN de ese tenant. Si envías externalRef es idempotente — repetir con el mismo (tenantId, externalRef) devuelve el workspace existente como 200 en vez de crear uno nuevo (201); si se omite, se crea un workspace nuevo en cada llamada (se recomienda enviarlo para reintentos seguros). El workspace creado es un workspace de equipo (no personal). ⚠️ defaultLocale/accentHue/template se validan de forma laxa (los valores pasan tal cual; un template no reconocido se ignora). Si necesitas que el tenant se cree automáticamente, usa el aprovisionamiento (POST /provisioning/workspaces).
| Nombre | Tipo | Descripción |
|---|---|---|
| tenantIdREQUIRED | integer | Crear bajo este tenant (Long, obligatorio). La cuenta de la key debe ser OWNER/ADMIN de él |
| externalRef | string | Clave de idempotencia (≤100, opcional). Repetir con el mismo (tenantId, valor) devuelve el workspace existente. Omitir → un workspace nuevo en cada llamada |
| nameREQUIRED | string | Nombre del workspace (≤100, obligatorio) |
| slug | string | Slug del workspace (≤50, único dentro del tenant·derivado del nombre si se omite) |
| description | string | Descripción del workspace (≤500) |
| defaultLocale | string | Locale por defecto — ko|en|ja|zh|zh-TW|es|vi|th (pasa sin validar; envía el valor exacto) |
| accentHue | integer | Tono de acento (0–360, pasa sin validar) |
| template | string | Preset — BLANK|DEV|AGENCY|OPS (los valores no reconocidos se ignoran en silencio) |
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
SCOPE_FORBIDDENEscritura intentada con una key de solo lectura (sin alcance write) — bloqueada por el filtro del gateway{
"error": "SCOPE_FORBIDDEN",
"message": "This API key is read-only. A 'write' scope is required for this operation."
}FORBIDDENLa cuenta que llama no es OWNER/ADMIN del tenant tenantId (tenant.admin_required){
"timestamp": "2026-07-14T20:00:00Z",
"message": "관리자 권한이 필요합니다"
}FORBIDDENLa cuenta que llama no es miembro de ese tenant (incluidos tenants inexistentes) (tenant.not_member){
"timestamp": "2026-07-14T20:00:00Z",
"message": "테넌트 멤버가 아닙니다"
}VALIDATION_ERRORValidación fallida (falta tenantId/name, etc.) — detalles por campo en errors[]{
"timestamp": "2026-07-14T20:00:00Z",
"code": "VALIDATION_ERROR",
"message": "tenantId: must not be null",
"errors": [
{ "field": "tenantId", "code": "NotNull", "message": "must not be null" }
]
}UNAUTHORIZEDX-API-Key ausente, inválida o caducada (común a todos los endpoints External){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspaces/slug/availableComprobar disponibilidad del slug de workspace
Comprueba si un slug de workspace está disponible (no ocupado) dentro de un tenant. Como el slug es único por (tenantId, slug), se requiere tenantId igual que en la creación de un workspace. El slug se normaliza igual que al guardar (recorte de espacios, minúsculas) antes de comparar, y un valor vacío o duplicado devuelve available:false. La autorización usa la misma verificación que la creación de un workspace: quien llama debe ser OWNER/ADMIN de ese tenant (requireTenantAdmin); los no miembros y no administradores reciben 403 y no se revela la existencia del tenant. Al ser un GET, basta con un scope de lectura. Usa esto para comprobar la disponibilidad del slug antes de crear un workspace (POST /workspaces).
| Nombre | Tipo | Descripción |
|---|---|---|
| tenantIdREQUIRED | integer | Ámbito del tenant para la comprobación de unicidad (Long, query obligatorio). Ámbito de unicidad del slug. Quien llama debe ser OWNER/ADMIN de este tenant |
| slugREQUIRED | string | Slug de workspace a comprobar (query, obligatorio). Se compara tras normalizar (trim, minúsculas) |
| Nombre | Tipo | Descripción |
|---|---|---|
| available | boolean | true = disponible (no ocupado), false = ya en uso o slug vacío |
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
FORBIDDENQuien llama no es OWNER/ADMIN del tenant tenantId (incluidos los no miembros) — bloqueado por requireTenantAdmin (no se revela la existencia del tenant){
"timestamp": "2026-07-14T20:00:00Z",
"message": "관리자 권한이 필요합니다"
}UNAUTHORIZEDX-API-Key ausente, inválida o caducada (común a la API External){
"timestamp": "2026-07-14T20:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspacesListar workspaces
Devuelve los workspaces accesibles para la cuenta de la API key.
/workspaces/{workspaceId}Obtener workspace
Devuelve un workspace.
| Nombre | Tipo | Descripción |
|---|---|---|
| workspaceIdREQUIRED | integer | ID del workspace |
/projectsListar proyectos por tenant y workspace
Devuelve los proyectos del tenant y workspace indicados, paginados. tenantId y workspaceId son obligatorios; status permite filtrar más.
| Nombre | Tipo | Descripción |
|---|---|---|
| tenantIdREQUIRED | integer | ID del tenant (obligatorio) |
| workspaceIdREQUIRED | integer | ID del workspace (obligatorio) |
| status | enum | Coincidencia exacta del estado del proyecto (PLANNING, ESTIMATING, WAITING, IN_PROGRESS, ON_HOLD, COMPLETED, CANCELLED). Si se indica, también incluye cancelados/archivados. |
| page | integer | Número de página (desde 0) |
| size | integer | Tamaño de página (por defecto 20, máximo 100) |
El campo message se devuelve en el locale de la solicitud (?lang o Accept-Language) en 8 idiomas.
BAD_REQUESTEl workspace no existe o no pertenece al tenant (cross-tenant se devuelve como not_found sin revelar su existencia).{
"timestamp": "2026-07-14T09:00:00Z",
"message": "워크스페이스를 찾을 수 없습니다"
}BAD_REQUESTSe pasó un valor no admitido en status.{
"timestamp": "2026-07-14T09:00:00Z",
"message": "유효하지 않은 프로젝트 상태 값입니다"
}FORBIDDENSin acceso a este workspace (no es miembro ACTIVE ni cliente/partner aceptado — sin fallback de membresía del tenant).{
"timestamp": "2026-07-14T09:00:00Z",
"message": "워크스페이스 접근 권한이 없습니다"
}UNAUTHORIZEDX-API-Key ausente, inválida o expirada.{
"timestamp": "2026-07-14T09:00:00Z",
"message": "API key is missing, invalid, or expired."
}/workspaces/{workspaceId}/projectsListar proyectos
Devuelve los proyectos activos del workspace (excluye cancelados/archivados).
| Nombre | Tipo | Descripción |
|---|---|---|
| workspaceIdREQUIRED | integer | ID del workspace |
/projectsCrear proyecto (idempotente)
Crea un proyecto. Con externalRef la llamada es idempotente — repetir el mismo (workspaceId, externalRef) devuelve el proyecto existente con 200 en lugar de crear uno nuevo (201 en la primera creación).
| Nombre | Tipo | Descripción |
|---|---|---|
| workspaceIdREQUIRED | integer | ID del workspace |
| nameREQUIRED | string | Nombre del proyecto |
| externalRef | string | Clave de idempotencia — repetir con el mismo valor devuelve el proyecto existente con 200 en lugar de crear uno nuevo |
| code | string | Código del proyecto (prefijo de clave de tarea). Se genera automáticamente si se omite |
| description | string | Descripción |
| clientIds | array | Array de IDs de clientes |
| managerId | integer | ID de cuenta del manager |
| startDate | date | Fecha de inicio — ISO 8601 (YYYY-MM-DD) |
| endDate | date | Fecha de fin — ISO 8601 (YYYY-MM-DD) |
| budget | integer | Presupuesto |
/projects/{projectId}Detalle / progreso del proyecto
Devuelve el detalle del proyecto: estado, progreso (%), fechas planificadas/reales y última modificación.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
/projects/{projectId}Actualizar proyecto
Actualiza solo los campos enviados (actualización parcial).
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
| Nombre | Tipo | Descripción |
|---|---|---|
| name | string | Nombre del proyecto |
| status | enum | Estado — PLANNING·ESTIMATING·WAITING·IN_PROGRESS·ON_HOLD·COMPLETED·CANCELLED |
| progressRate | integer | Progreso (%) 0–100 |
| startDate | date | Fecha de inicio — ISO 8601 (YYYY-MM-DD) |
| endDate | date | Fecha de fin — ISO 8601 (YYYY-MM-DD) |
/projects/{projectId}Eliminar proyecto
Elimina un proyecto. Devuelve 204 No Content si tiene éxito (sin cuerpo).
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
/projects/{projectId}/timelineTimeline (hitos)
Hitos del proyecto — nombre, estado (PLANNED/IN_PROGRESS/COMPLETED), fecha límite, fecha de finalización y progreso. Ordenados por sortOrder.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
/projects/{projectId}/activitiesActividades
Eventos de cambio de tareas del proyecto, del más reciente al más antiguo — tipo, mensaje, actor y hora.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
| Nombre | Tipo | Descripción |
|---|---|---|
| page | integer | Número de página (desde 0, por defecto 0) |
| size | integer | Tamaño de página (por defecto 50, máx. 200) |
/projects/{projectId}/filesArchivos
Adjuntos de tareas y comentarios del proyecto combinados, del más reciente al más antiguo. fileUrl es una URL estática sin expiración.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
| Nombre | Tipo | Descripción |
|---|---|---|
| limit | integer | Máximo de elementos (por defecto 100, máx. 500) |
/projects/{projectId}/tasksListar tareas
Devuelve las tareas de un proyecto. Usa /tasks/paged si necesitas paginación.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
| Nombre | Tipo | Descripción |
|---|---|---|
| sortBy | string | Campo de ordenación (por defecto createdAt) |
/tasksCrear tarea
Crea una tarea.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | integer | ID del proyecto (numérico) |
| titleREQUIRED | string | Título |
| description | string | Descripción |
| assigneeId | integer | ID de cuenta del asignado |
| status | enum | Estado — PENDING·TODO·IN_PROGRESS·IN_REVIEW·BLOCKED·COMPLETED·CANCELLED |
| priority | enum | Prioridad — URGENT·HIGH·MEDIUM·LOW |
| dueDate | date | Fecha límite — ISO 8601 |
| milestoneId | integer | ID del hito a vincular |
/tasks/{taskId}Obtener tarea
Devuelve una tarea.
| Nombre | Tipo | Descripción |
|---|---|---|
| taskIdREQUIRED | integer | ID de la tarea |
/tasks/{taskId}Actualizar tarea
Actualiza solo los campos enviados.
| Nombre | Tipo | Descripción |
|---|---|---|
| taskIdREQUIRED | integer | ID de la tarea |
| Nombre | Tipo | Descripción |
|---|---|---|
| title | string | Título |
| status | enum | Estado — PENDING·TODO·IN_PROGRESS·IN_REVIEW·BLOCKED·COMPLETED·CANCELLED |
| priority | enum | Prioridad — URGENT·HIGH·MEDIUM·LOW |
| dueDate | date | Fecha límite — ISO 8601 |
/tasks/{taskId}Eliminar tarea
Elimina una tarea. Devuelve 204 No Content si tiene éxito.
| Nombre | Tipo | Descripción |
|---|---|---|
| taskIdREQUIRED | integer | ID de la tarea |
/tasks/assignedMis tareas asignadas
Devuelve las tareas asignadas a la cuenta de la API key.
/projects/{projectId}/documentsListar documentos
Devuelve los documentos de un proyecto. Usa /documents/paged si necesitas paginación.
| Nombre | Tipo | Descripción |
|---|---|---|
| projectIdREQUIRED | string | ID del proyecto — ID numérico o publicId que empieza con p_ |
/documents/{documentId}Obtener documento
Devuelve un documento incluyendo su contenido.
| Nombre | Tipo | Descripción |
|---|---|---|
| documentIdREQUIRED | integer | ID del documento |
/documentsCrear documento
Crea un documento.
| Nombre | Tipo | Descripción |
|---|---|---|
| workspaceIdREQUIRED | integer | ID del workspace |
| titleREQUIRED | string | Título |
| documentTypeREQUIRED | enum | Tipo de documento (p. ej. REQUIREMENT, MEETING_NOTE) |
| projectId | integer | ID del proyecto (numérico) |
| content | string | Cuerpo del documento |
| visibility | enum | Visibilidad — PUBLIC·TEAM·PRIVATE |
/documents/{documentId}Actualizar documento
Actualiza solo los campos enviados.
| Nombre | Tipo | Descripción |
|---|---|---|
| documentIdREQUIRED | integer | ID del documento |
| Nombre | Tipo | Descripción |
|---|---|---|
| title | string | Título |
| content | string | Cuerpo del documento |
| visibility | enum | Visibilidad — PUBLIC·TEAM·PRIVATE |
/documents/{documentId}Eliminar documento
Elimina un documento. Devuelve 204 No Content si tiene éxito.
| Nombre | Tipo | Descripción |
|---|---|---|
| documentIdREQUIRED | integer | ID del documento |
Códigos de error
Cada respuesta de error incluye error.code y error.message.
| Código | Nombre | Descripción | Acción |
|---|---|---|---|
| 400 | Bad Request | El cuerpo de la solicitud no es válido. | Valida el body de la solicitud |
| 401 | Unauthorized | La clave de API es inválida o falta. | Vuelve a comprobar la clave de API |
| 403 | Forbidden | No tienes permiso para acceder a este recurso. Las solicitudes de escritura con una clave de solo lectura devuelven SCOPE_FORBIDDEN. | Comprueba roles/ámbitos |
| 404 | Not Found | No se encontró el recurso solicitado. | Vuelve a comprobar el ID |
| 429 | Rate Limited | Has superado el límite de tasa. | Consulta la cabecera Retry-After y aplica backoff |
| 500 | Server Error | Se produjo un error al procesar la solicitud en el servidor. | Reintenta tras 5 min, revisa status.pjt.ai |