Skip to main content

EarthRanger Client

Introduction

EarthRanger is a software solution that helps protected area managers, ecologists, and wildlife biologists make informed operational decisions for wildlife conservation.

The earthranger-client (er-client) is a Python library for accessing the EarthRanger HTTP API. It simplifies interaction with the API by abstracting resource-based endpoints and offers both synchronous and asyncio clients, plus multi-threaded helpers for bulk reads.

Uses of er-client

  • Extracting data for analysis
  • Importing ecological or other historical data
  • Integrating a new field sensor type. If you do and will be supporting multiple ER sites, contact us to talk about our Gundi integrations platform
  • Performing external analysis that results in publishing an Alert on the ER platform.

Quick Start

See docs/examples/simple-example.py for a full sync example (pulse, subjects, tracks, create event, attach file, query events).

Installation

From PyPI:

pip install earthranger-client

Choosing sync vs async

Use case Client Notes
Scripts, notebooks, one-off jobs ERClient (sync) Blocking calls; no event loop.
Asyncio apps (e.g. web servers, async pipelines) AsyncERClient (async) Use async with or call close() when done.

Both clients take the same constructor arguments, apart from two async-only timeouts (see "Constructor arguments" below). The async client supports a subset of the sync client's endpoints (see "Async client scope" below).

Sync client (ERClient)

Import and construct with service_root and either username/password (+ client_id) or a bearer token:

from erclient import ERClient

# Username/password (client_id required)
client = ERClient(
    service_root="https://sandbox.pamdas.org",
    client_id="example_client_id",
    username="your_username",
    password="your_password",
    provider_key="your_provider_key",  # only needed for sensor / camera-trap posts
)
# Or with a bearer token
client = ERClient(service_root="https://sandbox.pamdas.org", token="your_bearer_token")

Common patterns:

import json
from datetime import datetime, timezone

# Single item
event = client.get_event(event_id="uuid")
subject = client.get_subject(subject_id="uuid")

# Paginated iteration (generators).
# `filter` must be a JSON-encoded string, not a dict.
event_filter = json.dumps({
    "date_range": {
        "lower": "2023-11-10T00:00:00-06:00",
        "upper": "2023-11-11T00:00:00-06:00",
    },
})
for event in client.get_events(filter=event_filter, max_results=100):
    ...
for obs in client.get_observations(
    start=datetime(2023, 11, 10, tzinfo=timezone.utc),
    end=datetime(2023, 11, 11, tzinfo=timezone.utc),
):
    ...

# Create / update
new_event = client.post_report({
    "event_type": "wildlife_sighting_rep",  # must match an event type in your ER site
    "title": "A new event",
    "location": {"latitude": 47.5978393, "longitude": -122.3308366},
})
client.post_sensor_observation(observation, sensor_type="generic")  # requires provider_key
client.post_event_file(event_id, filepath="/path/to/file", comment="...")

For bulk reads the sync client also provides get_objects_multithreaded(object="observations", ...).

Async client (AsyncERClient)

Use an async context manager so the HTTP session is always closed:

import asyncio
import json

from erclient import AsyncERClient

async def main():
    async with AsyncERClient(
        service_root="https://sandbox.pamdas.org",
        client_id="example_client_id",
        username="your_username",
        password="your_password",
        provider_key="your_provider_key",  # only needed for sensor / camera-trap posts
    ) as client:
        # Single-item calls: await
        event = await client.get_event(event_id="uuid")
        event_types = await client.get_event_types()

        # Stream events or observations: async for.
        # `filter` must be a JSON-encoded string, not a dict.
        event_filter = json.dumps({"date_range": {"lower": "2023-11-10T00:00:00-06:00"}})
        async for event in client.get_events(filter=event_filter, page_size=100):
            ...
        async for observation in client.get_observations(start="2023-11-10T00:00:00-06:00"):
            ...

        # Post (await)
        await client.post_sensor_observation(position)
        await client.post_report(report)
        await client.post_camera_trap_report(camera_trap_payload, file=file_handle)

asyncio.run(main())

Without a context manager, create the client and call await client.close() when finished:

async def main():
    client = AsyncERClient(service_root="...", client_id="...", username="...", password="...")
    try:
        await client.post_report(report)
        async for obs in client.get_observations(start="2023-11-10T00:00:00-06:00"):
            print(obs)
    finally:
        await client.close()

asyncio.run(main())

Async client scope

The async client currently supports:

  • Post: Sensor observations (positions), events/reports, event attachments, camera trap reports, messages, event types, event categories; adding subjects to a subject group.
  • Get: Events, single event, event types, event categories, observations, subject groups, subject sources, feature groups, sources (by manufacturer id), source assignments (subjectsources), user/me.
  • Patch: Events, reports, subjects, event types, event categories.
  • Delete: Events, event files, event notes, subjects, sources. Neither client can delete event types or event categories.
  • Relationships: Adding/removing events to and from incidents; removing subjects from a subject group.

For the full sync surface (e.g. patrols, tracking data export, multithreaded bulk), use ERClient.

Constructor arguments

Both clients are declared as __init__(self, **kwargs), so every argument is keyword-only — ERClient("https://sandbox.pamdas.org") raises TypeError. Unrecognised keywords are silently ignored rather than rejected, so a typo such as provider_ke= leaves provider_key unset with no error.

Argument Default Notes
service_root None Base URL, e.g. https://sandbox.pamdas.org. A full API root is also accepted: any /api/... suffix is stripped, so passing .../api/v2.0 does not select v2.0.
client_id None Required for username/password auth.
username, password None Use together with client_id, or pass token instead.
token None Bearer token. Skips the OAuth2 password grant entirely.
provider_key None Required for sensor and camera-trap posts; it becomes a path segment.
token_url {service_root}/oauth2/token Override only if the auth endpoint differs.
max_http_retries 5 Connection-level retries. Effective on async only — the sync client accepts and stores it but never uses it; sync retry behavior is fixed (5 session-level retries on 502, plus per-request retries in GETs).
realtime_url None Accepted and stored, but unused by this library.
connect_timeout 3.1 Seconds. Async only.
data_timeout 20 Seconds. Async only.

Use either token, or client_id + username + password.

API versions

Event-type endpoints exist in two API versions. v1.0 is the default; pass version= to opt into v2.0:

from erclient import ERClient, VERSION_2_0

client = ERClient(service_root="https://sandbox.pamdas.org", token="your_bearer_token")
event_types = client.get_event_types(version=VERSION_2_0)

Four methods accept version=, on both clients — get_event_types, get_event_type, post_event_type, patch_event_type. Every other call is v1.0, and passing .../api/v2.0 as service_root does not change that (see "Constructor arguments").

Accepted values are VERSION_1_0 ("v1.0") and VERSION_2_0 ("v2.0"); the aliases "v1" and "v2" work too. Anything else raises ValueError, including "1.0" — the leading v is required.

patch_event_type identifies the event type differently per version, and raises ValueError when the key it needs is absent from the payload:

Version Key used Resulting path
v1.0 event_type["id"] activity/events/eventtypes/{id}
v2.0 event_type["value"] (slug) activity/eventtypes/{value}

Caveat: the async client's patch_event_type reads event_type["value"] regardless of version (for logging), so on AsyncERClient a v1.0 payload must include value as well as id — a payload without value raises KeyError there, not ValueError.

Common method signatures (reference)

  • Events:
    get_events(*, filter, page_size, max_results, ...) → sync: generator; async: async generator.
    get_event(*, event_id, include_details, include_notes, ...) → single dict.
    post_report(data) / post_event(data) → created resource.

  • Observations:
    get_observations(*, subject_id, source_id, start, end, page_size, ...) → sync: generator; async: async generator.
    post_sensor_observation(observation, sensor_type='generic') → requires provider_key.

  • Single resources:
    get_subject(subject_id), get_source_by_id(id), get_event_type(event_type_name, version=...), etc. return one object.

Sync vs async differences

Behaviors that are not shared, despite the common signatures above:

Behavior ERClient (sync) AsyncERClient (async)
get_observations start/end datetime only — ISO strings are silently ignored datetime or ISO 8601 string
get_events max_results honored client-side ignored (forwarded as a query param)
get_observations page_size default 10000 100
HTTP 409 / 429 plain ERClientException ERClientRateLimitExceeded, with retry_after
HTTP error → exception subclass only 403 / 404 are consistent; other codes often raise plain ERClientException, and 401 / 502 / 504 vary by method common statuses (400, 401, 403, 404, 409, 429, 500, 502, 503, 504) mapped to subclasses; others raise plain ERClientException
exc.status_code / exc.response_body / exc.retry_after never set (always None); the status is recoverable only from the exception type, or from the message text for unmapped codes populated on every HTTP error
Helpers only on one client get_subject, get_source_by_id, get_sources, get_subjects, pulse get_feature_group, get_source_subjects, get_source_assignments

Best practices

  • Async: Prefer async with AsyncERClient(...) as client: so the session is closed even on errors.
  • Errors: Catch ERClientException; every client error subclasses it. Async maps common statuses to specific subclasses (ERClientBadRequest, ERClientBadCredentials, ERClientPermissionDenied, ERClientNotFound, ERClientRateLimitExceeded, ERClientInternalError, ERClientServiceUnreachable); unmapped statuses raise ERClientException itself, still with status_code set. Sync maps only 403 and 404 consistently, and sync-raised exceptions never populate exc.status_code (it is always None). For the codes sync does map, the exception type is the only signal — a 404 raises ERClientNotFound with no message at all — and the numeric status reaches the message text only for unmapped codes. There is no reliable way to branch on status with the sync client.
  • Time ranges: Pass timezone-aware datetime for start/end — correct on both clients. Sync silently ignores ISO strings there; async accepts them. Filter date_range bounds are ISO 8601 strings with timezone, e.g. "2023-11-10T00:00:00-06:00".
  • Sensor/camera-trap posts: Set provider_key on the client when posting sensor observations or camera trap reports.
  • Large reads: Sync: consider get_objects_multithreaded for big list endpoints. Async: use page_size and optional batch_size in get_events/get_observations; cursor-based pagination is used by default.
  • Rate limits: The API may throttle (e.g. one observation per second per source). Async maps 409/429 to ERClientRateLimitExceeded, with exc.retry_after in seconds; sync raises a plain ERClientException with exc.status_code unset — the status appears only in the message text.

For more on the EarthRanger API and event types, see EarthRanger and your ER instance's API documentation.

Release files for earthranger-client 1.17.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for earthranger-client 1.17.0
File Size Uploaded
earthranger_client-1.17.0.tar.gz 205.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for earthranger-client 1.17.0
File Interpreter ABI Platform
earthranger_client-1.17.0-py3-none-any.whl Python 3 none any Details

Total release size: 237.4 kB

Release files / earthranger_client-1.17.0.tar.gz

Download URL earthranger_client-1.17.0.tar.gz
Size 205.9 kB
Tags Source
SHA-256 checksum
How to use checksums
4fa58ae6ce64f10d646c4ec560a06cb6a6185a990b870e402b9b1d938083c960
BLAKE2b-256 checksum
How to use checksums
1cf7e1690f08a4e4f24aa490fe196446e4681b075fd828324dd7efb93a7e5898
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.

Transparency log

Release files / earthranger_client-1.17.0-py3-none-any.whl

Download URL earthranger_client-1.17.0-py3-none-any.whl
Size 31.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3e5b32242335c24de173995664207e414d89b553e8c292cb49cd3e422a87c266
BLAKE2b-256 checksum
How to use checksums
c33475695fda8b02fa0dad13108b4f7fa2cfd58627f5911c5b21e59b74d10157
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.17.0 This release

2 release files

1.16.0

2 release files

1.15.0

2 release files

1.14.0

2 release files

1.13.0

2 release files

1.12.0

2 release files

1.11.0

2 release files

1.10.0

2 release files

1.9.0

2 release files

1.8.0

2 release files

1.6.0

2 release files

1.5.2

1 release file

1.5.1

2 release files

1.5.0

2 release files

1.0.49

2 release files

1.0.44

2 release files

1.0.43

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page