Reports over API
Lightning activity, housing and damage assessment — evaluate first, issue the PDF after.
Reports over API
Three products, one shape. Each answers a cheap question first; if the answer is useful to you, you issue the signed PDF.
| Product | The question | The document |
|---|---|---|
| Lightning activity | Was there lightning near the point in the window? | Signed GLM report |
| Housing | Is there a building on the parcel? | Presence report |
| Damage assessment | How much damage did the event leave? | Dossier signed with Ed25519 |
Evaluating does not commit you to issuing. That is the design decision the rest follows from: asking is cheap, issuing costs more, and you decide when to spend.
sequenceDiagram
participant C as Client
participant S as SBOX
C->>S: POST /v1/lightning/evaluations
S-->>C: 200 · detected, coverage, flashes[]
Note over C: Evaluating does not commit to issuing
C->>S: POST /v1/lightning/reports { evaluationId }
S-->>C: 202 · Location /v1/jobs/{id}
C->>S: GET /v1/jobs/{id}/result
S-->>C: 200 · downloadUrl (signed, 15 min)
S-)C: webhook report.ready (HMAC)
Authentication
An API key over Authorization: Bearer. It is minted once in the console and
never shown again — save it when you create it.
curl -sS "$SBOX/v1/lightning/evaluations" \
-H "authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{"lat": -34.6, "lng": -58.4, "eventAt": "2026-01-15T18:30:00Z", "radiusKm": 20}'The integracion:reportes scope covers all three products plus webhook
management with a single key.
Idempotency-Key
Required on everything that consumes provider quota. Without it, a retry of
yours after a timeout pays again for the same question. Repeating the same key
with the same body returns the same result; with a different body it returns
409.
1. Lightning activity
POST /v1/lightning/evaluations
{ "lat": -34.6, "lng": -58.4, "eventAt": "2026-01-15T18:30:00Z", "radiusKm": 20, "windowMin": 15 }The response distinguishes three things that are not the same:
coverage: "ok"withdetected: true— there was activity.coverage: "ok"withdetected: false— we looked and found nothing.coverage: "out_of_coverage" | "no_data"— we could not look.
That last case is not "there was no lightning". It is a gap in the satellite archive or a date before it, and treating it as absence would assert something that was never measured.
You can also send aoiRef instead of the point, and the parcel centroid is used.
For the PDF:
POST /v1/lightning/reports
{ "evaluationId": "ev_lgt_...", "locale": "es-AR" }Returns 202 with a job. The report cites the evaluation you already ran — it
does not query the satellite again, so two reports of the same fact cannot
contradict each other.
2. Housing
POST /v1/housing/evaluations
{ "aoiRef": "aoi_...", "minConfidence": 0.7 }Returns hasBuilding, the footprint count, covered area and maximum confidence.
Two limits worth knowing before you price on this:
- It detects building footprints, not dwellings. A shed and a house are the same footprint.
- The dataset is a 2023-05 snapshot. Anything built later does not appear, and its absence does not prove the parcel is empty.
Both limits are printed in the report, not only here.
3. Damage assessment
This one has an extra step, because it depends on the satellite having passed again.
POST /v1/damage/feasibility
{ "aoiRef": "aoi_...", "peril": "granizo", "eventDate": "2026-01-15" }This call consumes no quota: it queries the scene catalogue, not the compute engine. Five possible answers:
status | What to do |
|---|---|
ready | Request the assessment |
pending_post_imagery | Wait. retryAfter estimates when (~5-day revisit) |
cloud_blocked | Imagery is covered. If it carries retryAfter, another pass may be clearer |
unsupported_peril | That peril leaves no spectral signature (e.g. wind) |
invalid_event | The date does not allow planning the comparison |
Once ready:
POST /v1/damage/assessments
{ "aoiRef": "aoi_...", "peril": "granizo", "eventDate": "2026-01-15" }Then POST /v1/damage/reports with the assessmentId.
Drought needs no post-event imagery: it compares against the climatology of
previous years, so it is usually ready right away.
Asynchronous jobs
Everything that issues a PDF returns 202 with a job:
GET /v1/jobs/{id} → status
GET /v1/jobs/{id}/result → result, with the signed PDF URL
DELETE /v1/jobs/{id} → cancelThe signed URL expires in 15 minutes. That is deliberately short: a URL valid for hours is a portable credential, and whoever forwards it by email shares the document with everyone who reads that email.
Webhooks
So you do not have to poll:
POST /v1/webhooks/subscriptions
{ "url": "https://your-system/ucotron", "eventTypes": ["report.ready"] }The signing secret travels once, in that response. Every delivery arrives with
x-ucotron-signature: t=<timestamp>,v1=<hmac>, verified over
v1:<timestamp>:<body>.
The timestamp is part of the signature so a captured delivery cannot be replayed indefinitely. We accept a 5-minute tolerance.
The event does not carry the PDF URL. A webhook ends up in logs, proxies or a Slack channel; the event says it is ready and you fetch the document with your credential. Polling remains the source of truth: a lost delivery does not mean the report does not exist.
Retries: three attempts with 1 s, 5 s and 15 s backoff. A 4xx from your
endpoint goes straight to the DLQ without retrying — the same body three times
does not fix it — except 408 and 429, which are about time, not content.
Errors
RFC 7807 on every error response, with a retryable flag so you do not have to
maintain your own table of what is worth retrying.
One detail that matters: a resource belonging to another organization returns
404, exactly like a route that does not exist, with the same body. That is
not an oversight — a 403 would confirm the resource exists somewhere, and that
confirmation is enough to enumerate other tenants' portfolios.