17Trabajos, resultados y reintentos

Sección 17

Trabajos, resultados y reintentos

.md

Toda petición a /v1 crea un trabajo, y su identificador viaja en el sobre como job_id. En las cinco operaciones síncronas eso es un detalle de contabilidad; en las dos asíncronas es el hilo por el que se sigue la operación.

GET /v1/jobs/{job_id}

Es la red de seguridad de lectura: quien no monte receptor de webhooks tiene que poder preguntar por su trabajo y recibir una respuesta que distinga todos los casos.

curl -sS -X GET "$ASIXTO_BASE_URL/v1/jobs/job_9f2a7c41" \
  -H "Authorization: Bearer $ASIXTO_API_KEY"
JSON
{
  "request_id": "req_9c67575147ae560b557cf5f3",
  "job_id": "cd5e91f3-371e-4ffc-b168-a0beb22f31a5",
  "status": "succeeded",
  "operation": "agent_turn",
  "environment": "live",
  "created_at": "2026-08-30T00:43:59.601Z",
  "started_at": "2026-08-30T00:43:59.640Z",
  "finished_at": "2026-08-30T00:44:01.354Z",
  "result_state": "available",
  "result": { "status": "completed", "stream_id": "cd5e91f3-…", "last_sequence": 1 },
  "usage": { "operation": "agent_turn", "unit": "request", "quantity": 1, "credits": 11 },
  "credits_charged": 11,
  "error": null
}

Necesita el permiso jobs:read. Consultar no es ejecutar, y por eso no reutiliza el permiso de la operación que creó el trabajo: es la única entrada de la superficie que no gasta créditos ni cupo, así que puede concedérsela a su panel de soporte sin concederle marcar llamadas ni sintetizar audio.

Los cuatro campos que comparte con el sobre —request_id, job_id, usage y result— significan lo mismo y se llaman igual, para que escriba una sola capa de transporte y la use en el acuse, en la repetición de una clave y en esta consulta.

request_id aquí es el de esta consulta, no el de la petición que creó el trabajo.

Lo que no se publica, y es deliberado: la entrada de su petición —que no se persiste— y la clave de API que lo creó. El environment sí, porque un trabajo no cruza de entorno y una integración que promociona de pruebas a producción necesita poder verlo.

status: el ciclo de vida

ValorQué significa
queuedAceptado, sin empezar
runningEn curso
succeededTerminó bien
failedTerminó con un fallo. error.code dice cuál
expiredSe agotó su plazo sin cerrarse

Los tres últimos son terminales. expired no es lo mismo que failed: un trabajo cuyo plazo vence libera su reserva de créditos sin que nadie lo haya cerrado, y confundirlo con un fallo haría imposible auditar esa liberación.

result_state: cinco casos, no un result que a veces es nulo

Un nulo colapsaría situaciones cuya respuesta correcta es distinta.

ValorQué significaQué hacer
pendingEl trabajo no ha terminadoVuelva a consultar
availableEl resultado está en resultÚselo
expiredVenció su ventana de retenciónPida la operación con una clave nueva
not_storedEsa operación no guarda su resultado por diseñoPídala con una clave nueva
unavailableTerminó sin resultado utilizableMire status y error

La diferencia entre expired y not_stored importa: en el primer caso hubo un resultado y se purgó; en el segundo nunca hubo copia. Hoy not_stored solo lo produce la síntesis de voz.

Cuánto vive el resultado

El resultado tiene una ventana de retención de 24 h, contada desde que el trabajo se cierra. Dentro de la ventana, repetir su clave de idempotencia le devuelve el mismo resultado sin volver a cobrar; pasada, el resultado se purga y result_state pasa a expired.

credits_charged sobrevive a esa purga, y se publica aparte de usage justamente por eso: pasada la ventana queda el importe y no el desglose. Con solo usage, un trabajo consultado al día siguiente parecería no haber costado nada.

El resto de las ventanas del contrato están en la página 13.

Repetir una clave de idempotencia: caso por caso

Es la misma decisión que toma la consulta de un trabajo, vista desde el otro lado.

Estado del trabajo anteriorQué recibe
Todavía en curso409 request_in_flight
Terminó bien y su resultado se guardaEl mismo job_id, el mismo usage y el resultado. Sin cobrar otra vez
Terminó bien y su resultado no se guarda409 result_not_replayable. El cobro no se duplica
Terminó bien y su resultado venció la ventana de retención de 24 h409 result_not_replayable, y el mensaje nombra la ventana. Pídala con una clave nueva
Terminó con un falloEl mismo fallo que la primera vez
Caducó409 job_not_completed
La clave se usó para otra operación, o con otras variables409 idempotency_key_reused

Que un fallo se republique tal cual es lo que hace que reintentar con la misma clave sea seguro: no se vuelve a ejecutar nada ni se vuelve a cobrar. Si lo que quiere es volver a intentarlo de verdad, use una clave nueva.

Cómo seguir una operación asíncrona

Dos formas, y no son excluyentes:

  1. Webhooks (página 11): le avisamos cuando el trabajo llega a un estado terminal. Es lo que evita consultar en bucle.
  2. Consulta de GET /v1/jobs/{job_id}: siempre disponible, y es lo que usa quien todavía no ha montado el receptor o quien está diagnosticando un caso concreto.

Si va a consultar en bucle, espacie las consultas: la consulta no gasta créditos pero sí cuenta para el techo de tasa de la plataforma (página 16).

Para el turno del agente hay además un tercer camino, que es leer la respuesta mientras se genera por el stream_url del acuse. Leerla no cuesta nada, ni la primera vez ni al reanudar (página 12).

Ese stream_url lo autoriza el mismo jobs:read que esta consulta, y no el permiso con el que abrió el turno: una clave que puede conversar pero no leer se queda sin la respuesta que ya pagó. Los detalles del permiso están en la página 15.

Qué hacer cuando se pierde una respuesta

Es el caso para el que existe todo lo anterior: su proceso murió, su tiempo de espera se agotó, su red falló.

  1. Repita la petición con la MISMA clave de idempotencia. Si la operación ocurrió, recibe su resultado sin pagarlo dos veces; si no ocurrió, se ejecuta ahora.
  2. Si recibe 409 request_in_flight, la anterior sigue viva. En una operación síncrona, espere lo que indica Retry-After y repita con la misma clave, o consulte el trabajo si conserva su job_id. En una llamada, Retry-After es el tiempo máximo que le queda (puede durar hasta 900 s, max_duration_seconds, más un margen de cierre): no reintente antes, porque mientras habla solo consume su límite de peticiones. Espere al webhook del cierre o a que pase ese tiempo, y repita entonces (Si lo que perdió es el acuse de una llamada).
  3. Si recibe 409 result_not_replayable o job_not_completed, la operación no se puede recuperar: pídala con una clave nueva.

Si lo que perdió es el acuse de una llamada

Una llamada aceptada sigue en curso hasta que cuelga, y su trabajo no es terminal mientras tanto. Por eso, mientras habla, la misma Idempotency-Key responde 409 request_in_flight: el 202 del acuse no se repite. Ese 409 lleva en Retry-After los segundos que le quedan como máximo a la llamada, que es una cota: si cuelga antes, la misma clave ya devuelve su resultado; mientras siga hablando, devuelve el mismo 409. Cuando cuelga, la misma clave devuelve el resultado guardado de la llamada (status completed, duration_seconds, billed_minutes) con el job_id original y los mismos warnings del acuse; si la llamada falló, devuelve el mismo fallo.

Si perdió el 202, perdió también el job_id, y mientras la llamada vive la API no se lo devuelve: el 409 no lo trae y no hay un listado de trabajos en /v1, solo GET /v1/jobs/{job_id}. Lo recupera de dos formas, las dos al colgar:

  • El webhook del cierre (job.succeeded, job.failed o job.expired) trae job_id y, en data.metadata, el metadata que envió al originarla. Envíe en él su identificador y podrá casar el cierre aunque no guardara nada nuestro.
  • Repetir la petición con la misma clave cuando la llamada ya terminó.

En la consola, el listado de trabajos filtra por entorno, estado, operación y fechas, pero no por clave de idempotencia.

Lo que no conviene: reintentar sin clave de idempotencia. Es la única forma de acabar pagando dos veces por lo mismo.