Get started
Making requests
Request and response formats, filtering, sorting and pagination: the conventions every endpoint shares.
Two formats
Endpoints come in two families:
| Paths | Format | Content-Type |
|---|---|---|
/api/json/… | JSON:API: devices, organizations, users, usage, messages, plans, invoices | application/vnd.api+json |
/api/auth/…, /api/me | Plain JSON | application/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.
{
"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.
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" }
}
}'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:
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:
| Operator | Matches |
|---|---|
in | Any of several values: filter[status][in][]=active&filter[status][in][]=suspended |
is_nil | true for records without a value, false for records with one |
greater_than, greater_than_or_equal | After or from a value (timestamps, numbers) |
less_than, less_than_or_equal | Before 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.
# 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.
{
"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 } }
}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.