Referencia REST de la API
Contrato legible por máquinas para integradores: OpenAPI 3.1 (/openapi/v1.json), generado desde esquemas Zod en @tcms/shared y verificado en CI.
Usa la referencia interactiva v1 para esquemas por operación, cuerpos de petición y ejemplos de respuesta.
URL base
Sección titulada «URL base»Todas las rutas de integración usan el prefijo /api/v1.
| Entorno | URL base |
|---|---|
| Producción | https://app.probara.net |
| Worker local | http://localhost:8787 |
Ejemplo:
curl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects"Rutas sin versión (/api/...) responden 404 not_found.
Autenticación
Sección titulada «Autenticación»Usa un token API por organización como Authorization: Bearer <secreto>. Con un token válido no hace falta X-Organization-Id.
Las rutas solo de sesión (/api/v1/auth/*, /api/v1/me/api-tokens, aceptar invitación con cookie) están en la guía de autenticación, no en el documento OpenAPI.
Errores
Sección titulada «Errores»Las peticiones fallidas devuelven JSON:
{ "error": { "code": "not_found", "message": "Mensaje legible", "details": {} }}Códigos habituales: validation_failed, not_found, conflict, unauthorized, forbidden, too_many_requests, seat_limit_exceeded, project_limit_exceeded, api_result_limit_exceeded, billing_provider_unavailable, organization_plan_required, run_case_assignee_locked, project_locked.
project_locked es el único de estos que no habla de la petición que enviaste. Una organización
que tiene más proyectos de los que su plan permite mantiene abiertos solo los creados más
recientemente; todos los demás quedan bloqueados, y cualquier lectura o escritura sobre ellos
responde 403 { code: "project_locked" } — por esta API y por MCP, tanto con una sesión como con
una clave de API. DELETE /api/v1/projects/{projectId} es la única operación que un proyecto
bloqueado sigue sirviendo, porque eliminar es la forma de que la organización vuelva a estar
dentro del límite. El bloqueo no elimina nada: GET /api/v1/projects sigue listando el proyecto,
con un lockedAt no nulo, y subir el límite de la organización borra la marca y lo reabre sin
cambios. No lo reintentes: solo se libera cuando una persona actúa.
Un enlace público de una ejecución que pertenece a un proyecto bloqueado responde exactamente el
mismo 404 { code: "not_found" }, byte a byte, que un token desconocido. Es deliberado: quien
tiene un enlace válido no debe poder usarlo para deducir que la organización supera su límite.
details a veces incluye su propio code, distinto del de nivel superior — por ejemplo, un
rechazo validation_failed de un marcado incluye details: { code: "elapsed_ms_required", path: ["elapsedMs"] }. Usa siempre primero el code de nivel superior para decidir la rama; trata
details.code como información complementaria, nunca como un reemplazo.
Paginación
Sección titulada «Paginación»Los listados de tabla (proyectos, hitos, ejecuciones, defectos, miembros de la organización, invitaciones, grupos de usuarios, campos personalizados y el listado de casos archivados descrito más abajo) usan paginación por página. Aceptan page (base 1, por defecto 1) y pageSize (por defecto 50, máximo 200). Valores fuera de rango o no enteros devuelven 422 validation_failed. La respuesta incluye la página, su tamaño y el total de filas que coinciden con los filtros y la búsqueda activos — no solo las de la página:
{ "items": [], "page": 1, "pageSize": 50, "total": 0}Una page posterior a la última devuelve 200 con items: [] y el total real. Muchos listados aceptan además un parámetro q para búsqueda por subcadena sin distinción de mayúsculas; q se aplica antes de paginar y se refleja en total.
Paginación por cursor (feeds y árboles). Los endpoints de feed cronológico (eventos de auditoría y sus variantes por ejecución/defecto) y los endpoints del árbol de suites/casos (incluido el listado activo por defecto GET /api/v1/projects/{projectId}/test-cases y su filtro archived=true) mantienen la paginación por cursor: limit (por defecto 50, máximo 200) y cursor opcional (ULID), con respuesta { "items": [], "nextCursor": "01ARZ3…" }. Si nextCursor es null, no hay más páginas. La única excepción es el endpoint por página GET /api/v1/projects/{projectId}/test-cases/archived descrito abajo, que existe justamente para navegar los casos archivados por página numerada (Papelera) en lugar de recorrerlos como feed por cursor.
Versionado
Sección titulada «Versionado»Los cambios incompatibles requieren una ruta mayor nueva (por ejemplo /api/v2). /api/v1 se mantiene hasta deprecación explícita.
Búsqueda por display ID
Sección titulada «Búsqueda por display ID»Las suites y los casos exponen display IDs estables en las respuestas (displayId, más suiteNumber / caseNumber). Los integradores pueden resolver recursos por display ID sin conocer el ULID:
GET /api/v1/projects/{projectId}/suites/by-display-id/{displayId}—{projectId}es el código del proyecto (por ejemploACME).{displayId}debe coincidir con{projectId}-S{enteroPositivo}(por ejemploACME-S2). Devuelve la misma formaSuiteResponsequeGET /api/v1/suites/{suiteUlid}.GET /api/v1/projects/{projectId}/test-cases/by-display-id/{displayId}—{displayId}debe coincidir con{projectId}-{enteroPositivo}(por ejemploACME-14). Devuelve la misma formaTestCaseResponsequeGET /api/v1/test-cases/{caseUlid}.
Display IDs mal formados devuelven 422 validation_failed. IDs desconocidos devuelven 404 not_found. Prefijos que no coinciden con el código del proyecto devuelven 400 validation_failed.
Mutaciones de suites. Crear (POST /api/v1/projects/{projectId}/suites), renombrar o mover (PATCH /api/v1/suites/{suiteUlid}) y eliminar (DELETE /api/v1/suites/{suiteUlid}) suites requieren el permiso de ejecución de suites (que tienen owner/admin/member); un viewer recibe 403 forbidden. Leer suites (árbol, detalle, búsqueda por display ID) no cambia para cualquier miembro autenticado.
Casos de prueba archivados (Papelera)
Sección titulada «Casos de prueba archivados (Papelera)»GET /api/v1/projects/{projectId}/test-cases/archived devuelve un listado por página, a nivel de proyecto, de los casos de prueba archivados — el archivado más reciente primero (archivedAt descendente, ULID descendente como desempate). Acepta los parámetros estándar page / pageSize y responde con la forma de página estándar:
{ "items": [], "page": 1, "pageSize": 50, "total": 0}total cuenta todos los casos archivados del proyecto, no solo los de la página devuelta; una page posterior a la última sigue devolviendo 200 con items: [] y el total real. El listado ignora el filtro de suite — siempre devuelve los casos archivados de todas las suites (y los casos sin suite), igual que la exclusión de casos archivados que ya aplica el listado por defecto GET /api/v1/projects/{projectId}/test-cases.
Este endpoint es aditivo: no reemplaza a GET /api/v1/projects/{projectId}/test-cases?archived=true, que mantiene su contrato y comportamiento por cursor ({ items, nextCursor }) sin cambios. Usa el endpoint por cursor para recorrer todo el conjunto archivado como feed; usa /archived para renderizar una vista de Papelera con páginas numeradas.
Páginas relacionadas
- Restaurar casos archivados — recorrido para usuarios de la vista de Papelera paginada y la selección entre páginas.
Valores de custom fields en casos de prueba
Sección titulada «Valores de custom fields en casos de prueba»Los casos de prueba ya no llevan los atributos de clasificación (priority, severity, status, type, layer, behavior, automationStatus, isFlaky, preconditions, postconditions) como claves del cuerpo. Esos atributos son ahora custom fields del sistema y se leen/escriben mediante el contrato dedicado de valores:
-
GET /api/v1/test-cases/{caseUlid}incluye un arraycustomFieldValuescon una entrada por cada definición visible para el proyecto del caso. Si no hay valor guardado, la entrada toma eldefaultValuede la definición (onull). -
GET /api/v1/projects/{projectId}/test-casestambién incrustacustomFieldValuesen cada item con la misma forma. El endpoint de listado arma el array con una lectura masiva, así que la cantidad de round-trips a D1 es constante independientemente del tamaño de página. -
Cada entrada incluye dos claves opcionales y nulables además de
fieldUlidyvalue:systemKey: elsystemKeyde la definición ('priority','severity','status','type','layer','behavior','automation_status','is_flaky','preconditions','postconditions') cuandogroup = 'system', onullcuandogroup = 'custom'.optionName: elnamede la opción seleccionada ('High','Critical','Active', …) para tiposselect_single/radiocuandovaluees un ULID válido de opción;nullpara otros tipos o cuandovalueesnull.optionNamese resuelve según el idioma de la solicitud, de la misma forma que se explica en las definiciones de campos ahora son bilingües más abajo:Accept-Language: esdevuelve el nombre en español cuando existe, y cae al inglés en caso contrario — un encabezado ausente o con un idioma no soportado resuelve a inglés, así que un cliente que nunca lo envía recibe respuestas idénticas byte a byte a las de antes de este cambio.
Con esas claves, los clientes pueden renderizar chips de system fields sin pedir aparte la lista de definiciones. Los clientes antiguos que ignoran las claves nuevas siguen funcionando sin cambios.
Las definiciones de campos ahora son bilingües
Sección titulada «Las definiciones de campos ahora son bilingües»Los títulos de campo, los placeholders y los nombres de opción ahora pueden llevar una traducción
al español. El título y el nombre de opción requieren un valor en inglés no vacío; el valor en
inglés de un placeholder es opcional (un campo puede no tener placeholder en ningún idioma), por lo
que placeholderI18n.en puede ser null. GET /api/v1/orgs/{orgUlid}/custom-fields
resuelve title, placeholder y el name de cada opción según el idioma de la solicitud
(Accept-Language: es devuelve el valor en español cuando existe, y cae al inglés en caso
contrario — un encabezado ausente o con un idioma no soportado resuelve a inglés, así que un
cliente que nunca lo envía recibe respuestas idénticas byte a byte a las de antes de este cambio).
Agrega ?locales=all para recibir además titleI18n, placeholderI18n y el nameI18n de cada
opción — un mapa por etiqueta de idioma ({ en, es }) — junto a los valores ya resueltos. El
parámetro de búsqueda q encuentra un campo cuyo título está guardado en cualquiera de los
dos idiomas.
POST /api/v1/orgs/{orgUlid}/custom-fields, PATCH .../custom-fields/{fieldUlid} y
POST .../custom-fields/{fieldUlid}/reset aceptan el mismo parámetro ?locales=all en la
respuesta field de un único objeto, con la misma condición: sin él, la respuesta se mantiene
idéntica byte a byte a la forma previa a la localización (sin ninguna clave *I18n), incluso
para integradores autenticados con API key.
La exportación (GET /api/v1/projects/{projectId}/exports) es la única excepción a la resolución
por idioma: los nombres de opción de un archivo exportado deliberadamente no se resuelven por
idioma y se mantienen siempre en inglés, para que un CSV/JSON exportado y su reimportación
coincidan siempre en un mismo vocabulario, sin importar quién descargó el archivo. Esto es
independiente del customFieldValues.optionName documentado arriba, que sí se resuelve por
idioma en cualquier lectura ordinaria.
Actualización consolidada del caso (PATCH)
Sección titulada «Actualización consolidada del caso (PATCH)»PATCH /api/v1/test-cases/{caseUlid} acepta un cuerpo consolidado que agrupa los cambios de campos escalares, la lista de pasos y los valores de campos personalizados en una sola petición. Es lo que usa el editor web al guardar; cada llamada genera un único evento de auditoría con un diff unificado.
{ "patch": { "title": "Login falla en móvil", "description": "...", "suiteUlid": "01J...SUITE", "milestoneUlid": null, "tags": ["smoke"] }, "steps": [ { "position": 1, "action": "Abrir la pantalla de login" }, { "position": 2, "action": "Enviar credenciales inválidas" } ], "customFieldValues": [{ "fieldUlid": "01J...PRIORITY", "value": "01J...OPTION_HIGH" }]}Debe estar presente al menos uno de patch, steps o customFieldValues. El handler lee el caso actual, los pasos y los valores de campos personalizados visibles antes de mutar, aplica las mutaciones solicitadas, computa el diff unificado y emite exactamente un evento test_case.updated (o test_case.moved si cambió suiteUlid). Si alguna sub-mutación falla, la respuesta es no-2xx y no se registra ningún evento de auditoría.
Requiere el permiso de escritura de casos de prueba (que tienen owner/admin/member); un token o sesión con rol viewer recibe 403 forbidden y no se muta ningún campo.
Los endpoints separados PUT /api/v1/test-cases/{caseUlid}/steps y PUT /api/v1/test-cases/{caseUlid}/custom-field-values siguen disponibles para integradores directos de la API, pero ya no emiten eventos de auditoría por su cuenta: el PATCH consolidado es la única fuente de eventos test_case.updated. PUT .../steps requiere el mismo permiso de escritura de casos de prueba (que tienen owner/admin/member); un viewer recibe 403 forbidden.
Imágenes en pasos
Sección titulada «Imágenes en pasos»Los pasos admiten attachments[] opcional al crear (POST .../test-cases) y en PUT .../steps. Las imágenes usan un flujo stage-and-commit, y crear en staging o eliminar un adjunto de paso requiere el permiso de ejecución de adjuntos (que tienen owner/admin/member); un viewer recibe 403 forbidden:
POST /api/v1/test-cases/{caseUlid}/step-attachments:stage— subida multipart (varias partesfile); devuelve claves staged bajostaging/<orgUlid>/sin fila en base de datos.- Commit al incluir esas referencias en
attachmentsdel paso al guardar. DELETE /api/v1/test-cases/{caseUlid}/step-attachments/{attachmentUlid}— elimina un adjunto ya confirmado.
Consulta Imágenes en pasos de caso para límites, reglas de reconciliación y ejemplos.
PUT /api/v1/test-cases/{caseUlid}/custom-field-valuesreemplaza atómicamente el conjunto de valores del caso:
curl -sS -X PUT \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "content-type: application/json" \ -d '{ "values": [ { "fieldUlid": "01J...PRIORITY", "value": "01J...OPTION_HIGH" }, { "fieldUlid": "01J...IS_FLAKY", "value": true }, { "fieldUlid": "01J...PRECONDITIONS", "value": "Sesión iniciada" } ] }' \ "https://app.probara.net/api/v1/test-cases/01J.../custom-field-values"En campos checkbox, el booleano JSON (true / false) coincide con lo que devuelve el GET; el formato legado con cadenas ("true" / "false") sigue aceptándose.
Los valores vacíos / null borran la fila correspondiente. La respuesta es 200 con el array embebido completo. Las reglas de validación por tipo de campo viven en el esquema OpenAPI (PutCustomFieldValuesInput).
Para descubrir los ULIDs de los campos del sistema (priority, severity, etc.), llama a GET /api/v1/orgs/{orgUlid}/custom-fields. Añade ?projectUlid={projectUlid} para filtrar a las definiciones visibles para un proyecto concreto (útil al construir formularios de caso).
Actualizar un solo valor de custom field
Sección titulada «Actualizar un solo valor de custom field»PATCH /api/v1/test-cases/{caseUlid}/custom-field-values/{fieldUlid} inserta o actualiza un campo sin reemplazar el resto de valores del caso. El cuerpo es solo { "value": <unknown> } (sin claves extra). La respuesta es 200 con el mismo array embebido completo customFieldValues que devuelve el PUT.
El PATCH solo exige obligatorio en el campo objetivo; no revisa si otros campos obligatorios están vacíos. Usa PUT para un reemplazo atómico de formulario completo, cuando todos los campos obligatorios visibles deben ir en el cuerpo.
curl -sS -X PATCH \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "content-type: application/json" \ -d '{ "value": "01J...OPTION_HIGH" }' \ "https://app.probara.net/api/v1/test-cases/01J.../custom-field-values/01J...SEVERITY"Los valores vacíos / null borran la fila si el campo no es obligatorio. Intentar vaciar un campo obligatorio devuelve 422 validation_failed con details en path: ["value"].
Actualizar un custom field
Sección titulada «Actualizar un custom field»PATCH /api/v1/orgs/{orgUlid}/custom-fields/{fieldUlid} actualiza metadatos de la definición y, en tipos basados en opciones, la lista options. Cada entrada en options PUEDE incluir el ulid de la opción existente. Si el ulid coincide con una fila ya guardada para el campo, el PATCH conserva ese ULID y actualiza name, icon, color y position en el mismo registro. Las entradas sin ulid se insertan como opciones nuevas con un ULID recién generado. Las filas cuyo ulid no aparece en el patch se eliminan (en campos de sistema sigue prohibido quitar opciones semilla con systemKey).
Como los valores de select_single, select_multi y radio guardan ULIDs de opción en custom_field_values, devuelve siempre el ulid que entregó el GET cuando quieras modificar una opción existente. Si omites ulid en una fila que ya existía, se crea una opción nueva y los valores guardados que apuntaban al ULID anterior quedan huérfanos.
Los campos obligatorios de ejecución requieren un valor por defecto
Sección titulada «Los campos obligatorios de ejecución requieren un valor por defecto»entity en POST /api/v1/orgs/{orgUlid}/custom-fields y PATCH /api/v1/orgs/{orgUlid}/custom-fields/{fieldUlid} acepta test_case, test_run y defect. Una definición con entity: "test_run" e isRequired: true DEBE tener un defaultValue no vacío. Incumplir esto en cualquiera de los dos endpoints devuelve 422 validation_failed con details.field = "defaultValue", y no crea ni modifica ninguna fila de definición. El invariante se evalúa sobre el estado efectivo de la definición después de la escritura, así que marcar como obligatorio un campo test_run existente sin valor por defecto guardado se rechaza en PATCH igual que en POST; enviar isRequired y un defaultValue no vacío juntos en la misma solicitud se acepta. Los campos test_case y defect quedan exentos y pueden seguir siendo obligatorios sin valor por defecto, como antes.
Restablecer un campo de sistema a los valores semilla
Sección titulada «Restablecer un campo de sistema a los valores semilla»Los propietarios y administradores pueden devolver una definición de sistema a su estado sembrado:
curl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "X-Organization-Id: $ORG_ULID" \ -H "content-type: application/json" \ -d '{}' \ "https://app.probara.net/api/v1/orgs/$ORG_ULID/custom-fields/$FIELD_ULID/reset"- 200 — Devuelve
{ field }con título semilla, placeholder vacío, nombres/iconos/colores de opciones semilla y ULIDs estables (porsystemKey). Las opciones agregadas por el usuario (systemKey: null) se eliminan. - 400
not_system_field— El objetivo no es un campo de sistema. - 403 — El usuario no es propietario ni administrador.
- 404 — Campo u organización desconocidos, o acceso entre inquilinos.
La operación es idempotente si el campo ya estaba en estado semilla. El alcance por proyecto (allProjects, projectUlids) no cambia.
Cascada de valores: Al eliminar opciones de usuario, los valores guardados en casos que apuntaban a esos ULIDs se actualizan en la misma petición: en select_single / radio se usa el ULID del valor por defecto semilla si existe; si no, se borra la fila. En select_multi se filtran los ULIDs eliminados y, si el array queda vacío, se aplica el valor por defecto semilla. Los valores de tipos no basados en opciones (párrafo, checkbox, etc.) no se modifican.
Proyectos
Sección titulada «Proyectos»PATCH /api/v1/projects/{projectId} requiere el permiso de gestión de proyectos (que tienen owner/admin). member y viewer reciben 403 forbidden con error.code forbidden. Crear un proyecto (POST /api/v1/projects) requiere el mismo permiso de gestión de proyectos: solo owner/admin pueden crear un proyecto — un member o viewer ahora también recibe 403 forbidden. GET en /api/v1/projects y GET /api/v1/projects/{projectId} no cambian para los miembros autorizados de la organización.
GET /api/v1/projects/{projectId} incluye avatarKey (string | null) y avatarVersion (number). Cuando avatarKey no es nulo, la imagen se sirve públicamente en GET /__assets/{avatarKey}?v={avatarVersion} (sin sesión).
| Método | Ruta | Cuerpo | Respuesta |
|---|---|---|---|
POST | /api/v1/projects/{projectId}/avatar | multipart/form-data con un file (PNG/JPEG/WebP, máx. 5 MiB) | { avatarKey, avatarVersion } |
DELETE | /api/v1/projects/{projectId}/avatar | — | { avatarKey: null, avatarVersion } |
POST/DELETE en /api/v1/projects/{projectId}/avatar requieren owner o admin. Las imágenes se normalizan en el servidor a WebP 256×256 bajo avatars/projects/{projectUlid}.webp.
Control de acceso
Sección titulada «Control de acceso»GET/PATCH /api/v1/projects/{projectId}/access leen y cambian el modo de acceso público/privado de un
proyecto y su propietario — un recurso dedicado; el estado de acceso nunca se agrega a la respuesta
general de GET /projects/{projectId}. PATCH requiere ser owner/admin de la organización o el
propietario actual del proyecto — un miembro que solo tiene acceso de lectura (una fila granted en la
lista de acceso) no está automáticamente autorizado a alternarlo.
GET/POST/DELETE /api/v1/projects/{projectId}/access/members listan, otorgan y revocan miembros
individuales en la lista de acceso de un proyecto privado, sin cambiar nunca el modo del proyecto — solo
PATCH /access { mode } hace eso. Misma regla de autorización que el alternador. Eliminar el último
otorgamiento restante ahora tiene éxito (204): la lista de acceso puede legítimamente quedar vacía en un
proyecto privado; publicar un proyecto siempre es un cambio de modo explícito a través de
PATCH /access { mode }, nunca un efecto secundario de revocar al último miembro de la lista.
Ver Acceso a proyectos para la forma completa de las peticiones y
respuestas, las reglas de siembra al alternar, y una limitación documentada: los tokens de API nunca se
evalúan contra la lista de acceso. La aplicación en sí sigue a los datos, no a la forma de la URL — las
rutas que resuelven su proyecto de forma transitiva a partir de un ULID de entidad quedan protegidas igual
que /projects/{projectId}/*.
Un grupo de usuarios también puede asignarse a la lista de acceso de un proyecto, otorgando acceso a todos los miembros actuales y futuros del grupo en una sola operación:
| Método | Ruta | Notas |
|---|---|---|
GET | /api/v1/projects/{projectId}/access/groups | Lista los grupos asignados a la lista de acceso del proyecto |
POST | /api/v1/projects/{projectId}/access/groups | Asigna un grupo ({ userGroupUlid }); tiene éxito en un proyecto público — la asignación queda dormida hasta que sea privado |
DELETE | /api/v1/projects/{projectId}/access/groups/{userGroupUlid} | Desasigna un grupo; nunca 409, ni siquiera para el último grupo restante |
GET | /api/v1/groups/{userGroupUlid}/projects | La vista inversa: los proyectos a los que está asignado un grupo dado (ver abajo) |
POST | /api/v1/groups/{userGroupUlid}/projects | Asigna el grupo a uno o varios proyectos en una sola solicitud ({ projectUlids: string[] }); sin barrera de organización propia — se autoriza por proyecto |
Misma regla de autorización que la lista de acceso individual (owner/admin de la
organización, o el propietario actual del proyecto). A diferencia de la familia individual, asignar
un grupo está permitido en un proyecto público: access_mode es un dato almacenado, nunca
inferido de una asignación, así que el otorgamiento queda dormido y sobrevive a cualquier cambio de
modo posterior en cualquier dirección. No existe una revocación a nivel de grupo — la fila revoked
propia de la lista de acceso individual siempre prevalece sobre un otorgamiento por grupo.
GET /api/v1/groups/{userGroupUlid}/projects está abierto a lectura como los demás GET de grupos
(ver Grupos de usuarios), así que cualquier miembro de la
organización puede llamarlo — pero, a diferencia del projectCount propio del grupo, este listado
filtra cualquier proyecto que el llamante no pueda descubrir de otra forma: un proyecto privado al
que el grupo está asignado se omite silenciosamente de items y total para ese llamante, mientras
que un owner/admin de la organización ve todas las asignaciones.
Configuración de ejecución
Sección titulada «Configuración de ejecución»GET/PATCH /api/v1/projects/{projectId}/run-settings leen y cambian las doce
configuraciones que gobiernan el marcado, la escritura en ejecuciones cerradas y los valores
predeterminados de creación de runs — resueltas mediante
proyecto → organización → valor por defecto del código para once de las doce claves.
GET/PATCH /api/v1/orgs/{orgUlid}/run-settings leen y cambian los valores predeterminados a
nivel de organización para esas mismas once claves. PATCH en el endpoint de proyecto requiere
owner/admin de la organización o el propietario actual del proyecto; PATCH en el endpoint
de organización requiere el permiso org-settings.manage. Consulta
Configuración de ejecución para el vocabulario
completo, el contrato de resolución/source, y la única brecha documentada aplicada solo en el
cliente (requireCommentOnNegativeResult).
Listado: estadísticas opcionales y filtro por miembro
Sección titulada «Listado: estadísticas opcionales y filtro por miembro»GET /api/v1/projects acepta dos parámetros de consulta opcionales y aditivos.
Ambos son opt-in: una solicitud sin ellos devuelve exactamente la forma plana
actual de cada item, y las respuestas de POST/GET /api/v1/projects/{projectId}
no cambian.
| Parámetro | Notas |
|---|---|
include=stats | Incrusta un objeto stats opcional, un arreglo team y un entero teamCount en cada item del listado. El único valor aceptado es stats. |
memberUlid | Conjunto de ULIDs de miembros separados por comas. Filtra la página (y total) a los proyectos cuyo equipo incluye a alguno de los miembros indicados. Los ULIDs desconocidos (que no sean miembros activos de tu organización) se descartan en silencio. Es independiente de include: puedes filtrar sin pedir estadísticas. |
Cuando include=stats está presente, cada item lleva además:
| Campo | Tipo | Significado |
|---|---|---|
stats.cases | entero | Casos de prueba no archivados |
stats.runs | entero | Todas las ejecuciones |
stats.runsInProgress | entero | Ejecuciones con state = open |
stats.defectsUnresolved | entero | Defectos con estado open o in_progress |
stats.milestones | entero | Hitos vigentes (no eliminados) |
team | arreglo | Subconjunto liviano y listo para avatares del equipo del proyecto — como máximo 4 miembros, ordenados por fecha de incorporación a la membresía en orden ascendente. Cada entrada es { userUlid, firstName, lastName, avatarKey, avatarVersion }. |
teamCount | entero | Tamaño total del equipo del proyecto (alimenta un indicador de excedente +N). |
Un proyecto sin filas que coincidan reporta 0 en cada métrica (nunca un campo
ausente). Todos los conteos están aislados por organización: solo incluyen las
filas de tu propia organización.
curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects?include=stats&memberUlid=01J...,01K..."
team/teamCount/memberUliddependen del modo. En un proyecto público el equipo es la membresía activa de la organización (menos quien haya sido revocado individualmente) — el comportamiento opt-out original, sin cambios. En un proyecto privado el equipo es exactamente los miembros con una filagranteden la lista de acceso, intersectados con la membresía activa; los miembros que alcanzan el proyecto solo mediante el bypass de rolowner/adminde la organización no tienen otorgamiento y están correctamente ausentes del equipo, aunque sí aparecen en la lista de miembros con acceso del proyecto.memberUlidfiltra sobre este mismo conjunto dependiente del modo, así que tiene efecto real tanto en proyectos públicos como privados.
Listado: filtros de presencia y estado
Sección titulada «Listado: filtros de presencia y estado»GET /api/v1/projects también acepta cuatro filtros opcionales y aditivos de
presencia/estado que acotan qué proyectos aparecen. Se aplican del lado del
servidor, por lo que el total devuelto y la paginación siempre reflejan el
conjunto filtrado. Cada filtro toma una lista de tokens de opción separados por
comas (también puedes repetir el parámetro, por ejemplo
runs=without&runs=active). Dentro de un mismo filtro las opciones seleccionadas
se combinan con OR; entre filtros distintos se combinan con AND. Se
encadenan con q y memberUlid, y una solicitud sin ninguno de ellos no cambia.
Los tokens desconocidos se descartan en silencio (sin 422); un filtro cuyos
tokens son todos desconocidos se trata como ausente.
Cada filtro refleja exactamente la definición de la métrica correspondiente, así
que filtrar concuerda con los conteos que ves bajo include=stats.
| Parámetro | Opciones | Significado |
|---|---|---|
runs | without | Proyectos sin ejecuciones |
active | Proyectos con al menos una ejecución abierta (state = open) | |
any | Proyectos con al menos una ejecución (cualquier estado) | |
defects | has | Proyectos con al menos un defecto sin resolver (estado open o in_progress) |
without | Proyectos sin defectos sin resolver (solo resueltos o ninguno) | |
milestones | has | Proyectos con al menos un hito vigente (no eliminado) |
without | Proyectos sin hitos vigentes | |
cases | has | Proyectos con al menos un caso de prueba activo (no archivado) |
without | Proyectos sin casos activos |
defectssignifica sin resolver. El filtrodefectssolo considera defectos cuyo estado esopenoin_progress— la misma definición questats.defectsUnresolved. Los defectos resueltos o cerrados nunca hacen que un proyecto coincida condefects=has.
Los cuatro filtros están aislados por organización: solo consideran filas de tu propia organización.
# Proyectos con una ejecución abierta Y sin defectos sin resolver, con nombre tipo "mobile":curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects?q=mobile&runs=active&defects=without"
# OR dentro de un filtro: proyectos sin ejecuciones O con una ejecución abierta:curl -sS \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects?runs=without,active"Ejecuciones y resultados
Sección titulada «Ejecuciones y resultados»Una ejecución congela los casos seleccionados en ella. Crea la ejecución con los casos a ejecutar y luego lee y muta sus casos:
| Método | Ruta | Notas |
|---|---|---|
POST | /api/v1/projects/{projectUlid}/runs | Body { name, description?, defaultAssigneeUlid?, environmentId?, environment?, caseUlids?[], planUlid?, configurationUlids?[], customFieldValues?[] }. Congela título + pasos de cada caso. caseUlids puede omitirse solo cuando se envía planUlid (los casos y los responsables por caso de la ejecución se siembran desde la selección actual del plan); en caso contrario es obligatorio. Una sola solicitud crea exactamente una ejecución — planUlid nunca genera más de una. |
GET | /api/v1/projects/{projectUlid}/runs | Cada elemento incluye progreso (total, executed, passRate), un objeto counts exacto de 5 bandas, agregados temporales (firstResultAt, lastResultAt, totalDurationMs) y un author discriminado (ver abajo). |
GET | /api/v1/projects/{projectUlid}/runs/environments | Deprecado. Devuelve { items: string[] } con los valores de texto libre environment no vacíos y distintos del proyecto, ordenados ascendentemente. Se mantiene por compatibilidad; se recomienda filtrar por ULID con environmentId en su lugar. |
GET / PATCH | /api/v1/runs/{runUlid} | PATCH actualiza solo metadatos (name, description, defaultAssigneeUlid, environmentId, milestoneId, configurationUlids); el status agregado se deriva, nunca se parchea. Permitido en una ejecución cerrada y no abortada; devuelve 409 conflict solo cuando la ejecución está abortada. |
POST | /api/v1/runs/{runUlid}/close | Cierra la ejecución antes de tiempo (state = closed) aunque queden casos sin probar. Una ejecución también se cierra sola automáticamente en cuanto todos los casos tienen resultado — no requiere llamada del cliente. Llamar a close en una ejecución ya cerrada devuelve 409 conflict (el pestillo es de un solo sentido). |
POST | /api/v1/projects/{projectUlid}/runs/{runUlid}/clone | ”Volver a ejecutar”: crea una nueva ejecución abierta a partir de una ejecución origen cerrada (completada o abortada). Cuerpo { title: string, cloneAssignees?: boolean, statusFilter?: TestOutcomeStatus[] } — title es obligatorio; statusFilter selecciona qué casos del origen se copian (omitido/vacío = todos). Generaliza la antigua ruta rerun-failed, que se elimina. |
GET / POST | /api/v1/runs/{runUlid}/cases | Lista los casos de la ejecución; POST añade (y congela) más casos. |
POST | /api/v1/runs/{runUlid}/cases/bulk-mark | Cuerpo { caseUlids: string[1..500], status } (passed | failed | skipped | blocked). Una transacción atómica; un evento run.cases_bulk_marked; responde { affected, unchanged }. |
POST | /api/v1/runs/{runUlid}/cases/bulk-assign | Cuerpo { caseUlids, assigneeUlid: string | null }. Misma atomicidad y forma de respuesta; emite run.cases_bulk_assigned. |
POST | /api/v1/runs/{runUlid}/cases/bulk-retry | Cuerpo { caseUlids }. Reinicia los casos seleccionados (y sus pasos) a untested; emite run.cases_bulk_retried. |
POST | /api/v1/runs/{runUlid}/cases/bulk-remove | Cuerpo { caseUlids }. Quita casos del run con capturas previas; emite run.cases_bulk_removed. |
GET / PATCH | /api/v1/runs/{runUlid}/cases/{runCaseUlid} | PATCH define assigneeUlid / status / elapsedMs. Un cambio de estado marca todos los pasos y añade una fila de resultado. La respuesta de PATCH incluye resultUlid nullable (el intento añadido cuando cambió status; null en parches solo de asignado/tiempo). El detalle GET añade linkedDefects[] — defectos distintos vinculados vía cualquier intento del caso (ulid, defectNumber, title, status y assigneeUlid, que es null cuando el defecto no está asignado). PATCH rechaza con 403 run_case_assignee_locked cuando la configuración assigneeResultLock del proyecto está activa y quien llama no es el asignado ni está exento (sin exención para tokens de API), y con 422 validation_failed (details.code: "elapsed_ms_required") cuando timeTracking es required y un marcado terminal omite elapsedMs — ver Casos de ejecución. |
POST | /api/v1/runs/{runUlid}/cases/{runCaseUlid}/open | Autoasigna al usuario actuante cuando está sin asignar. |
PATCH | /api/v1/runs/{runUlid}/cases/{runCaseUlid}/steps/{stepUlid} | Marca un paso; el estado del caso es el agregado de sus pasos. |
GET | /api/v1/runs/{runUlid}/results | Historial de marcados de solo-adición; un caso puede aparecer varias veces, ordenado por executedAt. |
Todos los endpoints de mutación requieren rol member o superior (viewer → 403 forbidden) y emiten eventos de auditoría. Una ejecución abortada siempre rechaza estas mutaciones con 409 conflict. Una ejecución cerrada sin abortar las acepta por defecto (allowResultsInClosedRuns tiene por defecto true); una organización o proyecto que fije esa configuración de ejecución en false restaura el comportamiento anterior, rechazándolas también en una ejecución cerrada.
Vinculación de entorno en ejecuciones
Sección titulada «Vinculación de entorno en ejecuciones»Las ejecuciones pueden vincularse a una entidad Entorno estructurada gestionada por el proyecto.
Crear (POST /api/v1/projects/{projectUlid}/runs)
| Campo | Notas |
|---|---|
environmentId | ULID opcional de un entorno que pertenezca al proyecto de la ejecución. Devuelve 404 not_found si el ULID no resuelve a un entorno activo en el proyecto. Omítelo o envía null para dejar la ejecución sin entorno vinculado. |
environment | Solo compatibilidad heredada. Cadena de texto libre. Cuando environmentId está ausente y environment es una cadena no vacía, el servidor busca o crea un entorno cuyo slug coincida con el valor slugificado y vincula la ejecución a él. Si se proporcionan ambos campos, environmentId tiene prioridad. |
Actualizar (PATCH /api/v1/runs/{runUlid})
| Campo | Notas |
|---|---|
environmentId | ULID opcional para cambiar o vincular el entorno de la ejecución. Envía null para desvincular. Devuelve 404 not_found para ULIDs desconocidos. El campo environment de texto libre no se acepta en PATCH. |
Campos añadidos a RunResponse
| Campo | Tipo | Notas |
|---|---|---|
environmentId | string (ULID) | null | ULID del entorno vinculado, o null si no hay vínculo. |
environment | { ulid, name, slug } | null | Resumen del objeto entorno vinculado, o null si no hay vínculo. |
environmentName | string | null | Deprecado. Valor de texto libre de la columna environment heredada. Usa environment.name en su lugar. |
Filtros del listado de ejecuciones
Sección titulada «Filtros del listado de ejecuciones»GET /api/v1/projects/{projectId}/runs acepta paginación por página (page, pageSize) y estos filtros de servidor. Los filtros se componen con semántica AND y total refleja la población filtrada:
| Parámetro | Notas |
|---|---|
q | Búsqueda por subcadena sin distinguir mayúsculas en nombre de ejecución y entorno |
status | Estado proyectado separado por comas: in_progress, passed, failed |
env | Valor repetible. Repite env para semántica OR. Pasa el ULID del entorno para filtrar por entidad vinculada; pasa un texto libre para buscar en la columna environment heredada. Los valores ULID y de texto libre se componen con OR cuando se mezclan. Los valores pueden contener comas, así que no los dividas por coma. |
authorUlid | ULID de usuario del autor de la ejecución. Las ejecuciones creadas por tokens de API no coinciden con autores de usuario. |
assigneeUlid | ULID del usuario asignado por defecto, o empty para ejecuciones sin asignado por defecto. |
cf | Filtro repetible de campo personalizado, <fieldUlid>:<valor[,valor…]>. Los valores dentro de una misma entrada cf se combinan con OR; repite cf para un campo distinto y combinar con AND entre campos. Los campos de opción usan ULIDs de opción, checkbox usa true/false, user_picker usa un ULID de usuario, y el token literal empty coincide con ejecuciones sin fila almacenada para ese campo. La coincidencia solo considera filas almacenadas — un valor por defecto materializado nunca coincide. |
Ejemplo:
GET /api/v1/projects/DEMO/runs?status=failed&env=01JXXXXXXXXXXXXXXXXXXXXXXXXX&page=1&pageSize=50Forma de RunResponse
Sección titulada «Forma de RunResponse»Cada RunResponse (devuelto por GET /runs, GET /runs/{runUlid} y el cuerpo de POST /runs) incluye, además de la identidad de la ejecución, sus metadatos y los ejes state/status, los siguientes campos:
total,executed,passRate— contadores de progreso heredados (se mantienen por compatibilidad).counts— desglose exacto de 5 bandas detest_run_cases.status:{ passed, failed, blocked, skipped, untested }.Σ counts = total. Úsalo para pintar la barra de resultados; no lo derives depassRate.firstResultAt,lastResultAt— epoch ms del primer y del últimotest_results.executed_atregistrado en la ejecución; ambosnullhasta que aterriza el primer resultado. UsalastResultAt − firstResultAtpara el tiempo de reloj transcurrido; para una ejecución en curso, usanow − firstResultAt.totalDurationMs— suma detest_results.duration_msde la ejecución (los resultados sin duración medida contribuyen0).author— instantánea discriminada del actor que creó la ejecución, capturada al crearla para que la fila siga siendo renderizable tras eliminarse el usuario o el token. Una de:{ "kind": "user", "ulid": "<userUlid>", "displayName": "...", "avatarKey": <string|null> }{ "kind": "api_token", "ulid": "<apiTokenUlid>", "name": "..." }null— filas heredadas que la migración no pudo recuperar; renderiza como un guion largo.
configurations— arreglo de las etiquetas de configuración de la ejecución, cada una{ ulid, configurationUlid, groupName, valueName }.configurationUlidesnullcuando el valor de configuración subyacente fue eliminado permanentemente;groupName/valueNameson instantáneas tomadas al fijar la etiqueta, así que la combinación sigue siendo renderizable tras un renombrado o una eliminación.[]para una ejecución sin configuraciones. Presente en todoRunResponse— la lista de ejecuciones, el detalle y los elementos de ejecución de cada plan — nunca se omite.
Las ejecuciones creadas desde la API de integradores (Authorization: Bearer <apiTokenSecret>) llevan automáticamente author.kind = "api_token"; las creadas desde una sesión de usuario llevan author.kind = "user". La misma forma aparece en GET /api/v1/runs/{runUlid}.
Valores de custom fields en ejecuciones
Sección titulada «Valores de custom fields en ejecuciones»Las definiciones de custom_fields aceptan entity: "test_run" además de test_case y defect. Los valores a nivel de ejecución se leen y escriben mediante un contrato dedicado, siguiendo las mismas convenciones que casos de prueba y defectos:
GET /api/v1/runs/{runUlid}incluye un arraycustomFieldValuescon una entrada por cada campotest_runvisible para el proyecto de la ejecución. Los valores no almacenados se materializan con eldefaultValuedel campo (onull). El listado de ejecuciones (GET /api/v1/projects/{projectId}/runs) no incluye este array — leerlo siempre requiere el endpoint de detalle, de modo que paginar ejecuciones nunca paga un costo por fila.PUT /api/v1/runs/{runUlid}/custom-field-valuesreemplaza atómicamente el conjunto de valores de la ejecución:
curl -sS -X PUT \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "content-type: application/json" \ -d '{ "values": [ { "fieldUlid": "01J...BUILD", "value": "4471" }, { "fieldUlid": "01J...REGRESSION", "value": "01J...OPTION_YES" } ] }' \ "https://app.probara.net/api/v1/runs/01J.../custom-field-values"Un campo visible omitido del cuerpo hace que su fila almacenada se elimine; una fila almacenada para un campo que ya no es visible para el proyecto de la ejecución se deja intacta. Los valores vacíos / null eliminan la fila correspondiente. El endpoint devuelve 200 con el array embebido completo — la misma forma que devuelve GET /api/v1/runs/{runUlid}.
Requiere el permiso de ejecución de resultados (que tienen owner/admin/member); un token o sesión viewer recibe 403 forbidden y ninguna fila se toca. Solo una ejecución abortada rechaza la escritura con 409 conflict; una ejecución cerrada y no abortada sigue aceptándola — la misma regla de congelamiento que sigue cualquier otra mutación de ejecución.
Una escritura exitosa que cambia al menos un valor se pliega dentro del evento de auditoría run.updated ya existente de la ejecución (metadata.customFieldChanges); nunca emite un evento custom_field independiente. Reenviar un conjunto de valores idéntico al ya almacenado devuelve 200 sin ningún evento de auditoría nuevo.
No existe una ruta PATCH .../custom-field-values/{fieldUlid} de un solo campo para ejecuciones — usa PUT para cualquier escritura.
Para descubrir los ULID de los campos test_run visibles de un proyecto, llama a GET /api/v1/orgs/{orgUlid}/custom-fields?entity=test_run&projectUlid={projectUlid}.
Valores al crear (POST /api/v1/projects/{projectUlid}/runs)
El cuerpo de creación acepta un array opcional customFieldValues con la misma forma { fieldUlid, value } que PUT:
curl -sS -X POST \ -H "Authorization: Bearer $PROBARA_API_TOKEN" \ -H "content-type: application/json" \ -d '{ "name": "Sprint 24", "caseUlids": ["01J...CASE"], "customFieldValues": [{ "fieldUlid": "01J...BUILD", "value": "4471" }] }' \ "https://app.probara.net/api/v1/projects/DEMO/runs"Un campo test_run obligatorio siempre resuelve a un valor al crear: envíalo explícitamente, u omítelo por completo y se persiste el defaultValue del campo (un campo obligatorio nunca puede definirse sin un valor por defecto no vacío, así que la creación nunca puede fallar por un valor obligatorio faltante). Un campo opcional omitido del cuerpo no guarda ninguna fila — su valor por defecto solo se materializa al leer, exactamente igual que una ejecución que nunca llamó a PUT. Un valor inválido en cualquier campo enviado (tipo incorrecto, opción desconocida, etc.) rechaza la creación completa con 422 validation_failed — no se crea la ejecución ni se escribe ninguna fila de valor. Los valores se pliegan dentro del único evento de auditoría run.created de la ejecución (metadata.customFieldChanges); una ejecución creada sin campos de ejecución emite la misma forma de metadata que antes de que este campo existiera.
Clonar una ejecución (POST .../runs/{runUlid}/clone) copia los valores almacenados de la ejecución origen a la nueva ejecución, traducidos contra las definiciones de campos del proyecto destino — un valor almacenado para un campo que ya no es visible en el proyecto destino se descarta silenciosamente, nunca se rechaza. La copia se pliega dentro del evento run.cloned del clon; la ejecución origen nunca se modifica ni se vuelve a auditar.
Uso de resultados de API de la organización
Sección titulada «Uso de resultados de API de la organización»GET /api/v1/orgs/{orgUlid}/api-result-usage informa los resultados contabilizados y autoritativos de la organización para el mes calendario actual, junto con su techo efectivo apiResultsPerMonth — las mismas cifras que aplica el límite mensual de resultados de API descrito arriba, nunca un conteo recalculado. Cualquier miembro autenticado de la organización puede llamarlo; no existe restricción de rol.
{ "periodKey": "2026-08", "used": 42, "limit": 5000 }periodKey es una etiqueta opaca YYYY-MM (UTC) — trátala como un identificador, no como algo para analizar o calcular. Leer el uso nunca crea, reinicia ni avanza el contador, así que consultarlo repetidamente no tiene ningún efecto secundario sobre tu techo. Al comenzar el siguiente mes calendario, el reporte muestra 0 para el nuevo periodo automáticamente, sin ninguna acción del operador ni despliegue — reflejando el mismo reinicio que el límite de escritura ya realiza de forma perezosa en el siguiente resultado contabilizado.
Uso de puestos (seats) de la organización
Sección titulada «Uso de puestos (seats) de la organización»GET /api/v1/orgs/{orgUlid}/seat-usage informa los puestos usados de la organización y su límite efectivo de puestos — la misma definición de puesto usado que compara la aplicación del límite de puestos descrita arriba, nunca un conteo recalculado o derivado del listado de miembros. Cualquier miembro autenticado de la organización puede llamarlo; no existe restricción de rol.
{ "used": 2, "limit": 3 }Un miembro suspendido por el equipo de soporte sigue ocupando un puesto aquí, aunque el listado visible de miembros lo oculte — el reporte siempre coincide con la validación, nunca con el listado. Una invitación pendiente y no vencida reserva un puesto de la misma forma que lo hace para la validación; una invitación vencida que sigue en estado pendiente no reserva ninguno. Leer los puestos nunca modifica nada: no vence una invitación vencida, no revoca nada, ni rechaza por límite de puestos por sí misma. Un límite efectivo ilimitado se devuelve exactamente como lo resolvió el lector de entitlements.
Exportación del registro de auditoría de la organización
Sección titulada «Exportación del registro de auditoría de la organización»GET /api/v1/orgs/{orgUlid}/audit-events/export descarga el registro de auditoría de la organización como un único artefacto CSV — una fila por evento de auditoría, ordenado cronológicamente — pensado para un revisor de cumplimiento o seguridad que necesita “el último trimestre de actividad del workspace, como archivo”. Requiere membresía en la organización con rol owner o admin, y el entitlement auditExportEnabled (una capacidad del plan de pago). Ambos rechazos devuelven 403 { code: "forbidden" } y son indistinguibles en el cable — quien llama no puede deducir si la organización tiene el entitlement probando su propio rol, ni al revés.
Los parámetros from y to son obligatorios, en ISO-8601 UTC con el designador Z (una fecha sin hora u con desplazamiento horario se rechaza). La ventana es semiabierta — from es inclusivo, to es exclusivo — así que dos llamadas consecutivas donde el to de la primera coincide con el from de la segunda nunca producen un duplicado ni un hueco en el instante límite. El rango no puede superar los 366 días; un parámetro faltante, una ventana invertida (to <= from) o un rango excesivo devuelven 422 { code: "validation_failed" }.
Se calcula un conteo exacto de eventos que coinciden antes de leer ninguna fila. Una ventana cuyo conteo supera los 25.000 eventos se rechaza con 422 { code: "validation_failed" }, con un mensaje que nombra tanto el conteo real como el techo — reduce la ventana y vuelve a intentarlo. La exportación tiene su propio límite de 10 llamadas por organización por hora, en un cupo independiente del límite de exportación de casos de prueba descrito arriba; superarlo devuelve 429 { code: "too_many_requests" }.
La respuesta es Content-Type: text/csv; charset=utf-8 con Content-Disposition: attachment que nombra un archivo que incluye la ventana solicitada, de modo que dos exportaciones nunca coincidan en una carpeta de descargas. Columnas: created_at, event_ulid, action, actor_kind, actor_ulid, actor_label, entity_type, entity_ulid, entity_label, run_ulid, metadata. Una celda cuyo valor de origen pudiera interpretarse como una fórmula de hoja de cálculo (que empiece con = + - @) se neutraliza con un ' inicial antes de encomillarla. La columna run_ulid queda vacía para un evento que nunca estuvo asociado a una ejecución, contiene el ULID de la ejecución cuando esta todavía existe, y contiene el marcador literal [run purged] o [run unknown] para los dos casos en que la ejecución ya no existe pero el evento estaba (o podría haber estado) asociado a una ejecución. Un actor cuya identidad fue borrada se muestra como [identity removed] en actor_label, en lugar de cualquier dato personal resucitado. Exportar es una operación de solo lectura: nunca modifica ni elimina datos de auditoría, y llamarla no genera por sí misma un nuevo evento de auditoría.
Changelog
Sección titulada «Changelog»-
Ahora se puede cancelar un cambio programado de la suscripción desde la API.
DELETE /api/v1/orgs/{orgUlid}/subscription/pending-changedescarta un cambio programado para aplicarse al final del período (una reducción de puestos, o un cambio de plan diferido a la renovación) y deja vigente el compromiso actual, devolviendo el mismo reporte de suscripción que devuelven las demás mutaciones de esta superficie. Cuando no hay nada programado responde409 { code: "conflict" }sin enviar ningún cambio al proveedor de facturación. Requiereorg-settings.manage. -
Ahora se acepta cualquier compromiso de puestos positivo, y se retira el rechazo dedicado por uso superior. Cambio que rompe compatibilidad — se elimina el rechazo
409 { code: "seat_commitment_below_usage" }dePOST /api/v1/orgs/{orgUlid}/subscription/checkouty dePATCH /api/v1/orgs/{orgUlid}/subscription/seats. Ahora se acepta cualquier compromiso de puestos positivo; cuando los puestos comprometidos quedan por debajo de los puestos usados de la organización, Probara degrada a los miembros no propietarios más recientes (primero las invitaciones pendientes) al rol gratuitovieweren lugar de rechazar el cambio. -
Las configuraciones de ejecución ahora se pueden leer y modificar, a nivel de proyecto y de organización.
GET/PATCH /api/v1/projects/{projectId}/run-settingsyGET/PATCH /api/v1/orgs/{orgUlid}/run-settingsexponen doce configuraciones que gobiernan el marcado, la escritura en ejecuciones cerradas y los valores predeterminados de creación de runs. Once se resuelven medianteproyecto → organización → valor por defecto del código;defaultAssigneeUserIdes exclusiva del proyecto y no tiene nivel de organización. Junto con esto se publican tres cambios de comportamiento relacionados: (1) una ejecución cerrada sin abortar ahora acepta por defecto ediciones de notas de resultado y stage/commit/delete de adjuntos de resultado (allowResultsInClosedRunstiene por defectotrue) — una organización que prefiera el comportamiento anterior, más estricto, puede fijarlo enfalse; (2) marcar un caso de ejecución ahora rechaza con403 { code: "run_case_assignee_locked" }cuando la configuraciónassigneeResultLockdel proyecto está activa y quien llama no es el asignado ni está exento, sin exención para tokens de API; (3) marcar un caso de ejecución con un estado terminal ahora rechaza con422 validation_failed(details.code: "elapsed_ms_required") cuando la configuracióntimeTrackingdel proyecto esrequiredy la solicitud omiteelapsedMs— el marcado en bloque y el envío de resultados en bloque están exentos de este último rechazo. Consulta Configuración de ejecución y Casos de ejecución. -
Ahora se pueden crear invitaciones en bloque, hasta 50 direcciones por solicitud.
POST /api/v1/orgs/{orgUlid}/invitations/bulkacepta{ emails: string[], role, locale?, grantAllPrivateProjects? }— un solo rol y un solo idioma aplican a TODA la lista, nunca por dirección.emailsacepta como máximo 50 entradas; una dirección repetida en la misma lista se evalúa una sola vez, en el orden de su primera aparición. La respuesta es siempre{ results: [{ email, status }] }, una entrada por cada dirección distinta enviada, clasificada encreated,duplicate_pending,already_memberoinvalid_email. El techo de invitaciones pendientes de la organización y el límite de puestos se evalúan cada uno una sola vez, contra la cantidad de direcciones que realmente se crearían, nunca por dirección, y pueden seguir rechazando la solicitud completa con los mismos errores409de conflicto o límite de puestos que ya devuelve el endpoint de invitación individual.grantAllPrivateProjects(por defectofalse; también aceptado en el endpoint de invitación individual) es una intención de acceso a proyectos privados, no una concesión inmediata: cuando estrue, la persona invitada recibe una filagranteden cada proyecto privado en el momento en que acepta, reproducida de forma asíncrona a partir de la intención guardada en la invitación — nunca evaluada en el momento de invitar. Es un no-op para los rolesowner/admin, ya que esos roles ya sortean la lista de acceso de proyectos privados y no ganarían nada con una concesión explícita. La app web muestra esto como la casilla Acceso en las pestañas Individual y Masiva del diálogo de invitación: presente y deshabilitada con un motivo explícito cuando el rol seleccionado ya sortea la lista de acceso, de modo que el control nunca se oculta — solo queda inerte donde otorgarlo no haría nada. -
Ahora se puede leer el resultado final de entrega de correo de una invitación, y se retira el código de error dedicado a destinatario suprimido. Los elementos de
GET /api/v1/orgs/{orgUlid}/invitationsganaronemailDeliveryStatus:nullmientras el correo de la invitación sigue en cola o ya fue entregado,"suppressed"si la dirección del destinatario es permanentemente no entregable, o"failed"si la entrega se reintentó hasta el techo de reintentos de la cola. Esto es solo un modelo de lectura — el correo de invitación se entrega desde una cola interna después de la respuesta, así que ningún endpoint de invitación tiene un veredicto de entrega que reportar mientras su solicitud sigue abierta. Cambio que rompe compatibilidad — se elimina el conflicto dedicado409 { code: "email_recipient_suppressed" }tanto de la creación como del reenvío de invitaciones; un destinatario permanentemente no entregable ya no aparece como un código de error distinto en ninguno de los dos endpoints — ambos ahora responden su mismo estado de éxito tolerante de siempre (201/200) en cualquier resultado de envío, y el resultado se reporta después a través deemailDeliveryStatus. -
Ahora se pueden agregar en bloque las asignaciones de proyectos de un grupo, en el plano de grupo.
POST /api/v1/groups/{userGroupUlid}/projectsasigna uno o varios proyectos a un grupo en una sola solicitud ({ projectUlids: string[] }, hasta 200,.min(1)) — una operación nueva junto a los endpoints de grupo del plano de proyecto ya existentes, no un reemplazo de ellos. No lleva ninguna barrera de rol de organización propia: la autorización corre por proyecto, dentro de la misma transacción, con la misma regla que ya aplicaPOST /api/v1/projects/{projectId}/access/groups. La escritura es todo-o-nada — un proyecto no resoluble (404) o no autorizado (403) en cualquier parte del lote rechaza la solicitud completa y no confirma nada, incluso un proyecto anterior en el lote para el que el llamante sí estaba autorizado. Los ULID repetidos se deduplican a una sola asignación; un proyecto ya asignado nunca es un error y nunca bloquea el resto del lote. Ver Grupos de usuarios. -
Ahora se pueden agregar miembros de un grupo en lote, en una sola solicitud.
POST /api/v1/groups/{userGroupUlid}/membersahora también acepta{ userUlids: string[] }junto a la forma existente{ userUlid: string }— la misma operación y la misma ruta, no un endpoint nuevo. Un cuerpo que incluya ambas claves, o ninguna, se rechaza con422 { code: "validation_failed" }. La escritura es todo-o-nada: unuserUlidque ya dejó la organización (o nunca fue miembro) en cualquier posición del arreglo rechaza la solicitud completa con404 not_foundy no agrega a nadie, y una solicitud que no agregaría ninguna membresía nueva (todos los listados ya son miembros) responde409 { code: "conflict" }— igual que el comportamiento existente de un solo miembro. Los ULID repetidos se deduplican silenciosamente. Cada miembro efectivamente agregado sigue emitiendo su propio eventouser_group.member_added; un ULID filtrado (ya miembro) no emite ninguno. Ver Grupos de usuarios. -
Ahora se pueden incorporar las asignaciones de proyectos de un grupo de forma atómica al crearlo.
POST /api/v1/groupsacepta un arreglo opcionalprojectUlids, siguiendo la misma convención que el ya existentememberUserUlids: la fila del grupo y cada asignación incorporada se confirman o fallan todas juntas, los duplicados se deduplican silenciosamente, y un ULID de proyecto desconocido rechaza la creación completa con404 not_found. Una asignación incorporada nunca se rechaza por apuntar a un proyecto público. Ninguno de los dos campos se acepta enPATCH /api/v1/groups/{userGroupUlid}— una clave desconocida rechaza toda la solicitud. Ver Grupos de usuarios. -
Ahora se pueden asignar grupos de usuarios a la lista de acceso de un proyecto.
GET/POST /api/v1/projects/{projectId}/access/groupsyDELETE /api/v1/projects/{projectId}/access/groups/{userGroupUlid}reflejan el plano y la regla de autorización de la lista de acceso individual, yGET /api/v1/groups/{userGroupUlid}/projectsreporta los proyectos a los que está asignado un grupo, filtrados a lo que el miembro que llama puede descubrir por sí mismo.projectCountenGET/POST /api/v1/groupsyGET /api/v1/groups/{userGroupUlid}ya no es siempre0— ahora reporta el conteo real de asignaciones del grupo. Ver Grupos de usuarios y Acceso a proyectos. -
Ya está disponible un reporte de puestos (seats) con alcance de organización.
GET /api/v1/orgs/{orgUlid}/seat-usageinforma los puestos usados de la organización y su límite efectivo de puestos — la misma definición de puesto usado que compara la aplicación del límite de puestos, nunca un conteo recalculado o derivado del listado de miembros. Disponible para cualquier miembro autenticado de la organización, sin restricción de rol. Leerlo nunca modifica nada. -
Ya está disponible un endpoint de exportación del registro de auditoría de la organización.
GET /api/v1/orgs/{orgUlid}/audit-events/exportdescarga el registro de auditoría de la organización como CSV para una ventana obligatoria y acotada (máximo 366 días). Restringido por rol de organización (owner/admin) Y por el entitlementauditExportEnabled— ambos rechazos son403 forbiddene indistinguibles en el cable. Se calcula un conteo exacto antes de leer ninguna fila; una ventana con más de 25.000 eventos se rechaza con422 validation_failed, nombrando tanto el conteo real como el techo. Limitado a 10 exportaciones por organización por hora, de forma independiente al límite de exportación de casos de prueba. La exportación nunca modifica ni elimina nada. -
Ya está disponible un reporte de uso de resultados de API con alcance de organización.
GET /api/v1/orgs/{orgUlid}/api-result-usageinforma los resultados contabilizados con autoría de máquina de la organización durante el mes calendario actual, junto con su techo efectivoapiResultsPerMonth. Disponible para cualquier miembro autenticado de la organización, sin restricción de rol. Reporta el valor propio del contador autoritativo — nunca un conteo recalculado escaneando resultados — y leerlo nunca crea, reinicia ni avanza el contador. Al comenzar el siguiente mes calendario, el reporte muestra0para el nuevo periodo sin ninguna acción del operador ni despliegue. -
Ahora se aplica el límite mensual de resultados de API de la organización. Marcar un caso de ejecución (
PATCH /api/v1/runs/{runUlid}/cases/{runCaseUlid}constatus),bulk-markybulk-submit-resultahora rechazan una solicitud que llevaría los resultados contabilizados con autoría de máquina de la organización durante el mes calendario actual más allá de suapiResultsPerMonthefectivo, con409 { code: "api_result_limit_exceeded" }. Solo se contabiliza una solicitud autenticada con un token de API o un token de acceso OAuth; un llamador con cookie de sesión (incluida la propia aplicación web del producto) nunca se mide ni se rechaza por este motivo. Una solicitud masiva se rechaza por completo — nunca se aplica parcialmente. El rechazo no lleva encabezadoRetry-Aftery no revela ni tu techo ni tu uso actual; es distinto de429 too_many_requests(un limitador de ráfagas) y del409 conflictque ya devuelve una ejecución cerrada. El valor por defecto documentado es el techo mensual del plan Free (5000 resultados contabilizados); una integración por debajo de ese volumen no observa ningún cambio, y un plan de nivel superior o un override explícito sube el techo de una organización específica, sin ningún despliegue, y el techo se reinicia automáticamente al comenzar el siguiente mes calendario. -
Ahora se aplica el límite de tasa del plan de API de la organización. Cualquier solicitud a
/api/v1autenticada con un token de API o un token de acceso OAuth ahora cuenta contra el techo por minuto del plan de tu organización. Superarlo responde429 { code: "too_many_requests" }con un encabezadoRetry-Afterque indica los segundos enteros restantes hasta que se reinicie la ventana actual de 60 segundos — espera ese tiempo y la solicitud idéntica se admite, sin necesidad de más cálculo de reintento. Este techo se aplica únicamente al tráfico autenticado por credencial de máquina: un llamador con cookie de sesión (incluida la propia aplicación web del producto) nunca se rechaza por este motivo. El valor por defecto documentado es de 60 solicitudes por minuto; un plan de nivel superior o un override explícito sube el techo de una organización específica y desbloquea la solicitud idéntica previamente rechazada de inmediato, sin ningún despliegue. Una llamadatools/callde/mcpque despacha una o más solicitudes secundarias a/api/v1cuenta cada solicitud secundaria despachada por separado — y además — de los límites de ráfaga de/mcpya existentes, que permanecen sin cambios. -
El límite de proyectos ahora alcanza a los proyectos que la organización ya tiene. Una organización por encima de su límite efectivo de proyectos mantiene abiertos solo los creados más recientemente; todos los demás quedan bloqueados, y cualquier lectura o escritura sobre un proyecto bloqueado responde
403 { code: "project_locked" }, por/api/v1y por MCP, tanto con una sesión como con una clave de API.DELETE /api/v1/projects/{projectId}sigue funcionando — es la única operación que sirve un proyecto bloqueado, y eliminar uno reabre el siguiente más antiguo en cuanto el total vuelve a estar dentro del límite. El bloqueo no elimina nada:GET /api/v1/projectssigue devolviendo todos los proyectos, cada uno conlockedAt(milisegundos epoch, onull), y subir el límite borra todas las marcas y reabre los proyectos sin cambios. Un enlace público de una ejecución que pertenece a un proyecto bloqueado se colapsa en el mismo404 { code: "not_found" }, byte a byte, que un token desconocido, para que un enlace válido no sirva para detectar el estado. Esto sustituye el comportamiento solo-en-creación descrito en la entrada de abajo. -
Ahora se aplica el límite de proyectos de la organización.
POST /api/v1/projectsahora rechaza una solicitud que llevaría los proyectos usados de la organización más allá de su límite efectivo, con409 { code: "project_limit_exceeded" }, distinto del409 { code: "conflict" }que el mismo endpoint ya devuelve por un id de proyecto duplicado. El valor por defecto documentado es el techo de proyectos del plan Free (3); un plan de nivel superior o un override explícito sube el límite y desbloquea la solicitud idéntica sin ningún despliegue. En su momento, ningún proyecto existente se veía afectado por un límite reducido y solo se rechazaba la creación siguiente; eso ya no es así — ver la entrada sobre el bloqueo de proyectos más arriba. -
Invitar a una dirección que ya es miembro ahora se rechaza.
POST /api/v1/orgs/{orgUlid}/invitationsahora responde409 { code: "conflict" }cuando la dirección invitada ya tiene una membresía en esa organización, para cualquier rol, incluidoviewer. La operación correcta para alguien que ya está dentro de la organización es un cambio de rol (PATCH /api/v1/orgs/{orgUlid}/members/{userUlid}), no una invitación. Antes solo se rechazaba una segunda invitación pendiente para la misma dirección, así que una dirección cuya invitación anterior ya había sido aceptada podía volver a invitarse — y mientras esa invitación seguía pendiente, esa persona contaba dos veces contra el límite de puestos de la organización. La verificación está acotada a la organización que invita: una dirección que es miembro de otra organización sigue siendo invitable. -
Ahora se aplica el límite de puestos (seats) de la organización.
POST /api/v1/orgs/{orgUlid}/invitations(para un rol distinto deviewer) yPATCH /api/v1/orgs/{orgUlid}/members/{userUlid}(al ascender a unviewera un rol distinto deviewer) ahora rechazan una solicitud que llevaría los puestos usados de la organización más allá de su límite efectivo, con409 { code: "seat_limit_exceeded" }. El rolviewernunca consume un puesto y nunca se rechaza. Aceptar una invitación pendiente nunca se rechaza por límite de puestos — solo convierte una reserva ya contabilizada. -
Potencialmente disruptivo — El
PATCHde casos de prueba ahora exige el rol de escritura en el servidor.PATCH /api/v1/test-cases/{caseUlid}(yPATCH /defects/{defectUlid}) ahora rechazan a un caller con rolviewercon403 forbiddenantes de cualquier escritura, igualando el control ya aplicado a los valores de custom fields y a otros endpoints de escritura. Una API key con alcanceviewerque antes tenía éxito en estas dos rutas ahora recibe403. -
Breaking — Las ejecuciones se cierran solas; se elimina Reopen;
rerun-failedse generaliza enclone. Ahora una ejecución se cierra sola automáticamente en el instante en que todos los casos tienen resultado — no requiere llamada del cliente; el pestillo es de un solo sentido: nada dentro de una ejecución cerrada vuelve a abrirla.POST /api/v1/runs/{runUlid}/reopense elimina (404).POST /api/v1/projects/{projectUlid}/runs/{runUlid}/rerun-failedse renombra a.../cloney se generaliza: el cuerpo ahora es{ title: string, cloneAssignees?: boolean, statusFilter?: TestOutcomeStatus[] }(antes{ defaultAssigneeUlid? }, que solo clonaba casos fallidos/bloqueados/sin probar/reintentados). Una ejecución cerrada y no abortada ahora aceptaPATCH /runs/{runUlid}, agregar/quitar/marcar/reintentar casos y reconciliar — solo una ejecución abortada permanece congelada de forma terminal (409 conflict). -
Ejecuciones — vinculación estructurada de entorno.
POST /api/v1/projects/{projectUlid}/runsyPATCH /api/v1/runs/{runUlid}ahora aceptanenvironmentId(ULID). Las respuestas de ejecuciones exponen un objetoenvironment: { ulid, name, slug } | nullyenvironmentId. La cadenaenvironmentde texto libre sigue aceptándose en la creación por compatibilidad, pero no debe usarse en integraciones nuevas.GET .../runs/environmentsse mantiene pero está deprecado; filtra conenv=<environmentUlid>en su lugar. Ver API de entornos. -
Breaking — La ingesta de resultados ahora es un registro de solo-adición. Se elimina
PUT /api/v1/runs/{runUlid}/results/{caseUlid}(upsert idempotente). Registra resultados marcando casos de la ejecución (PATCH /api/v1/runs/{runUlid}/cases/{runCaseUlid}) o pasos;GET /api/v1/runs/{runUlid}/resultsahora devuelve el historial completo de marcados (varias filas por caso) conexecutedByUlid. Las ejecuciones gananstate(open/closed),description,defaultAssigneeUlidy contadores de progreso. -
Proyectos —
PATCH/DELETErestringidos a owner/admin.memberyviewerreciben403 forbiddenal mutar metadatos del proyecto. Usa token o sesión de propietario o administrador para renombrar el código del proyecto o eliminarlo. -
Breaking — Los campos de clasificación de los casos se mueven a custom-field-values.
POST/PATCHsobre/api/v1/projects/{projectId}/test-casesy/api/v1/test-cases/{caseUlid}ya no aceptanpriority,severity,status,type,layer,behavior,automationStatus,isFlaky,preconditionsnipostconditions. Léelos desde el arraycustomFieldValuesen la respuesta del caso; escríbelos víaPUT /api/v1/test-cases/{caseUlid}/custom-field-values. Los valores del enum siguen siendo las opciones sembradas en los custom fields del sistema correspondientes.