Python SDK
The official client for robots and fleet software. Zero runtime dependencies, fully typed, and designed around the one thing that actually goes wrong in the field: the network disappearing halfway through a request.
Install
Python 3.10 or newer. The floor is 3.10 because that is what ROS 2 Humble ships, and Humble is the oldest distribution the ROS 2 package supports.
pip install hoodly
Nothing else comes with it. No requests, no pydantic, no transitive resolver conflicts — the transport is built on the standard library, because this installs onto industrial PCs and into ROS 2 workspaces where a dependency clash is a real cost.
Five lines
The API key comes from HOODLY_API_KEY if you do not pass one.
from hoodly import HoodlyClient
client = HoodlyClient() # reads HOODLY_API_KEY
verification = client.prove(
"Dock inspection, bay 7",
{"dock": 7, "payload_kg": 12.5, "defects_found": 0},
)
print(verification.tx_hash)
print(f"https://hoodly.fun{verification.share_url}")prove() is the shortcut: it claims a task and completes it in one call. When you need the task id in between — to log it, or because claiming and finishing are minutes apart — use the two steps directly.
task = client.create_task("Dock inspection, bay 7")
# ... the robot does the work ...
verification = client.complete_task(task.id, {"dock": 7, "defects_found": 0})What complete_task does when the network drops
This is the part worth reading, because it is the difference between a library that works on a desk and one that works on a robot.
A completion request triggers an on-chain transaction. If the connection dies before the response arrives, you cannot know from the client side whether the anchor happened. A naive retry would anchor the same task twice and burn quota and gas for a duplicate.
So complete_task never blindly retries. On a transport failure it reconciles: it polls the task's real state and reports what actually happened.
from hoodly import (
HoodlyClient,
AnchorUnconfirmedError,
InsufficientCreditError,
AnchoringFailedError,
)
client = HoodlyClient()
try:
verification = client.complete_task(task_id, proof)
print("anchored:", verification.tx_hash)
except AnchorUnconfirmedError as exc:
# Broadcast, but no receipt in time. It may still land.
# Do NOT resubmit — poll instead.
print("unconfirmed:", exc.tx_hash)
verification = client.wait_for_anchor(exc.task_id, timeout=300)
except InsufficientCreditError as exc:
# Nothing was sent. Top up, then complete the same task again.
print("out of credit:", exc.balances)
except AnchoringFailedError:
# Nothing reached the chain and the credit hold was released.
# Safe to resubmit as a new task.
passEvery failure mode the API can report has its own exception type, because a robot has to react differently to each: a 402 means top up, a 429 means slow down, and an unconfirmed anchor means wait and look.
Never retry an unconfirmed anchor
AnchorUnconfirmedError is the one failure that must not be retried. The transaction is already on the wire; sending the proof again can anchor the same task a second time. Poll wait_for_anchor() or open exc.explorer_url instead. Everything else in this SDK — the offline queue included — follows that same rule.
The offline queue
A mobile robot loses connectivity constantly. Dropping a proof because a WiFi handover happened mid-upload would defeat the point of keeping records, so OfflineQueue writes evidence to a local SQLite file first and uploads it when the network returns. The file survives a reboot and a power cut.
from hoodly import HoodlyClient
from hoodly.queue import OfflineQueue
client = HoodlyClient()
queue = OfflineQueue("/var/lib/hoodly/queue.db")
# Never blocks on the network. Returns a local id.
queue.enqueue("Waypoint 7 reached", {"battery_pct": 61.5})
# Call this on a timer. Unfinished work stays queued.
report = queue.flush(client)
print(report.anchored, "anchored,", report.still_pending, "still waiting")Three properties make it safe to run unattended.
A crash between claiming and anchoring cannot orphan a task. The task id is persisted the moment the claim succeeds, so resuming completes that same task instead of claiming a second one.
A flush stops early instead of hammering a wall. No network, no credit or a rate limit ends the pass, since the next entry would hit the same condition. Order is preserved, so the anchored sequence matches the real one.
A flush is bounded in time. It gives up on an unconfirmed anchor quickly rather than blocking for minutes, because the queue is durable — the next pass reconciles the same task id. That is what makes it usable from a ROS 2 timer or any single-threaded control loop.
When the work happened vs. when it was anchored
For queued proofs these are different moments, sometimes hours apart, and only one of them is the fact being attested. The queue records the event time inside the evidence under hoodly_occurred_at:
{
"battery_pct": 61.5,
"hoodly_occurred_at": "2026-08-06T12:41:07.812+00:00"
}The chain timestamps the record; the evidence timestamps the event. Pass your own occurred_at to enqueue() if the work finished before it was recorded.
Telemetry
Telemetry is free and separate from proofs — it feeds the live fleet view without consuming anchor credit. Batch it if you sample often.
client.send_telemetry(
battery_pct=61.5,
metrics={"cpu_pct": 23.0, "lat": 52.52, "lon": 13.405},
)
client.send_telemetry_batch([
{"battery_pct": 61.5, "recorded_at": "2026-08-06T12:41:07Z"},
{"battery_pct": 60.9, "recorded_at": "2026-08-06T12:42:07Z"},
])Verifying without a key
Verification is public, so this call needs no authentication and works for any proof hash — including one from a fleet you do not own. Read checks rather than verified: a summary is exactly what an auditor should not have to trust.
result = client.verify("0x9c22ff5f…")
if result is None:
print("never anchored")
elif result.failed_checks:
print("suspect:", result.failed_checks)
else:
print("verified in block", result.block_number)Client options
| Field | Type | Description |
|---|---|---|
| api_key | str | None | Defaults to the HOODLY_API_KEY environment variable. |
| base_url | str | Defaults to https://www.hoodly.fun. Point at a staging deployment if you have one. |
| timeout | float | Per-request timeout in seconds. Keep it above the server's anchoring budget so a slow block is not mistaken for a dead connection. |
| max_retries | int | Applies to idempotent reads only. Completion is never retried automatically — it reconciles instead. |
Next
If the robot runs ROS 2, do not wire this up by hand — the ROS 2 package wraps this SDK in an action server and collects pose, diagnostics and battery state into the evidence for you.
