API REST pública — v1
Acceso programático a los mismos primitivos de QA que expone la UI de ArtificialQA. Disparar test executions desde CI/CD, integrar evaluación en una plataforma de agentes interna, o construir dashboards sobre tus runs.
/api/v2/public/. Los cambios aditivos (nuevos endpoints, nuevos campos, nuevos params opcionales) salen sin bump de versión. Revisión actual: v1.6.4.
Base URLs
- Producción:
https://app.artificialqa.com/api/v1/public - Local dev:
http://localhost:7878/api/v1/public
OpenAPI spec: /openapi.yaml — pasalo a cualquier herramienta OpenAPI (Postman, Stoplight, Speakeasy, openapi-generator, etc.) para generar un cliente tipado.
¿Quieres una superficie chat-friendly? Los mismos primitivos están expuestos sobre Model Context Protocol en /api/v1/mcp. Usá esa ruta para integrar con Claude Desktop, Cursor, Continue o cualquier agente compatible con MCP.
1. Autenticación
Cada request necesita una API key en el header Authorization:
Authorization: Bearer aqa_live_xxxxxxxxxxxxxxxxxxxxxxxxx
Generar una key
- Inicia sesión en ArtificialQA → clic en tu avatar → API Keys & MCP → pestaña API Keys → New API Key.
- Elige el proyecto al que la key queda atada. Solo va a poder leer/escribir datos dentro de ese proyecto.
- Elige un scope:
read— solo GET. Para dashboards, reporting, integraciones read-only.write— GET + POST. Necesario para disparar ejecuciones / evaluaciones.
- Elige una fecha de expiración (v1.6.3 — obligatoria). Mínimo 1 día, máximo 12 meses desde hoy. Default 6 meses. No hay opción de "no expira nunca" — anotá un recordatorio para rotar.
- Copia la key en texto plano — se muestra una sola vez. Guárdala en un secret manager.
Quién puede crear keys:
org_adminde la organización dueña de la key — sí, en cualquier proyecto.super_admin— sí (bypass).project_admin— no (sí podía en v1.6.2, sacado en v1.6.3). Los project admins pueden ver el listado de keys de su org pero ya no pueden crearlas — las keys estáticas son una credencial de servicio a nivel organización.tester/auditor/member— no.
Las keys tienen scope por proyecto (v1.6.2). Nunca exponen datos de otro proyecto, ni entre organizaciones ni entre proyectos de la misma organización. Si necesitas acceso a múltiples proyectos, creá una key por proyecto. ¿Perdiste una key? Revócala desde la misma UI y genera una nueva.
Semántica de audit — las keys actúan como servicio (v1.6.3)
Cuando una API key crea o modifica datos vía esta API REST, el audit log registra el actor como service:<prefix de la key>, no el email del humano que la generó. El humano que la creó queda capturado aparte en details.createdByUserId para trazabilidad. De un vistazo el auditor ve "esto lo hizo una máquina, esta es la key" en lugar de atribuir engañosamente miles de operaciones de CI a una sola persona.
Las entradas legacy (pre-v1.6.3) conservan su string original apikey:<prefix> — el discriminador en la tabla api_keys (actorKind) le dice al audit writer qué formato emitir.
Binding por proyecto (v1.6.2)
Cada API key está atada a exactamente un proyecto. Comportamiento:
- Los endpoints que leen o escriben recursos con scope de proyecto (TestCases, TestSuites, TestPlans, AgentConnections, Executions, EvaluationRuns, etc.) filtran automáticamente por el proyecto de la key. No hace falta pasar
projectId. - Los endpoints que aceptan
projectIden el body (ej.POST /test-cases,POST /test-plans) lo tratan como opcional:- Omitido → usa el binding de la key implícitamente.
- Coincide con el binding → ok, aceptado.
- No coincide →
400 validation_failedcon ambos ids en el campodetail.
- Lecturas cross-project sobre un id de recurso →
404 not_found. La fila existe en la DB pero la key no la ve. - Las idempotency keys son con scope por proyecto: dos proyectos de la misma org pueden reutilizar el mismo string sin colisionar.
- Los recursos a nivel organización (Evaluators — globales y custom de la org; Subscription; AI providers) quedan con scope por organización. La key con scope de proyecto igual los ve.
OAuth (solo MCP)
Hay un segundo camino de autenticación — tokens Bearer oat_* — disponible exclusivamente en el endpoint MCP (/api/v1/mcp). Es el camino que usan Claude Desktop, Cursor y otros clientes MCP manejados por personas: en lugar de pegar una key estática, el usuario hace login en ArtificialQA en el navegador y elige un proyecto en una pantalla de consentimiento. La API REST (/api/v1/public) sigue usando solo keys estáticas aqa_* — los tokens OAuth no se aceptan en las rutas REST. Configuración: Conectar por MCP.
Errores
| Status | Code | Cuándo |
|---|---|---|
| 401 | missing_token | Falta el header Authorization. |
| 401 | invalid_token | Key no encontrada o malformada. |
| 401 | token_revoked | Key revocada desde la UI. |
| 401 | token_expired | Key pasó su expiresAt. |
| 403 | insufficient_scope | Key read intentó un POST. |
2. Convenciones
Asíncrono + polling
Todos los endpoints que cambian estado son asíncronos. El POST devuelve 202 Accepted inmediatamente con un executionId / evaluationId y un statusUrl. Los clientes hacen polling al GET hasta que el status llega a estado terminal.
| Endpoint | Estados terminales |
|---|---|
/executions/{id} | completed, failed, cancelled |
/evaluations/{id} | completed, failed |
No hay webhooks en v1.
Cadencia de polling
Un cliente razonable hace polling con backoff exponencial, capeado en 10 segundos:
delay = min(10, 1.5 ** intento) segundos
La mayoría de las ejecuciones terminan en menos de 5 minutos; las ejecuciones browser con Playwright pueden correr 15+ minutos.
Errores — RFC 7807 problem+json
Cada respuesta de error tiene content-type: application/problem+json y este body:
{
"type": "https://docs.artificialqa.com/errors/<code>",
"title": "Resumen corto legible por humanos",
"status": 404,
"code": "not_found",
"detail": "Explicación verbose opcional."
}
Haz switch sobre code, no sobre title o detail. Los titles pueden reescribirse entre releases; los codes son parte del contrato.
Catálogo completo:
| Status | Code | Significado |
|---|---|---|
| 400 | missing_path_param | URL malformada — típicamente un UUID faltante. |
| 400 | validation_failed | Body, query o header no pasó validación. |
| 401 | missing_token / invalid_token / token_revoked / token_expired | Auth (ver sección 1). |
| 402 | quota_exceeded | Cuota mensual alcanzada (ejecuciones, evaluaciones o tokens). |
| 403 | insufficient_scope | El scope de la key es read pero la ruta requiere write. |
| 403 | mcp_disabled | La organización tiene la feature MCP apagada (solo ruta MCP). |
| 404 | not_found | El recurso no existe en esta org / proyecto. |
| 409 | invalid_state | El recurso está en un estado que bloquea la operación. |
| 409 | empty_plan | El plan tiene cero test cases activos. |
| 409 | evaluation_in_progress | Hay una evaluación previa sobre esta ejecución todavía pending o running. |
| 409 | no_evaluators | La org no tiene evaluadores activos configurados. |
| 409 | duplicate_membership | La fila de membership ya existe (suite item o plan-suite link). |
| 429 | rate_limit_exceeded | Rate limit por key alcanzado. |
| 500 | internal_error | Error inesperado del servidor — abrí un ticket. |
Idempotencia
Los endpoints POST aceptan un header Idempotency-Key (1-255 chars). Reutilizar la misma key dentro del proyecto devuelve la fila existente con idempotent: true y HTTP 200 en lugar de crear un duplicado y devolver 202.
POST /api/v1/public/test-plans/<id>/executions
Authorization: Bearer aqa_live_...
Content-Type: application/json
Idempotency-Key: ci-build-4815162342
{ "agentConnectionId": "..." }
Las idempotency keys tienen scope por proyecto (v1.6.2), persisten indefinidamente. Un patrón típico es usar el CI build ID, el commit SHA, o un UUIDv4 generado por intento. Para POST /test-cases/import el TTL del cache es 24h.
idempotencyKey. Si mandas ambos, gana el header. Migrá al header.
Rate limits
Cada key tiene rate limit independiente. En cada respuesta:
X-RateLimit-Limit: <max-por-ventana>
X-RateLimit-Remaining: <restante-en-ventana>
X-RateLimit-Reset: <unix-seconds-cuando-resetea>
Un 429 agrega Retry-After: <segundos>. El body es un problem+json estándar con code: "rate_limit_exceeded". El pool de rate-limit es compartido entre keys estáticas aqa_* y tokens OAuth oat_* — ambos llaveados por prefix.
Cuotas
Las ejecuciones, evaluaciones y tokens LLM cuentan contra la cuota mensual del plan de la org. Pegarle a cualquiera devuelve 402 quota_exceeded. El body indica qué cuota:
{
"code": "quota_exceeded",
"detail": "Monthly execution limit reached (250/250)."
}
3. El flujo
El flujo estándar de integración con CI es 4 llamadas:
1. GET /test-plans descubrir id del plan
2. GET /agent-connections descubrir id de la conexión
3. POST /test-plans/{id}/executions disparar ejecución
(opcional evaluate=true)
4. GET /executions/{id} (polling) esperar estado terminal
Si evaluate: true se seteó en el paso 3, la evaluación corre automáticamente después usando todos los evaluadores permitidos por el tier de la org — GET /executions/{id}.evaluation trae el resumen. Si no:
5. (opcional) GET /evaluators descubrir ids de evaluadores
6. POST /executions/{id}/evaluations disparar evaluación explícitamente
(opcional con evaluatorIds)
7. GET /evaluations/{id} (polling) esperar estado terminal
El happy path más corto (evaluate: true) es 2 llamadas + polling.
4. Endpoints
Test plans
GET /test-plans
Lista cada plan en el proyecto de la API key.
Query params: status, limit.
Response 200 — array de TestPlanSummary:
[
{
"id": "744cd22e-bf76-4e8c-8060-9f283a64796c",
"name": "TP_Browser_ArtQA",
"description": null,
"status": "completed",
"environment": null,
"defaultAgentConnectionId": "12b17e7b-...",
"suiteCount": 3,
"executionCount": 7,
"createdAt": "2026-05-21T13:44:09.103Z",
"updatedAt": "2026-06-10T17:01:55.211Z"
}
]
GET /test-plans/{id}
Detalle con desglose de suites. Útil para elegir un plan en una UI.
POST /test-plans — crear (v1.4)
Crea un plan vacío. Requerido: name. Opcional: description, status (default "draft"), agentConnectionId, environment, scheduleCron, projectId (auto si la org tiene exactamente un proyecto activo; requerido si no — el error enumera candidatos). El runner es el único que puede escribir running y completed; create acepta solo {draft, ready}.
Response 201 — TestPlanDetail completo con createdBy="apikey:<prefix>", suites=[], executionCount=0.
PATCH /test-plans/{id} — actualizar (v1.4)
Update parcial. Patcheables: name, description, status (subset {draft, ready, archived}), agentConnectionId, environment, scheduleCron. No patcheables: projectId (huérfanaría las memberships de suite), createdBy, createdAt, isActive (usar DELETE).
PATCH sobre un plan en running se rechaza con 409 invalid_state — el runner está a mitad del snapshot.
DELETE /test-plans/{id} — soft-delete (v1.4)
Pone isActive=false. Idempotente. Las ejecuciones pasadas que snapshottearon el plan siguen funcionando. Rechazado con 409 invalid_state si el plan está actualmente running.
POST /test-plans/{id}/suites — enlazar una suite (v1.4)
{ "testSuiteId": "uuid", "sortOrder": 5 }
sortOrder opcional — si lo omites el servidor asigna MAX(sortOrder)+1. El link de suites tiene scope por proyecto: link cross-project → 409 invalid_state con ambos project ids en detail. Link duplicado → 409 duplicate_membership.
DELETE /test-plans/{id}/suites/{suiteId} — desvincular (v1.4)
Quita el enlace. Idempotente. Actualiza plan.updatedAt. Rechazado con 409 invalid_state si el plan está actualmente running.
Forma de TestPlanSummary:
{
id: uuid,
name: string,
description: string | null,
status: "draft" | "ready" | "running" | "completed" | "archived",
environment: string | null,
defaultAgentConnectionId: uuid | null,
suiteCount: int,
executionCount: int,
createdAt: ISO,
updatedAt: ISO,
}
Test suites
GET /test-suites
Lista paginada con cursor: { data: TestSuiteSummary[], nextCursor: string | null }. Query params: cursor, limit (1-200, default 50), projectId, containsTestCaseId, tags, search, createdAfter, updatedSince, isActive.
GET /test-suites/{id}
Detalle completo. items[] ordenado por (sortOrder ASC, id ASC), cada uno con un summary embebido del testCase. 404 sobre suites soft-deleted salvo que pases ?isActive=false.
POST /test-suites
Crea una suite vacía. Requerido: name. Opcional: description, tags, projectId (auto si la org tiene un proyecto activo; requerido si no). La membership va por los endpoints sub-resource de abajo — por diseño, defines la estructura por separado del contenido.
PATCH /test-suites/{id}
Update parcial. Patcheables: name, description (manda null para limpiar), tags, isActive. No patcheables: projectId, testCount (mutado solo vía endpoints de membership), createdBy, createdAt.
DELETE /test-suites/{id}
Soft-delete. Idempotente. Las join rows sobreviven, así que cualquier plan que referenciara la suite sigue resolviendo.
POST /test-suites/{id}/test-cases
Agrega una membership.
{ "testCaseId": "uuid", "sortOrder": 5 }
Scope por proyecto: un test case solo puede agregarse a una suite del mismo proyecto. Add cross-project → 409 invalid_state. Duplicado → 409 duplicate_membership.
DELETE /test-suites/{id}/test-cases/{tcId}
Quita una membership. Idempotente. Quitar un TC que no era miembro devuelve 204 sin escrituras en DB.
Test cases
CRUD completo más bulk import. Permite que un CI / IaC / script de agente autore y curate test cases sin tocar la UI.
Decisiones de contrato:
typees"simple"(un input/output) o"conversational"(diálogo multi-turn). El valor a nivel DBagent_taskse rechaza en el boundary público por diseño.sourcees siempre"manual"(create individual) o"imported"(bulk). El caller no puede setearlo."generated"está reservado para el runner del generador.reviewStatuses siempre"approved"en test cases autorados por API. PATCH no puede cambiarlo.- Solo soft-delete.
DELETEponeisActive=falseylifecycleStatus="archived". La fila queda para que las ejecuciones / suite items conserven sus FK. design(JSONB) es la forma unificada. Las columnas legacyinput/expectedOutput/turnsconviven para compat — cada lectura devuelve ambas formas; las escrituras aceptan cualquiera, condesignganando si se mandan ambas.- Auto-detección de PII corre del lado servidor en cada create y en cada PATCH que toque un campo con input. Los resultados van a
piiDetected+piiTypesy están expuestos también en el summary de listado.
GET /test-cases
Lista paginada con cursor: { data: TestCaseSummary[], nextCursor: string | null }.
Query params: cursor, limit (1-200, default 50), suiteId, type, difficulty, lifecycleStatus, reviewStatus, source, industryId, piiDetected, tags (repetido, ANY-match), search, createdAfter, updatedSince, isActive.
GET /test-cases/{id}
Detalle completo — cada columna de TestCase menos campos encriptados / internos. Devuelve tanto la forma legacy como la design.
POST /test-cases
Requerido: type más O design, O input/expectedOutput, O turns (para conversational). El boundary Zod atrapa el caso conversational-sin-turns antes de cualquier escritura en DB.
Resolución de proyecto: si la org tiene exactamente un proyecto activo, projectId se elige automáticamente. Si no, es requerido y el detail del error enumera los proyectos disponibles con (id, name).
Response 201 — TestCaseDetail completo. source=manual, reviewStatus=approved.
PATCH /test-cases/{id}
Update parcial. El body tiene que tener al menos un campo. No se puede cambiar type, source, ni reviewStatus. El PII se re-escanea solo cuando el patch toca input / expectedOutput / turns / design / tags.
DELETE /test-cases/{id}
Soft-delete. Idempotente — re-DELETE sobre una fila archivada devuelve el mismo 204 sin escritura.
POST /test-cases/import — bulk
Hasta 500 items por request. Independencia por fila: una fila mala va a errors[] y el batch sigue.
Idempotencia: con Idempotency-Key el call es replay-safe — cacheado contra (project, key, "test_case") en la tabla bulk_imports por 24 horas. Una invocación repetida dentro de esa ventana devuelve el resultado original verbatim con idempotent: true y cero mutaciones en DB.
{
"createdIds": ["uuid-1", "uuid-2", ...],
"errors": [{ "index": 2, "code": "not_found", "detail": "industryId ... does not exist" }],
"idempotent": false
}
Status codes: 200 éxito total (o replay del cache), 207 Multi-Status éxito parcial (el mismo status aplica en replay), 400 rechazo de schema, 402 cuota.
Agent connections
GET /agent-connections
Lista conexiones. Nunca devuelve secrets — solo los campos necesarios para elegir una conexión.
Query params: protocol (http | browser | websocket), isActive.
[
{
"id": "12b17e7b-2cec-4206-986d-9df390be2de3",
"name": "ArtificialQA Test Bank",
"protocol": "browser",
"baseUrl": "https://app.artificialqa.com",
"isActive": true,
"createdAt": "2026-05-19T09:12:11.502Z"
}
]
GET /agent-connections/{id} — detalle (v1.5)
Devuelve el AgentConnectionDetail completo con secrets enmascarados como "***<last4>". Útil para inspeccionar la config existente antes de un PATCH.
POST /agent-connections — crear (v1.5)
Requerido: name, protocol, baseUrl, authConfig, messageConfig. Opcional: description, environment (default "production"), preChatConfig, postChatConfig, templateVars, environments, projectId.
El caller manda secrets en texto plano en los blobs de config; el helper los encripta (AES-256-GCM) antes del INSERT y los guarda como enc:<iv>:<tag>:<ciphertext>.
PATCH /agent-connections/{id} — actualizar (v1.5)
Update parcial. Patcheables: name, description, baseUrl, environment, isActive, authConfig, preChatConfig, messageConfig, postChatConfig, templateVars, environments. No patcheables: projectId, protocol.
Round-trip de secrets enmascarados: mandar un valor enmascarado (ej. "***c123") en una ruta de secret-named cae al valor desencriptado de la DB existente — puedes leer el detalle, editar cualquier campo en texto plano, y volver a hacer PATCH de toda la config sin manejar manualmente secrets. Solo los valores NUEVOS en texto plano sobreescriben el secret guardado.
Los blobs de config se reemplazan por key, no se hace deep-merge. Si omites una key en tu PATCH, se pierde de la columna. null explícito limpia un blob nullable.
DELETE /agent-connections/{id} (v1.5)
Soft-delete. Idempotente. Las ejecuciones / plans pasados siguen funcionando. Para reactivar: PATCH { "isActive": true }.
POST /agent-connections/{id}/test — smoke-test (v1.5)
Smoke-test sincrónico contra el agente configurado. Body opcional: { "runtimeVars": { ... } } mergeado sobre los templateVars persistidos solo para este call.
Comportamiento por protocolo:
http— timeout de 30s. Devuelve latencia, status HTTP upstream y una respuesta truncada endetails.browser— levanta Chromium. Puede tardar 30-60s; configurá el timeout del cliente en ~90s.websocket— no implementado todavía. Devuelve{ status: "completed", ok: false, error: "websocket protocol test not supported in v1.5" }.
Rate limit de 1 call por 60s por (connectionId, apiKeyId).
{
"status": "completed",
"ok": true,
"latencyMs": 1234,
"error": null,
"details": { "protocol": "http", "statusCode": 200, "response": "..." },
"startedAt": "2026-06-17T10:00:00.000Z",
"completedAt": "2026-06-17T10:00:01.234Z"
}
status es un discriminador. v1.5 siempre setea "completed" porque el endpoint es sincrónico. Haz pattern-match sobre status como union para que un futuro "pending" + polling async no rompa los clientes.
Ejecuciones
POST /test-plans/{id}/executions — disparar
Arranca un run en background. Devuelve 202 con el nuevo id; 200 si es idempotente.
{
"agentConnectionId": "12b17e7b-...",
"evaluate": true,
"evaluatorIds": ["ad12-...", "be34-..."],
"evaluatorWeights": { "ad12-...": 2.0 },
"runtimeVars": { "documentId": "doc-42" }
}
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
agentConnectionId | uuid | sí | — |
evaluate | bool, default false | no | Dispara automáticamente la evaluación cuando termina la ejecución. |
evaluatorIds | uuid[] | no | Solo se respeta cuando evaluate: true. Restringe la auto-eval. Omitir → corren todos los evaluadores permitidos por el tier. |
evaluatorWeights | {uuid: number > 0} | no | Solo se respeta cuando evaluate: true. Override de peso por evaluador. Las omitidas usan el peso default configurado del evaluador. |
runtimeVars | {string: string} | no | Substituciones de template vars para la agent connection. |
idempotencyKey | string, deprecado | no | Preferí el header Idempotency-Key. |
Response 202:
{
"executionId": "9e2f-...",
"status": "pending",
"statusUrl": "https://app.artificialqa.com/api/v1/public/executions/9e2f-..."
}
Response 200 (replay idempotente): misma forma más "idempotent": true.
409 comunes: invalid_state (plan no en ready/completed), empty_plan.
GET /executions — list
Filtros: testPlanId, status, limit. Devuelve ExecutionSummary[] — misma forma que el detalle pero sin results[].
GET /executions/{id} — detalle
Devuelve la ejecución + cada resultado + la evaluación más reciente (si hay).
{
"id": "9e2f-...",
"testPlanId": "744cd22e-...",
"agentConnectionId": "12b17e7b-...",
"runNumber": 8,
"status": "completed",
"totalCases": 12,
"completedCases": 12,
"failedCases": 1,
"durationMs": 184320,
"errorMessage": null,
"createdAt": "2026-06-10T17:02:11.040Z",
"completedAt": "2026-06-10T17:05:15.360Z",
"results": [
{
"id": "...",
"testCaseId": "...",
"executionStatus": "SUCCESS",
"responseValidity": "VALID",
"responseTimeMs": 8234,
"success": true,
"retryCount": 0,
"finalized": true,
"createdAt": "..."
}
],
"evaluation": {
"id": "ad12-...",
"status": "completed",
"runNumber": 1,
"overallScore": 0.87,
"passRate": 0.92,
"passed": true,
"totalCases": 12,
"passedCases": 11,
"failedCases": 1,
"durationMs": 32104
}
}
executionStatus: SUCCESS, ERROR, TIMEOUT, SKIPPED. responseValidity: VALID, EMPTY, MALFORMED. Solo los resultados SUCCESS + VALID entran a evaluación.
Evaluadores
GET /evaluators
Lista los evaluadores visibles para la org — los globales de la plataforma más cualquier evaluador custom de la org. Úsalo para descubrir el id que pasas como evaluatorIds.
Query params: includeBlocked (default true).
Seguridad. Nunca expone el agentConfig del evaluador (credenciales encriptadas) ni el systemPrompt (prompt de calibración).
| Campo | Notas |
|---|---|
isGlobal | true para los globales de plataforma; false para custom de la org (Enterprise). |
planAllowed | true si tu tier de billing permite este evaluador. false significa que un POST /evaluations que lo referencie lo va a descartar silenciosamente del run. Filtra sobre este campo del lado del cliente. |
weight | Peso default aplicado al calcular el overall score. Override por run vía evaluatorWeights en el POST. |
Evaluaciones
POST /executions/{id}/evaluations — disparar
Corre los evaluadores configurados sobre una ejecución completed. Body (todo opcional):
{
"evaluatorIds": ["uuid", "uuid"],
"evaluatorWeights": { "uuid": 2.0 }
}
Si se omite evaluatorIds, corren todos los evaluadores activos permitidos por el tier. El default de pesos coincide con la UI: cualquier evaluador no presente en evaluatorWeights usa su peso default configurado de GET /evaluators.
409 comunes: invalid_state (ejecución todavía no completed), evaluation_in_progress, no_evaluators.
GET /evaluations/{id} — detalle
Devuelve el run + cada score por test case. Mientras status: "running" la respuesta incluye scoresCompleted / scoresTotal para una progress bar.
{
"id": "ad12-...",
"executionId": "9e2f-...",
"runNumber": 1,
"status": "completed",
"overallScore": 0.87,
"passRate": 0.92,
"passed": true,
"totalCases": 12,
"passedCases": 11,
"failedCases": 1,
"durationMs": 32104,
"createdAt": "...",
"completedAt": "...",
"scores": [
{
"id": "...",
"evaluatorId": "...",
"evaluatorName": "Tone",
"evaluatorSlug": "tone",
"weight": 1.0,
"testCaseId": "...",
"score": 0.83,
"passed": true,
"explanation": "Response stayed polite and professional throughout.",
"createdAt": "..."
}
]
}
scores[].weight es el peso de runtime que efectivamente se aplicó al calcular overallScore. Si pasaste evaluatorWeights en el trigger, coincide con ese override; si no, coincide con el default configurado del evaluador. En evaluaciones legacy puede ser null.
Reportes de evaluación (v1.6)
4 endpoints + 2 endpoints PDF que exponen el pipeline de reportes de evaluación: resumen ejecutivo, análisis por evaluador, scores por test case y un PDF. GET /report es read-only — nunca llama al LLM, nunca consume tokens, los resúmenes faltantes salen como strings vacíos ("").
Decisiones de contrato:
- Solo evaluaciones completas reportan. Cada endpoint rechaza
running/failed/pendingcon409 invalid_state. - Los POST consumen tokens. Los dos endpoints de regenerar gastan contra la cuota mensual de
tokensy requieren la featureai_evaluation_reportsdel plan.402 quota_exceededen cualquiera de los gates. - Cache-gated por default. El mismo
(evaluationId, lang)o(evaluationId, evaluatorId)devuelve el resumen cacheado concached: trueytokensUsed: null. Pasaforce: truepara bypassear. langes"en" | "es"únicamente. Cada idioma se cachea independientemente para el resumen ejecutivo; sin fallback.- La key del cache por evaluador es
(evaluationId, evaluatorId)— el idioma NO es parte. Regenerar en EN y después en ES SOBREESCRIBE la fila EN.
GET /evaluations/{id}/report
Devuelve el reporte JSON consolidado. Read-only.
GET /evaluations/{id}/report-pdf
Devuelve el PDF como application/pdf con Content-Disposition: attachment; filename="report_<plan>_eval<runNumber>_<YYYY-MM-DD>.pdf". Read-only.
curl -H "Authorization: Bearer aqa_xxx" \
https://app.artificialqa.com/api/v1/public/evaluations/<id>/report-pdf \
-o report.pdf
POST /evaluations/{id}/report-pdf-url — URL tokenizada (v1.6.1)
Emite una URL tokenizada de corta duración (TTL 1h) que descarga el PDF sin necesidad de header Authorization. Útil para pasarle un link de clic-y-listo a una persona o a un cliente LLM que no puede hacer round-trip con el Bearer token.
{
"url": "https://app.../api/v1/public/downloads/<token-de-43-chars>",
"expiresAt": "2026-06-17T18:30:00.000Z",
"expiresInSec": 3600
}
La URL es multi-uso dentro del TTL, con scope por organización, propósito único (desbloquea UN solo PDF de evaluación), y Cache-Control: no-store. NO gasta tokens y NO requiere ningún feature gate.
GET /downloads/{token} — público, sin Authorization
Transmite el binario PDF. No se requiere API key — el token es la auth. Token faltante / malformado / expirado → 404 unificado (NO 410) para que la superficie no filtre la existencia de tokens pasados o ajenos.
POST /evaluations/{id}/summary
Regenera el resumen ejecutivo. Body (opcional): { "lang": "en", "force": false }.
{
"summary": "Generated text...",
"providerName": "saia",
"model": "agent-v1",
"cached": false,
"tokensUsed": 1234
}
POST /evaluations/{id}/evaluator-summary
Regenera el análisis de un evaluador dentro de esta evaluación. Body: { "evaluatorId": "uuid", "lang": "en", "force": false }. evaluatorId requerido. Misma forma SummaryResult que la variante ejecutiva.
5. Ejemplos de código
Hay ejemplos ejecutables disponibles en tres sabores, cada uno implementa el flujo completo — descubrir → disparar → polling → imprimir scores:
- curl — pipeline de shell usando
curl+jq. - Node — Node.js 20+ con
fetchbuilt-in. - Python — Python 3.10+ con
httpx(orequests).
Para un cliente tipado en cualquier lenguaje, generalo desde la spec OpenAPI.
6. Límites y edge cases
- Timeouts. Las ejecuciones pueden correr hasta 30 minutos (el test browser más largo que vimos fue ~15 min). Más allá de eso el runner marca la ejecución como
failed. Haz polling acorde. - Cancelación. No hay endpoint público de cancel en v1 — cancelá desde la UI.
- Datos cross-project / cross-org. Una key del proyecto A no puede ver nada del proyecto B. Devuelve
404 not_found(no403) para no filtrar qué UUIDs existen. - Ejecuciones concurrentes en el mismo plan. Permitido. El runner serializa internamente solo cuando ambas apuntan a la misma conexión browser en el mismo worker.
- Webhooks. No hay en v1. Haz polling del status endpoint y emití tu propio webhook downstream.
- Guard SSRF (v1.6.3). Las agent connections que apunten a rangos privados/reservados (RFC1918, loopback, link-local incl. IMDS, IPv6 ULA/link-local) o a schemes que no sean
http(s)://se bloquean en create-time, update-time, el endpoint sync de test, y en runtime de ejecución. Se manifiesta como400 validation_failedcon motivo + campo ofensivo. Escape hatch:Organization.allowPrivateAgentConnections(solo org_admin).
evaluatorIds, el pre-check sincrónico verifica que los ids pertenezcan a tu org pero no aplica el whitelist del tier de billing — eso corre async en el runner. Efecto neto: los ids con planAllowed: false se descartan silenciosamente y el scores[] simplemente los omite. Si todos los ids pedidos se descartan, la evaluación termina con status: "failed" y errorMessage: "No active evaluators configured". Filtra del lado del cliente sobre planAllowed: true de GET /evaluators antes de mandar. Fix en el roadmap v1.x.