Trabajos, entrega y errores
Cómo funciona el API de integradores por dentro — credenciales, idempotencia, ciclo de un trabajo, descarga y la tabla completa de códigos.
Los tres productos —actividad eléctrica, edificación y peritaje de daños— tienen la misma forma. Esta guía explica esa forma una sola vez; las guías de cada producto se ocupan de lo que cambia.
Los dos entornos
Tu organización recibe dos claves, y no son intercambiables:
| Clave | Entorno | Qué devuelve | Cuota |
|---|---|---|---|
uco_sbox_… | https://sbox.ucotron.com | Fixtures deterministas | No consume |
uco_live_… | https://api.ucotron.com | Datos satelitales reales | Consume |
Construí tu integración contra el sandbox y cambiá una variable para pasar a producción: las rutas y los cuerpos son idénticos, sólo cambia el host y la clave.
Presentar una clave contra el entorno equivocado responde 401 con código
environment_mismatch. Tiene un motivo propio a propósito: una clave
desconocida manda a revisar si se copió mal, y una del entorno equivocado manda
a revisar qué variable se desplegó. Son investigaciones distintas.
curl -s -X POST "$BASE/v1/lightning/evaluations" \
-H "Authorization: Bearer $UCOTRON_API_KEY" \
-H "content-type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"lat":-2.76,"lng":-60.52,"eventAt":"2026-06-26T17:22:00Z","radiusKm":20,"windowMin":15}'El secreto se muestra una sola vez al acuñarlo. Se guarda su hash, no el secreto, así que "no la vamos a volver a mostrar" es literal: si se pierde, se revoca y se acuña otra.
El sandbox no guarda estado
sbox.ucotron.com no tiene base de datos, y eso es una decisión: el entorno
declara databaseMutation: false. Todo lo que responde es un fixture, así que
cada respuesta es una función pura de su pedido y el identificador lleva el
pedido adentro. Cualquier instancia lo reconstruye idéntico sin guardar nada.
Eso tiene dos consecuencias visibles. No son errores de tu integración:
| Qué ves | Por qué |
|---|---|
GET /v1/jobs y GET /v1/reports devuelven data: [] | Listar es preguntar «¿qué hice?», y eso es exactamente el estado que el sandbox no guarda. Devolver un catálogo inventado te mostraría trabajos que nunca encolaste |
Los trabajos nacen succeeded | No hay cola ni worker que esperar. Tu bucle de poleo funciona igual —la respuesta trae pollUrl y Retry-After— pero cierra en la primera vuelta |
Y una tercera sobre el PDF: el archivo es real y abre, con su mapa satelital,
su logo y su bloque de firma, pero su contenido no refleja los parámetros de tu
pedido. Pedir un radio de 5 km o de 80 km devuelve el mismo documento. El
sha256 que informa el API sí es el de los bytes que descargás, así que
verificar la descarga funciona.
Para ejercitar el ciclo asincrónico completo, el listado y un PDF que refleje tu consulta, está producción.
Escenarios deterministas
Como el sandbox no guarda estado, los caminos negativos se piden por
convención del pedido. Están para que puedas escribir y probar el else de tu
integración sin salir a producción: un entorno que sólo sabe decir que sí no
sirve para eso.
| Qué querés ensayar | Cómo pedirlo | Qué responde |
|---|---|---|
| Rayos: no hubo actividad | lat y lng enteros (p. ej. -31, -68) | detected: false con coverage: "ok" |
| Rayos: fuera de cobertura | eventAt anterior a 2018 | coverage: "out_of_coverage", sin procedencia |
| Edificación: lote vacío | un anillo de 5 hectáreas o más | hasBuilding: false |
| Peritaje: lote inexistente | aoi_ref fuera del catálogo de demostración (demo:lote-1, -2, -3) | trabajo failed con aoi_not_found |
| Conflicto de idempotencia | Idempotency-Key: 00000000-0000-0000-0000-000000000409 | 409 idempotency_conflict, siempre |
| Anillo mal armado | un ring cuyo último punto no repite el primero | 422 invalid_ring |
| Fecha imposible | eventAt en el futuro | 422 event_in_future |
| Informe inexistente | un reportId inventado | 404 not_found |
La clave del 409 es reservada porque el sandbox no puede recordar que una clave ya se usó: sin ella, el conflicto sería el único código del contrato que no se puede ensayar antes de salir a vivo.
Idempotencia
Toda operación que gasta cuota exige el header Idempotency-Key. Sin él la
respuesta es 428, no 400: es una precondición que falta, no un cuerpo mal
armado.
Con la misma clave y el mismo cuerpo, la segunda llamada devuelve el resultado
guardado sin volver a calcular ni a cobrar. Con la misma clave y otro
cuerpo, la respuesta es 409 idempotency_conflict: es casi seguro un error de tu
lado, y contestar la pregunta nueva bajo la clave vieja te devolvería la
respuesta equivocada sin que nadie lo note.
Esto vale igual para los trabajos y para las evaluaciones. Si tu
integración se escribió contra una versión anterior de este API, conviene
revisarla: antes las evaluaciones exigían la clave y no la leían, así que un
reintento con el cuerpo cambiado respondía 200 en vez de 409.
El identificador de un trabajo sale del hash de su contenido. Pedir dos
veces exactamente el mismo análisis —aun con claves de idempotencia
distintas— devuelve el trabajo que ya existe, con su resultado intacto. Eso
también significa que reemitir el mismo informe no produce un documento nuevo:
produce el mismo, con el mismo sha256.
El ciclo de un trabajo
Las evaluaciones responden en el momento. Las emisiones de PDF son asincrónicas, porque renderizar un documento tarda segundos y mantenerte esperando una respuesta HTTP sería una promesa que no siempre se puede cumplir.
Fuente del diagrama (mermaid)
sequenceDiagram
participant C as Tu sistema
participant A as API
C->>A: POST /v1/lightning/reports { evaluationId }
A-->>C: 202 · { id, status: "queued", pollUrl }
loop cada Retry-After segundos
C->>A: GET /v1/jobs/{id}
A-->>C: 200 · { status: "running" }
end
C->>A: GET /v1/jobs/{id}
A-->>C: 200 · { status: "succeeded", resultUrl }
C->>A: GET /v1/reports/{id}/download
A-->>C: 302 · hacia el PDF
Estados: queued → running → succeeded | failed | cancelled. Los tres
últimos son terminales: un trabajo que llegó ahí no vuelve a cambiar.
Respetá el Retry-After que viene en el 202. Poletear más rápido no acelera
nada y te acerca al 429.
GET /v1/jobs lista tus trabajos, filtrables por status y operation. La
paginación es por cursor: pasá el nextCursor de la página anterior. No uses
offsets — la lista crece por arriba, y un trabajo encolado entre dos páginas
haría que una fila se repita o se saltee.
La descarga
GET /v1/reports/{id}/download devuelve un 302 hacia el PDF. Esa dirección
la podés guardar: no vence. Lo que vence es la URL a la que redirige, que se
acuña en el momento del click y dura minutos.
Es deliberado y es mejor que repartir un enlace firmado directo: el acceso se
corta solo cuando revocás la clave, cosa que un enlace de S3 ya entregado no
permite. Seguí el redirect con curl -L.
GET /v1/reports lista lo emitido, con el sha256 de cada documento y su firma
—lo que te permite comprobar más tarde que el archivo que tenés es el que
emitimos—.
Los códigos, y qué hacer con cada uno
Todos los errores son RFC 7807
y traen un campo retryable: no necesitás mantener tu propia tabla de qué
conviene reintentar.
| Código | Cuándo | retryable | Qué hacer |
|---|---|---|---|
400 | Cuerpo que no es JSON | no | Arreglar el request |
401 | Falta la clave, es inválida, fue revocada, venció, o es de otro entorno | no | Revisar la credencial. El code distingue el caso |
402 | Se agotó el presupuesto de la organización | no | Escribirnos: es un tope comercial, no un error técnico |
403 | La clave no tiene el permiso que la operación pide | no | El cuerpo trae requiredPermission |
404 | La ruta no existe o el recurso es de otra organización | no | Ver abajo |
409 | Misma Idempotency-Key, distinto cuerpo. O resultado pedido antes de tiempo | no | Usar una clave nueva, o esperar a succeeded |
422 | Los parámetros no tienen la forma esperada | no | El cuerpo lista los campos en issues |
422 invalid_ring | El anillo del lote no está cerrado | no | Repetir el primer punto al final. No lo cerramos por vos: sería peritar otra superficie |
422 event_in_future | La fecha del evento es posterior a hoy | no | Corregir la fecha. No hay imagen satelital de mañana |
428 | Falta Idempotency-Key en una operación que gasta cuota | no | Agregar el header |
429 | Demasiadas peticiones | sí | Esperar el Retry-After |
503 engine_not_configured | El motor satelital de ese producto no está disponible en el entorno | no | Escribirnos. Preferimos decirlo antes que emitir un documento firmado sin el dato que lo sostiene |
5xx | Algo nuestro | sí | Reintentar con backoff |
Por qué un recurso ajeno contesta 404 y no 403. Un 403 confirmaría que ese
identificador existe en algún lado. El 404 no distingue "no existe" de "no es
tuyo" —ni en el status ni en el cuerpo, que es idéntico en los dos casos— y por
eso este endpoint no sirve para averiguar qué ids son reales.
Qué afirma un informe
Cada PDF trae al pie el hash del análisis y una firma Ed25519. Eso permite comprobar que el archivo no fue alterado y que el análisis se puede rehacer: la misma consulta sobre las mismas escenas produce el mismo resultado.
La firma se verifica con una clave pública, no con un secreto compartido. La
diferencia importa cuando el documento va a un expediente: quien verifica no
puede fabricar otro. La clave está publicada, sin credenciales, en
https://app.ucotron.com/v1/verify/keys, y el propio PDF dice cuál usó y sobre
qué bytes se firmó. Está desarrollado en
Reportes firmados.
Lo que no hace es reemplazar una inspección en campo. Cada informe declara sus propias limitaciones en una sección dedicada, antes de la procedencia, para que quien llega al final ya sepa qué puede y qué no puede sostener el documento.
Peritaje de daños
Cuánto se dañó el lote tras un evento de granizo, inundación o sequía — con el paso previo que evita gastar cuota en un cálculo que todavía no se puede hacer.
API de integradores
Tres productos sobre datos satelitales, con informe PDF firmado: actividad eléctrica, edificación y peritaje de daños.
