SDKs
This content is not available in your language yet.
Both SDKs are generated from /v1/openapi.json, so they cover every endpoint
and every schema the API exposes and are regenerated when the document
changes. They add what a generated client cannot know on its own: an
Idempotency-Key on every POST, retries with backoff for 429 and 5xx,
the typed error envelope, and a wait helper for 202 jobs.
| Python | TypeScript | |
|---|---|---|
| Package | agrovisio-data-api (import agrovisio_data_api) |
@agrovisio/data-api |
| Source | sdk/python in the Agrovisio repository |
sdk/typescript |
| Runtime | Python 3.10+ (httpx, pydantic v2) |
Node 20+ with global fetch; ESM only |
| Clients | Client (sync), AsyncClient (asyncio) |
AgrovisioClient / createClient() |
| Errors | ApiError with .type, .status, .detail, .request_id, .doc_url, .retryable |
ApiError with .type, .status, .detail, .requestId, .docUrl, .retryable |
| Jobs | client.jobs.wait(job_id), client.damage.assess_and_wait(...) |
client.jobs.wait(jobId), client.damage.assessAndWait(...) |
| Webhooks | verify_webhook_signature, parse_webhook_event |
verifyWebhookSignature, parseWebhookEvent |
The key is read from AGROVISIO_API_KEY when not passed explicitly, and the
base URL from AGROVISIO_BASE_URL (default https://api.agrovisio.ai/v1). Sandbox and live
keys share the base URL - the sk_test_ / sk_live_ prefix decides which
world you see, and client.is_sandbox / client.isSandbox tells you which
one the client is talking to.
Install
Section titled “Install”pip install agrovisio-data-apinpm install @agrovisio/data-apiUntil the packages are on PyPI and npm, install them from the repository
checkout: pip install -e sdk/python and npm install ./sdk/typescript.
1. List the sandbox parcels
Section titled “1. List the sandbox parcels”from agrovisio_data_api import Client, LOCAL_BASE_URL
client = Client("sk_test_16e7fa3b7095c7f52f094bec33b48413", base_url=LOCAL_BASE_URL)
page = client.parcels.list(limit=25)for parcel in page.data: print(parcel.parcel_id, parcel.external_ref, parcel.area_ha, parcel.crop_hint)
# Cursor pagination: page.next_cursor / page.has_more, or let the client# follow the cursor for you: for parcel in client.parcels.iterate(): ...field_01 = next(p for p in page.data if p.external_ref == "SBX-FIELD-01")
print(client.last_response.request_id) # X-Request-Id of the last callimport { createClient, LOCAL_BASE_URL } from '@agrovisio/data-api';
const client = createClient({ apiKey: 'sk_test_16e7fa3b7095c7f52f094bec33b48413', baseUrl: LOCAL_BASE_URL,});
const page = await client.parcels.list({ limit: 25 });for (const parcel of page.data) { console.log(parcel.parcel_id, parcel.external_ref, parcel.area_ha, parcel.crop_hint);}
// Cursor pagination: page.next_cursor / page.has_more, or let the client// follow the cursor: for await (const p of client.parcels.iterate()) { ... }const field01 = page.data.find((p) => p.external_ref === 'SBX-FIELD-01')!;2. NDVI time series with baseline
Section titled “2. NDVI time series with baseline”from agrovisio_data_api import ApiError
try: series = client.indices.series( field_01.parcel_id, index="NDVI", from_="2025-04-01", to="2025-09-30" )except ApiError as err: if err.type == "baseline_not_ready": # Raw series without the anomaly block; or wait for parcel.data_ready. series = client.indices.series( field_01.parcel_id, index="NDVI", from_="2025-04-01", to="2025-09-30", baseline=False ) else: raise
print(series.observations, series.baseline.quality, series.method_version)for obs in series.series: if obs.value is None: print(obs.date, "no value:", obs.reason) # never interpolated else: print(obs.date, obs.value, obs.anomaly and obs.anomaly.get("z"))import { ApiError } from '@agrovisio/data-api';
let series;try { series = await client.indices.series(field01.parcel_id, { index: 'NDVI', from: '2025-04-01', to: '2025-09-30', });} catch (err) { if (err instanceof ApiError && err.type === 'baseline_not_ready') { // Raw series without the anomaly block; or wait for parcel.data_ready. series = await client.indices.series(field01.parcel_id, { index: 'NDVI', from: '2025-04-01', to: '2025-09-30', baseline: false, }); } else { throw err; }}
console.log(series.observations, series.baseline?.quality, series.method_version);for (const obs of series.series ?? []) { if (obs.value === null) console.log(obs.date, 'no value:', obs.reason); // never interpolated else console.log(obs.date, obs.value, obs.anomaly?.z);}3. Damage assessment: order, wait, read
Section titled “3. Damage assessment: order, wait, read”import uuidfrom agrovisio_data_api import ApiError
lodging = next(p for p in page.data if p.external_ref == "SBX-LODGING-1")body = {"parcel_id": lodging.parcel_id, "event_date": "2025-07-04", "peril": "storm", "claim_ref": "CLM-DEMO-1"}
# Two steps, so you can persist job_id between them...job = client.damage.assess(body, idempotency_key=str(uuid.uuid4())) # key generated when omittedprint(job.job_id, job.status, job.estimated_seconds) # 202: queued, ~90 sfinished = client.jobs.wait(job.job_id, timeout=300) # polls GET /jobs/{id}assessment = client.damage.get(finished.result["assessment_id"])
# ...or one call that does all three:# assessment = client.damage.assess_and_wait(body, timeout=300)
print(assessment.damage_pct, assessment.damage_pct_ci, assessment.confidence, assessment.billable)
try: # PDF only when confidence >= 0.50 pdf = client.damage.certificate_pdf(assessment.assessment_id) open("certificate.pdf", "wb").write(pdf)except ApiError as err: if err.type != "insufficient_data": raiseimport { randomUUID } from 'node:crypto';import { writeFile } from 'node:fs/promises';import { ApiError } from '@agrovisio/data-api';
const lodging = page.data.find((p) => p.external_ref === 'SBX-LODGING-1')!;const body = { parcel_id: lodging.parcel_id, event_date: '2025-07-04', peril: 'storm', claim_ref: 'CLM-DEMO-1' };
// Two steps, so you can persist job_id between them...const job = await client.damage.assess(body, randomUUID()); // key generated when omittedconsole.log(job.job_id, job.status, job.estimated_seconds); // 202: queued, ~90 sconst finished = await client.jobs.wait(job.job_id, { timeoutMs: 300_000 }); // polls GET /jobs/{id}const assessment = await client.damage.get(finished.result!.assessment_id as string);
// ...or one call that does all three:// const assessment = await client.damage.assessAndWait(body, { timeoutMs: 300_000 });
console.log(assessment.damage_pct, assessment.damage_pct_ci, assessment.confidence, assessment.billable);
try { // PDF only when confidence >= 0.50 // certificatePdf() returns ApiResponse<ArrayBuffer>: { data, status, requestId, headers } const { data: pdf } = await client.damage.certificatePdf(assessment.assessment_id); await writeFile('certificate.pdf', Buffer.from(pdf));} catch (err) { if (!(err instanceof ApiError && err.type === 'insufficient_data')) throw err;}jobs.wait raises JobFailedError (with the job’s error envelope) when the
job ends in failed or cancelled, and JobTimeoutError when your timeout
passes first; the job keeps running on the server and you can wait again with
the same id.
Errors
Section titled “Errors”Both clients raise one exception type for every non-2xx response and expose
the error envelope fields on it. Branch on type, log
request_id, follow doc_url. Transport failures (TransportError /
ConnectionError, RequestTimeoutError / TimeoutError) and job failures
share the base class AgrovisioError.
import timefrom agrovisio_data_api import AgrovisioError, ApiError
try: client.thermal.series(small.parcel_id, from_="2025-06-01", to="2025-08-31")except ApiError as err: print(err.status, err.type, err.request_id, err.doc_url) print(err.detail) # structured: fields, limits, windows if err.retryable: # insufficient_data, rate_limited, upstream_unavailable, ... time.sleep(err.retry_after_seconds or 5)except AgrovisioError as err: # network, timeout, job failed print("transport:", err)import { AgrovisioError, ApiError } from '@agrovisio/data-api';
try { await client.thermal.series(small.parcel_id, { from: '2025-06-01', to: '2025-08-31' });} catch (err) { if (err instanceof ApiError) { console.log(err.status, err.type, err.requestId, err.docUrl); console.log(err.detail); // structured: fields, limits, windows if (err.retryable) { // insufficient_data, rate_limited, upstream_unavailable, ... await new Promise((r) => setTimeout(r, (err.retryAfterSeconds ?? 5) * 1000)); } } else if (err instanceof AgrovisioError) { // network, timeout, job failed console.log('transport:', err.message); }}Rate-limit headers are parsed for you: client.rate_limits /
client.rateLimits holds the last seen X-RateLimit-* state per pool
(standard, write, expensive), and a 429 rate_limited is retried after
Retry-After up to max_retries / maxRetries times (default 2) before it
is raised.
Webhooks
Section titled “Webhooks”The signature helpers implement exactly what the webhooks guide
describes - HMAC-SHA256 over timestamp.body, a 5 minute tolerance window,
constant-time comparison:
from agrovisio_data_api import parse_webhook_event
# Flask / FastAPI / Django: pass the raw request body, not a parsed dict.event = parse_webhook_event(WEBHOOK_SECRET, request.headers, request.get_data())if event is None: # bad signature or outside the replay window abort(400)print(event["type"], event["data"]["job_id"])import { parseWebhookEvent } from '@agrovisio/data-api';
// Express with express.raw({ type: 'application/json' }); pass the Buffer.const event = await parseWebhookEvent(WEBHOOK_SECRET, req.headers, req.body);if (event === null) return res.status(400).end(); // bad signature or outside the replay windowconsole.log(event.type, event.data.job_id);Both return None / null when the signature does not match or the
timestamp is outside the replay window; the lower-level
verify_webhook_signature / verifyWebhookSignature returns a boolean, and
sign_webhook_payload / signWebhookPayload produces a valid signature for
your own receiver tests. Return 2xx only after the check passed.
