Skip to contents

This package reads and writes telemetry records — individuals, blueprints, devices, deployments, sensor deployments, captures, events and GPS positions — in the SpeedGoat database.

There is nothing to configure. The first call that needs credentials starts the login flow, and the API reads your project from your token, so no function here takes a project_id.

Reading

Every read takes filters through ..., which become filter[...] on the request:

devices <- telem_read_devices()
elk <- telem_read_individuals(species_id = 4)
dead <- telem_read_individuals(fate = "Dead")

recent <- telem_read_positions(
  device_id = 20,
  from = "2026-01-01",
  to = "2026-06-30"
)

The filters people ask for most are named

Rather than remembering filter names, the common ones are arguments:

telem_read_individuals(species_id = 4, sex = "Female", fate = "Dead")
telem_read_devices(state = "Deployed")
telem_read_events(type = "Mortality", from = "2026-01-01", to = "2026-06-30")
telem_read_positions(bounds = c(47.0, -124.5, 47.6, -123.8))  # see below

bounds is the exception: it serialises correctly and the endpoint returns 500 for every value, including a whole-world box. The failure is in the API’s query building rather than in what you pass, so there is nothing to get right on this side. The argument stays because it will work unchanged once the server side is fixed. Until then, filter positions by deployment and subset locally.

Three things are worth knowing about which filters exist, because the API is uneven and guessing wastes time:

  • Species is only on individuals. Devices, deployments, captures, events and positions carry no species, so you reach them in two steps — read the individuals for a species, then filter on the ids you get back.
  • Space is only on positions, as bounds — and that filter is currently broken server-side, as above. Other endpoints have bare latitude/longitude, which test for equality and are no use as a box.
  • Time is on every endpoint but under a different column each — the event date, the capture date, the deployment start. from and to map to whichever one the endpoint means, so you do not have to know its name.
# Every position for one species.
elk <- telem_read_individuals(species_id = 4)
telem_read_positions(individual_id = paste(elk$id, collapse = ","))

A filter value takes a small vocabulary beyond plain equality:

Written as Means
"Dead" equals
"!Dead" does not equal
"a,b,c" any of these
"~elk" contains
"?" / "!?" is null / is not null
">10" / "<10" greater / less than

There is no “between”: ">a,<b" does not do what it looks like. That is why periods are from and to rather than one string.

Reads return everything by default

The API paginates at 100 records. These functions ask for all of them, because a read that silently returns the first page is a correctness bug waiting to happen — on one project’s device list that was 100 of 179 records, with nothing to say the rest existed.

Pass pages when you would rather page deliberately:

telem_read_devices(pages = list(size = 100))              # first 100
telem_read_devices(pages = list(size = 100, number = 2))  # second 100

A read with no filters at all asks the API how many records there are before fetching them, and warns when that is more than max_records — 50,000 by default. The event tables run to millions of rows, so the whole of one is rarely what anybody meant. Filtering, or passing pages explicitly, skips the check.

Finding out what an endpoint accepts

Rather than guessing filter names, ask the API to describe itself:

# Every endpoint, and the actions each allows.
spdgt.auth::api_discover("telemetry")

# What one endpoint offers: the filters, includes, sorts and pagination
# for reads.
spdgt.auth::api_discover("telemetry", "/events")

Discovery is built from the API’s OpenAPI annotations, which have drifted from the validation rules that actually run — so for the fields a write accepts, prefer telem_fields() below.

Writing

Creates take a data frame, one row per record, and go in batches:

telem_create_devices(tibble::tibble(
  serial_number = c("103633", "103635"),
  state         = "In-Hand"
))

Updates take a single row containing id; deletes take an id:

telem_update_device(tibble::tibble(id = 20, state = "Deployed"))
telem_delete_device(20)

Ordering matters once records reference each other, and a create returns something less convenient than it looks. Both are covered in vignette("loading-data").

A create also refuses to run when your session carries no project. The API would accept such a write and report success, but the row it makes cannot be read, updated or deleted afterwards by anyone — so the guard is worth the interruption. spdgt.auth::auth_switch_project() is the fix it names.

What the API accepts

Every create checks your data before building a request, so a bad column fails locally with the whole list of problems rather than returning a 422 partway through a batch. telem_fields() is the list of writable fields per endpoint, and telem_validate() runs the same check on its own:

telem_fields("devices")
#> # A tibble: 9 × 9
#>   resource field         presence na_ok type      enum   max_len min_val max_val
#>   <chr>    <chr>         <chr>    <lgl> <chr>     <chr>    <int>   <dbl>   <dbl>
#> 1 devices  serial_number required FALSE character NA          50      NA      NA
#> 2 devices  state         present  TRUE  character telem…      25      NA      NA
#> 3 devices  blueprint_id  optional TRUE  integer   NA          NA      NA      NA
#> 4 devices  price         optional TRUE  numeric   NA          NA       0      NA
#> 5 devices  purchased_at  optional TRUE  date      NA          NA      NA      NA
#> 6 devices  retired_at    optional TRUE  date      NA          NA      NA      NA
#> 7 devices  frequency     optional TRUE  numeric   NA          NA       0      NA
#> 8 devices  notes         optional TRUE  character NA          NA      NA      NA
#> 9 devices  project_id    optional TRUE  integer   NA          NA      NA      NA
telem_validate(data.frame(serial_number = "103633"), "devices")
#> Error in `telem_validate()`:
#> ! `data` is not ready to send to "devices".
#> ✖ Required column state is missing.

vignette("loading-data") covers what the columns of that table mean.

Enumerated fields have a fixed vocabulary, exposed so a value can be checked against the list rather than guessed at:

telem_device_states()
#>  [1] "Unknown"         "Deployed"        "On"              "Off"            
#>  [5] "Paused"          "Halted"          "Faulty"          "Offline"        
#>  [9] "Low-Battery"     "Depleted"        "Dropped"         "In-Hand"        
#> [13] "In-Repair"       "At-Warehouse"    "At-Manufacturer" "Decommissioned" 
#> [17] "Retired"
telem_fates()
#> [1] "Unknown" "Alive"   "Sick"    "Injured" "Dead"
telem_capture_methods()
#> [1] "Dart"       "Net-Gun"    "Free-Range" "Culvert"    "Snare"     
#> [6] "Leg-Hold"

What you are allowed to do

The API decides from your role, not from which package you installed:

Operation Requires
read, create, update membership of the project
delete the Admin role, and membership of the project

Without the Admin role a delete returns 403 rather than deleting.

Seeing a request without sending it

Every function takes dry_run, and verbosity up to 3 for the full exchange:

telem_create_devices(devices, dry_run = TRUE, verbosity = 2)