Ucotron Cortex
Guides by flow

Jobs, delivery and errors

How the partner API works underneath — credentials, idempotency, the job lifecycle, downloads, and the full table of status codes.

The three products —lightning activity, housing and damage assessment— share the same shape. This guide explains that shape once; each product guide covers only what differs.

The two environments

Your organization gets two keys, and they are not interchangeable:

KeyEnvironmentWhat it returnsQuota
uco_sbox_…https://sbox.ucotron.comDeterministic fixturesNot consumed
uco_live_…https://api.ucotron.comReal satellite dataConsumed

Build your integration against the sandbox and change one variable to go live: routes and bodies are identical, only the host and the key change.

Presenting a key against the wrong environment returns 401 with code environment_mismatch. It has a reason of its own on purpose: an unknown key sends you to check whether it was copied correctly, a wrong-environment key sends you to check which variable was deployed. Those are different investigations.

bash
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}'

The secret is shown once, when minted. We store its hash, not the secret, so "we will not show it again" is literal: if it is lost, revoke it and mint another.

The sandbox keeps no state

sbox.ucotron.com has no database, and that is deliberate: the environment declares databaseMutation: false. Everything it answers is a fixture, so each response is a pure function of its request and the identifier carries the request inside it. Any instance reconstructs it identically without storing anything.

That has two visible consequences. They are not bugs in your integration:

What you seeWhy
GET /v1/jobs and GET /v1/reports return data: []Listing asks "what did I do?", and that is exactly the state the sandbox does not keep. Returning a made-up catalogue would show you jobs you never enqueued
Jobs are born succeededThere is no queue or worker to wait for. Your polling loop still works —the response carries pollUrl and Retry-After— but it closes on the first pass

And a third one about the PDF: the file is real and opens, with its satellite map, its logo and its signature block, but its content does not reflect your request's parameters. Asking for a 5 km or an 80 km radius returns the same document. The sha256 the API reports is the one of the bytes you download, so verifying the download does work.

To exercise the full asynchronous cycle, the listing, and a PDF that reflects your query, use production.

Idempotency

Every operation that consumes quota requires the Idempotency-Key header. Without it the answer is 428, not 400: a precondition is missing, not a malformed body.

Same key and same body: the second call returns the same job without charging again. Same key, different body: 409. That is almost certainly a bug on your side, and answering the new question under the old key would hand you the wrong answer without anyone noticing.

A job's identifier comes from the hash of its content. Asking twice for exactly the same analysis —even with different idempotency keys— returns the job that already exists, with its result untouched. That also means re-issuing the same report does not produce a new document: it produces the same one, with the same sha256.

The job lifecycle

Evaluations answer immediately. PDF issuance is asynchronous, because rendering a document takes seconds and keeping you on an open HTTP response would be a promise we cannot always keep.

sequenceDiagram
    participant C as Your system
    participant A as API
    C->>A: POST /v1/lightning/reports { evaluationId }
    A-->>C: 202 · { id, status: "queued", pollUrl }
    loop every Retry-After seconds
        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 · to the PDF

States: queuedrunningsucceeded | failed | cancelled. The last three are terminal: a job that got there never changes again.

Honour the Retry-After returned with the 202. Polling faster speeds nothing up and moves you closer to a 429.

GET /v1/jobs lists your jobs, filterable by status and operation. Pagination is cursor-based: pass the previous page's nextCursor. Do not use offsets — the list grows from the top, and a job enqueued between two pages would make a row repeat or be skipped.

Downloads

GET /v1/reports/{id}/download returns a 302 to the PDF. That address is safe to store: it does not expire. What expires is the URL it redirects to, minted at click time and valid for minutes.

This is deliberate, and better than handing out a signed link directly: access is cut the moment you revoke the key, which an already-delivered S3 link cannot do. Follow the redirect with curl -L.

GET /v1/reports lists what you have issued, with each document's sha256 and signature — which lets you verify later that the file you hold is the one we issued.

Status codes, and what to do with each

Every error is RFC 7807 and carries a retryable field: you do not need to maintain your own table of what is worth retrying.

CodeWhenretryableWhat to do
400Body is not JSONnoFix the request
401Key missing, invalid, revoked, expired, or from another environmentnoCheck the credential. code tells the cases apart
402The organization's budget is exhaustednoContact us: a commercial cap, not a technical error
403The key lacks the permission the operation needsnoThe body carries requiredPermission
404Route does not exist or the resource belongs to another organizationnoSee below
409Same Idempotency-Key, different body. Or result requested too earlynoUse a new key, or wait for succeeded
422Parameters are not shaped as expectednoThe body lists the fields under issues
428Missing Idempotency-Key on a quota-consuming operationnoAdd the header
429Too many requestsyesWait out the Retry-After
5xxSomething on our sideyesRetry with backoff

Why someone else's resource returns 404 and not 403. A 403 would confirm that the identifier exists somewhere. The 404 does not distinguish "does not exist" from "not yours" —neither in the status nor in the body, which is identical in both cases— and so this endpoint cannot be used to find out which ids are real.

What a report asserts

Every PDF carries the analysis hash and a signature in its footer. That lets you verify the file was not altered and that the analysis can be reproduced: the same query over the same scenes yields the same result.

What it does not do is replace a field inspection. Each report states its own limitations in a dedicated section, before the provenance block, so that whoever reaches the end already knows what the document can and cannot support.

On this page