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:
| Key | Environment | What it returns | Quota |
|---|---|---|---|
uco_sbox_… | https://sbox.ucotron.com | Deterministic fixtures | Not consumed |
uco_live_… | https://api.ucotron.com | Real satellite data | Consumed |
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.
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 see | Why |
|---|---|
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 succeeded | There 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: queued → running → succeeded | 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.
| Code | When | retryable | What to do |
|---|---|---|---|
400 | Body is not JSON | no | Fix the request |
401 | Key missing, invalid, revoked, expired, or from another environment | no | Check the credential. code tells the cases apart |
402 | The organization's budget is exhausted | no | Contact us: a commercial cap, not a technical error |
403 | The key lacks the permission the operation needs | no | The body carries requiredPermission |
404 | Route does not exist or the resource belongs to another organization | no | See below |
409 | Same Idempotency-Key, different body. Or result requested too early | no | Use a new key, or wait for succeeded |
422 | Parameters are not shaped as expected | no | The body lists the fields under issues |
428 | Missing Idempotency-Key on a quota-consuming operation | no | Add the header |
429 | Too many requests | yes | Wait out the Retry-After |
5xx | Something on our side | yes | Retry 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.