Skip to content

SDKs

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.

Terminal window
pip install agrovisio-data-api

Until the packages are on PyPI and npm, install them from the repository checkout: pip install -e sdk/python and npm install ./sdk/typescript.

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 call
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 uuid
from 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 omitted
print(job.job_id, job.status, job.estimated_seconds) # 202: queued, ~90 s
finished = 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":
raise

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.

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 time
from 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)

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.

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"])

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.