Skip to content
Docs

Get started

Making requests

Request and response formats, filtering, sorting and pagination: the conventions every endpoint shares.

Two formats

Endpoints come in two families:

PathsFormatContent-Type
/api/json/…JSON:API: devices, organizations, users, usage, messages, plans, invoicesapplication/vnd.api+json
/api/auth/…, /api/mePlain JSONapplication/json
/api/exports/…CSV files(no body)

Send Content-Type: application/vnd.api+json with every JSON:API request that has a body. Plain application/json there answers 415 Unsupported Media Type.

Reading records

A record comes back as a resource object: its type, its id and its attributes. Lists put an array of them in data.

GET /api/json/devices/{id}200 OK
{
  "data": {
    "type": "device",
    "id": "5f0c2b7e-8d41-4a96-b3e2-91c7d0a4e6f8",
    "attributes": {
      "iccid": "8999001234000020718",
      "imsi": "901880000010001",
      "status": "active",
      "customer_id": "c41e9a03-6b2f-4d8e-a571-0f3b9c2d7e14",
      "usage_this_month_bytes": 475037,
      …
    }
  },
  "jsonapi": { "version": "1.0" }
}

Fields you aren't allowed to see are left out entirely rather than sent as null: Staff never get prices or costs, and resellers never get their customers' message payloads.

Creating and updating

Creates send a resource object with its type and attributes. Updates also carry the record's id, the same as in the URL. Send only the attributes you change.

Create
curl -X POST https://api.houndsat.com/api/json/webhook_endpoints \
  -H "Authorization: Bearer $HOUNDSAT_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhook_endpoint",
      "attributes": { "url": "https://fleet.example.com/hooks", "description": "Fleet telemetry" }
    }
  }'
Update
curl -X PATCH https://api.houndsat.com/api/json/devices/5f0c2b7e-8d41-4a96-b3e2-91c7d0a4e6f8/assign \
  -H "Authorization: Bearer $HOUNDSAT_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "device",
      "id": "5f0c2b7e-8d41-4a96-b3e2-91c7d0a4e6f8",
      "attributes": { "customer_id": "c41e9a03-6b2f-4d8e-a571-0f3b9c2d7e14" }
    }
  }'

Creates answer 201 with the new record; updates answer 200. Relationships are never sent: refer to other records by id attributes such as customer_id.

Actions and bulk requests

Imports and bulk operations aren't records, so their arguments go straight under data, without type or attributes:

Bulk suspend
curl -X POST https://api.houndsat.com/api/json/devices/bulk/suspend \
  -H "Authorization: Bearer $HOUNDSAT_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data": {"ids": ["5f0c2b7e-8d41-4a96-b3e2-91c7d0a4e6f8", "0b6d4e2a-1c9f-4a3b-8e5d-7f2c0a9b6d13"]}}'

Bulk requests take 1 to 500 ids and always answer 200 with one result per id, in the order given. Each id succeeds or fails on its own: check every result's status.

Filtering and search

Lists take filter[field]=value for an exact match, or filter[field][operator]=value with an operator:

OperatorMatches
inAny of several values: filter[status][in][]=active&filter[status][in][]=suspended
is_niltrue for records without a value, false for records with one
greater_than, greater_than_or_equalAfter or from a value (timestamps, numbers)
less_than, less_than_or_equalBefore or up to a value

Combine as many filters as you need; they all apply. Devices also take search, which matches ICCIDs, IMSIs and external IDs starting with the text. Each list in the API reference names the fields it filters on.

Filters
# Active devices of one customer
curl -g "https://api.houndsat.com/api/json/devices?filter[status]=active&filter[customer_id]=c41e9a03-6b2f-4d8e-a571-0f3b9c2d7e14" \
  -H "Authorization: Bearer $HOUNDSAT_TOKEN"

# Devices not heard from since October 1
curl -g "https://api.houndsat.com/api/json/devices?filter[last_seen_at][less_than]=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer $HOUNDSAT_TOKEN"

# Suspended or terminated devices
curl -g "https://api.houndsat.com/api/json/devices?filter[status][in][]=suspended&filter[status][in][]=terminated" \
  -H "Authorization: Bearer $HOUNDSAT_TOKEN"

Sorting

Each list has a default order, usually newest first. sort=status sorts ascending, sort=-inserted_at descending, and sort=status,-inserted_at by several fields.

Pagination

Every list is paginated: 50 records per page by default, up to 200 with page[limit]. Follow links.next until it is null.

A page
{
  "data": [ … ],
  "links": {
    "self": "https://api.houndsat.com/api/json/devices?page[limit]=100",
    "first": "https://api.houndsat.com/api/json/devices?page[limit]=100",
    "next": "https://api.houndsat.com/api/json/devices?page[after]=g2wAAAACdAAAAA13…&page[limit]=100",
    "prev": null
  },
  "meta": { "page": { "limit": 100 } }
}
Read every page (Node.js)
let url = "https://api.houndsat.com/api/json/devices?page[limit]=200"
const devices = []

while (url) {
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.HOUNDSAT_TOKEN}` },
  })
  const page = await response.json()
  devices.push(...page.data)
  url = page.links.next // null on the last page
}

page[after] cursors stay stable while records are added. If you'd rather jump to a page, page[offset]=200 works too. Add page[count]=true to get the total number of matches in meta.page.total.

Choosing fields

To keep responses small, list the attributes you need: fields[device]=iccid,status,last_seen_at. The type name is singular, as in the records' type.