API Pagination, Expansion & Plugin Items¶
FOG 1.6 adds three additive, opt-in capabilities to the REST API described
in the main API article:
- Pagination — page through large list endpoints with
start/length. - Relation expansion — inline full related objects with
?expand=…. - The
pluginItemsenvelope — a namespaced place where plugins inject their
associations without ever clobbering core fields.
Nothing changes unless you ask for it
These features are strictly additive. If you don't send ?expand=… and
don't page, every existing response keeps exactly the same shape it had
before. Scalar foreign keys (for example imageID) are always preserved —
expansion adds an object alongside the key, it never replaces the key.
All examples assume the API is enabled and you are sending the
fog-api-token and fog-user-token headers (see
Authentication).
Pagination¶
List endpoints (/fog/host, /fog/image, /fog/snapin, /fog/user, …) are
backed by FOG's DataTables server-side processor. Ask for a page with length
(and optionally start) and the response tells you both how big the full
result set is and how to reach the next page.
Parameters¶
| Parameter | Meaning | Default | Where |
|---|---|---|---|
length |
Maximum number of rows to return. | all | query string or body |
start |
Zero-based offset of the first row to return. | 0 |
query string or body |
search |
Free-text filter (DataTables global search value). | none | body only |
order |
DataTables ordering array. | by name | body only |
draw |
Echoed back verbatim; useful for correlating async responses. | 0 |
body only |
The easy way: ?length on the URL
length and start work directly as query-string parameters, so a plain
GET .../fog/host?length=3 returns the first three hosts. This is the
simplest way to page and is what the examples below use. (If you omit
start, it defaults to 0.)
search / order / draw still go in the request body
The full DataTables parameters (search, order, draw) are read from the
request body because of the internal web-server rewrite that fronts the
API. Pass them with curl --data; the request is still a GET. Only
length and start may be sent either way.
Response envelope¶
{
"draw": 0,
"recordsTotal": 85,
"recordsFiltered": 85,
"recordsReturned": 3,
"data": [ { "id": 1, "name": "..." }, ... ],
"firstUrl": "/fog/host?length=3&start=0",
"prevUrl": null,
"nextUrl": "/fog/host?length=3&start=3",
"lastUrl": "/fog/host?length=3&start=84",
"_lang": "host"
}
recordsTotal— total rows in the table before filtering.recordsFiltered— rows matching the currentsearch(equals
recordsTotalwhen no search is applied).recordsReturned— how many rows are in this response (the size of
data). Always present on a list response.data— the current page of rows.firstUrl/prevUrl/nextUrl/lastUrl— request-relative URLs for the
first, previous, next and last pages. Present only when you asked for a
bounded page (length) against a non-empty result set; each isnullwhen it
does not apply (prevUrlon the first page,nextUrlon the last). They
echo the path you requested and preserve every other query parameter,
including?expand=….
The full counts never shrink to the page
recordsTotal and recordsFiltered always describe the whole result
set, not the current page — the web UI depends on that. Use
recordsReturned to see how big the page you got back actually is.
The Link header¶
The same first/previous/next/last pointers are also emitted as a standard
RFC 5988 Link response header, so a
client can page without reading the body:
Link: </fog/host?length=3&start=0>; rel="first",
</fog/host?length=3&start=3>; rel="next",
</fog/host?length=3&start=84>; rel="last"
rel="prev" is omitted on the first page and rel="next" on the last, so you
can stop as soon as there is no next.
Examples¶
First three hosts:
curl -H 'fog-api-token: yourapitoken' \
-H 'fog-user-token: yourusertoken' \
'http://fogserver/fog/host?length=3'
The next page (rows 4–5):
curl -H 'fog-api-token: yourapitoken' \
-H 'fog-user-token: yourusertoken' \
'http://fogserver/fog/host?length=2&start=3'
Walking every page automatically¶
Because each response hands back nextUrl (and the same pointer in the Link
header), a client can loop until there is nothing left — you never have to track
offsets yourself. Start from any ?length=… URL and follow nextUrl until it
is null.
Bash (curl + jq):
#!/usr/bin/env bash
base='http://fogserver'
next='/fog/host?length=50'
while [ -n "$next" ] && [ "$next" != "null" ]; do
page="$(curl -s \
-H 'fog-api-token: yourapitoken' \
-H 'fog-user-token: yourusertoken' \
"${base}${next}")"
echo "$page" | jq -c '.data[] | {id, name}'
next="$(echo "$page" | jq -r '.nextUrl')" # "null" when done
done
Python (requests):
import requests
base = "http://fogserver"
headers = {
"fog-api-token": "yourapitoken",
"fog-user-token": "yourusertoken",
}
url = "/fog/host?length=50"
hosts = []
while url:
body = requests.get(base + url, headers=headers).json()
hosts.extend(body["data"])
print(f"got {body['recordsReturned']} of {body['recordsFiltered']}")
url = body.get("nextUrl") # None on the last page -> loop ends
print(f"total collected: {len(hosts)}")
Ruby (net/http):
require "net/http"
require "json"
base = "http://fogserver"
headers = {
"fog-api-token" => "yourapitoken",
"fog-user-token" => "yourusertoken",
}
url = "/fog/host?length=50"
hosts = []
until url.nil?
res = Net::HTTP.get_response(URI(base + url), headers)
body = JSON.parse(res.body)
hosts.concat(body["data"])
puts "got #{body['recordsReturned']} of #{body['recordsFiltered']}"
url = body["nextUrl"] # nil on the last page -> loop ends
end
puts "total collected: #{hosts.length}"
Relation expansion (?expand)¶
By default a list row or a single entity contains scalar foreign keys
(imageID, snapins as an array of IDs, and so on). Add ?expand=… to inline
the full related objects instead of having to make a second round of calls.
Requesting expansion¶
expand is a comma-separated list of relation tokens on the URL query string.
Tokens are case-insensitive.
| Form | Effect |
|---|---|
?expand=image |
Inline the single image relation. |
?expand=snapins,printers |
Inline several relations. |
?expand=all |
Inline every relation the entity supports. |
Expansion works on both a single-entity GET (/fog/host/48?expand=all) and
a list endpoint (/fog/host?expand=snapins) — in the list case every row on the
page is enriched.
Supported relations¶
The set of expandable relations depends on the entity (and on which plugins are
installed). For a host the built-in relations are:
| Token | Kind | Inlines |
|---|---|---|
image |
one | The assigned image object. |
snapins |
many | The host's snapin objects. |
printers |
many | The host's printer objects. |
groups |
many | The groups the host belongs to. |
modules |
many | The host's client modules. |
The Location plugin adds two more tokens, delivered through the
pluginItems envelope: location (on a host) and
hosts (on a location).
One-to-one relations¶
A one-to-one relation is inlined as an object under its token, next to the
untouched scalar key:
{
"id": 48,
"imageID": 5,
"image": { "id": 5, "name": "Win11", "path": "/images/win11", ... }
}
One-to-many relations¶
A one-to-many relation becomes an array under its token, accompanied by two
companion keys so you can detect truncation:
{
"modules": [ { "id": 1, "name": "..." }, ... ],
"modules_total": 13,
"modules_truncated": false
}
<token>_total— the true number of related items (before any cap).<token>_truncated—truewhen the array was capped and therefore does
not contain every item.
Expansion is capped at 2500 items per relation
To bound memory, each one-to-many relation inlines at most 2500 items.
When more exist, the array holds the first 2500, <token>_total reports the
real count, and <token>_truncated is true. Fetch the remainder from that
relation's own list endpoint.
Depth is one level¶
Expansion goes exactly one level deep. Inlined related objects are not
themselves expanded and do not carry a pluginItems envelope, so responses can
never recurse or contain back-references to their parent.
Pagination + expansion together¶
Because expansion materializes a full object for every row, an expanded list
request bounds its own page size:
- If you send an explicit
lengthof 2500 or fewer, it is honored. - If
lengthis omitted,0, or greater than 2500, it is clamped to
2500 (andstartdefaults to0).
So to page through an expanded list, always send an explicit
length ≤ 2500:
curl -H 'fog-api-token: yourapitoken' \
-H 'fog-user-token: yourusertoken' \
'http://fogserver/fog/host?expand=snapins&length=100'
The nextUrl/Link pointers returned by an expanded list keep the ?expand=…
token, so the page-walking loops above work unchanged against expanded lists.
Sensitive fields are never exposed by expansion¶
Decrypted secrets (Active Directory password, product key, client security
tokens, and the like) are returned only on a direct single-entity GET of the
owning object (for example GET /fog/host/48). They are always stripped from:
- every row of a list response, including expanded list rows, and
- any object inlined as a related object or through
pluginItems.
In other words, GET /fog/host/48 may return the host's decrypted ADPass, but
that host appearing inside GET /fog/location/1?expand=hosts, or as a row of
GET /fog/host?expand=all, will not.
The pluginItems envelope¶
Plugins can contribute their associations to another entity's API output. To
guarantee a plugin can never overwrite (or be confused with) a core field, all
plugin-contributed data lives under a single namespaced key: pluginItems.
{
"id": 48,
"name": "lab-pc-01",
"imageID": 5,
"pluginItems": {
"location": { "id": 1, "name": "Main Office", "link": "..." }
}
}
Like the core expansion, pluginItems is only ever attached at the top
level — on a single-entity GET or on each row of a list — never on a nested
related object. That is what keeps plugin data free of back-references.
Example: the Location plugin¶
The Location plugin is bidirectional and demonstrates the pattern.
On a host — pluginItems.location is a lightweight link by default:
"pluginItems": {
"location": {
"id": 1,
"name": "Main Office",
"link": "../management/index.php?node=location&sub=edit&id=1"
}
}
Add ?expand=location to inline the full location object instead:
curl -H 'fog-api-token: yourapitoken' \
-H 'fog-user-token: yourusertoken' \
-X GET 'http://fogserver/fog/host/48?expand=location'
"pluginItems": {
"location": { "id": 1, "name": "Main Office", "storagegroup": {...}, ... }
}
On a location — pluginItems.hostCount is always present; add
?expand=hosts to also inline the member hosts:
curl -H 'fog-api-token: yourapitoken' \
-H 'fog-user-token: yourusertoken' \
-X GET 'http://fogserver/fog/location/1?expand=hosts'
"pluginItems": {
"hostCount": 42,
"hosts": [ { "id": 9, "name": "..." }, ... ],
"hosts_truncated": false
}
The inlined hosts array follows the same rules as any expanded one-to-many
relation: capped at 2500, hosts_truncated signals a cap, and each host has its
sensitive fields stripped.
Quick reference¶
| Goal | How |
|---|---|
| Page a list | ?length=<n> (and optional ?start=<n>) on the URL. |
| Follow pages automatically | Loop on nextUrl (or the Link header) until it is null. |
| See how many rows a page returned | Read recordsReturned. |
| Inline one relation | ?expand=<token> |
| Inline several relations | ?expand=a,b,c |
| Inline everything | ?expand=all |
| Detect a capped one-to-many relation | Check <token>_truncated / <token>_total. |
| Page an expanded list | Send an explicit length ≤ 2500. |
| Read plugin associations | Look under pluginItems. |
| Get decrypted secrets | Direct single-entity GET only (never lists/expansions). |