Pagination & Filtering
Every collection endpoint in FreeSDN returns a consistent paginated envelope. This page explains the query parameters you use to control page size, navigate pages, scope results to a site, filter by search term, and sort - along with the exact per-endpoint limits you need to plan around.
The paginated response envelope
Section titled “The paginated response envelope”All list endpoints return the same top-level shape:
{ "items": [ ... ], "total": 412, "page": 1, "per_page": 20, "pages": 21}| Field | Type | Description |
|---|---|---|
items | array | The objects for the current page |
total | integer | Total matching records across all pages |
page | integer | Current page number (1-indexed) |
per_page | integer | Number of items returned on this page |
pages | integer | Total number of pages at the current per_page |
The total and pages fields let you iterate without making an extra count request.
Pagination parameters
Section titled “Pagination parameters”- Type: integer
- Default:
1 - Minimum:
1(values below 1 are rejected with HTTP 422)
per_page
Section titled “per_page”- Type: integer
- Default: varies by endpoint (20 for users, sites, organizations, webhooks, integrations, and most automation routes; 25 for devices, agents, and controllers; 50 for audit, events, logs, switches, access-points, and automation executions - see the table below for the per-endpoint value)
- Maximum: varies by endpoint (see table below)
# Get page 3 of devices, 50 per pagecurl "https://freesdn.example.com/api/v1/devices/?page=3&per_page=50" \ -H "Authorization: Bearer $TOKEN"Per-endpoint per_page maximums
Section titled “Per-endpoint per_page maximums”The cap is set per resource to protect against large result sets and DB load. The most common limits are:
| Endpoint prefix | Default per_page | Maximum per_page |
|---|---|---|
/api/v1/users/ | 20 | 200 |
/api/v1/sites/ | 20 | 100 |
/api/v1/devices/ | 25 | 500 |
/api/v1/agents/ | 25 | 200 |
/api/v1/audit/ (all list routes) | 50 | 200 |
/api/v1/events/ | 50 | 200 |
/api/v1/logs/ | 50 | 200 |
/api/v1/organizations/ | 20 | 200 |
/api/v1/switches/ | 50 | 200 |
/api/v1/access-points/ (AP list) | 50 | 200 |
/api/v1/webhooks/ | 20 | 100 |
/api/v1/integrations/ | 20 | 100 |
/api/v1/marketplace/ reviews | 10 | 50 |
| Automation rules | 50 | 200 |
| Automation executions / logs | 20 | 100 |
Iterating all pages
Section titled “Iterating all pages”# Fetch the first page to learn how many pages there areFIRST=$(curl -s "https://freesdn.example.com/api/v1/devices/?page=1&per_page=100" \ -H "Authorization: Bearer $TOKEN")
PAGES=$(echo $FIRST | jq '.pages')
# Iterate the remainderfor PAGE in $(seq 2 $PAGES); do curl -s "https://freesdn.example.com/api/v1/devices/?page=$PAGE&per_page=100" \ -H "Authorization: Bearer $TOKEN"doneSite scoping with site_id
Section titled “Site scoping with site_id”Most collection endpoints accept an optional site_id query parameter. When provided, the backend filters results to resources belonging to that site only. The parameter is a UUID.
curl "https://freesdn.example.com/api/v1/devices/?site_id=6f2a1c3e-0001-4d8a-9b7c-000000000001" \ -H "Authorization: Bearer $TOKEN"How site scoping is enforced
Section titled “How site scoping is enforced”The site_id parameter is not merely a filter hint - the server enforces it as a tenancy boundary:
- The org-scope check runs first: the requested site must belong to the caller’s organization.
- If the caller is site-limited (has at least one explicit
UserSiteAccessgrant), they can only retrieve resources for sites they are explicitly granted. Passing asite_idoutside their grant set returns an emptyitemslist, not a 403 - to avoid leaking site existence. - Passing no
site_idreturns all resources the caller is permitted to see across the organization (which may be restricted to their granted sites).
Endpoints that require site_id
Section titled “Endpoints that require site_id”Some endpoints (particularly adapter sub-resources under /gateway-*) embed the site ID directly in the path rather than as a query parameter. The URL pattern there is always:
/api/v1/gateway-<area>/{controller_id}/sites/{site_id}/...In this case site_id is a required path segment, not an optional query param. Omitting it resolves to a 404 rather than returning cross-site data.
Search filtering
Section titled “Search filtering”Endpoints that support free-text search accept a search query parameter. The backend applies a case-insensitive ILIKE pattern match against the relevant fields for that resource type.
# Search users whose name or email contains "alice"curl "https://freesdn.example.com/api/v1/users/?search=alice" \ -H "Authorization: Bearer $TOKEN"The % and _ SQL wildcard characters are automatically escaped before the ILIKE is constructed, so your search strings are treated as literals.
Which endpoints support search
Section titled “Which endpoints support search”The search parameter is available on the main admin resource endpoints: users, sites, organizations, devices, agents, and others. Adapter sub-resources (switches, access points, cameras, VoIP phones) use their own vendor-specific filter params - consult the OpenAPI spec at /api/v1/openapi.json for the exact parameter names per endpoint.
Additional filters
Section titled “Additional filters”Beyond site_id and search, individual endpoints expose their own filter parameters. The most common patterns are:
| Parameter | Type | Where used | Effect |
|---|---|---|---|
site_id | UUID | Most collection endpoints | Restrict to one site |
search | string | Admin resource endpoints | Case-insensitive substring match |
status | string | Agents, devices, tasks | Filter by lifecycle state (online, offline, pending, etc.) |
feature_prefix | string | Pending changes | Filter staged changes by feature domain (e.g., vpn.) |
vendor | string | gateway-vpn/changes/by-gateway | Fan-out to a specific vendor adapter |
protocol | string | SSO providers | Filter by SSO protocol (oidc, saml, ldap) |
All filters are AND-combined with each other and with the tenant/site scope.
Soft-deleted records
Section titled “Soft-deleted records”FreeSDN uses soft deletes: rows carry a deleted_at timestamp and are excluded from all list queries automatically. You will never see a deleted user, site, or device in a paginated response unless you are using an internal admin interface. There is no include_deleted parameter exposed on the public API.
Sorting
Section titled “Sorting”Most core resource endpoints return results in a sensible default order (typically creation time descending for admin resources, name ascending for named resources like sites). Explicit sort_by / order parameters are not yet standardized across all endpoints.
Combining parameters
Section titled “Combining parameters”Parameters are independent and can be combined freely:
# Page 2, 50 per page, filtered to a site, searching for "switch"curl "https://freesdn.example.com/api/v1/devices/?page=2&per_page=50&site_id=<UUID>&search=switch" \ -H "Authorization: Bearer $TOKEN"The server applies filters in this order: tenant scope → site scope → search → pagination.
Rate limiting and pagination
Section titled “Rate limiting and pagination”Iterating pages quickly with many API calls can trigger the per-minute rate limit. The default is 600 requests per minute per authenticated user, with a burst allowance of 120 requests per second. If you are iterating a large dataset, add a small delay between page requests or use a larger per_page value to minimize round trips.
When rate-limited you receive one of two responses depending on which limit was hit:
Burst limit exceeded (more than 120 requests in the last second):
HTTP 429 Too Many RequestsRetry-After: 1Per-minute limit exceeded (more than 600 requests in the last minute):
HTTP 429 Too Many RequestsRetry-After: 60The X-RateLimit-Limit and X-RateLimit-Remaining headers are included on every non-rate-limited response so you can back off proactively.
Error shapes for invalid parameters
Section titled “Error shapes for invalid parameters”Parameter validation errors return HTTP 422 with a structured body:
{ "error": { "code": 422, "message": "Validation error", "request_id": "ext-abc123", "details": [ { "field": "per_page", "message": "Input should be less than or equal to 100", "type": "less_than_equal" } ] }}The details array lists every field that failed validation, so you can fix all problems in one round trip rather than discovering them one at a time.
Trailing slash handling
Section titled “Trailing slash handling”Both /devices and /devices/ resolve identically. A TrailingSlashNormalizeMiddleware rewrites the path server-side without issuing a redirect, so you will never see a 307 redirect leak an internal proxy hostname when iterating pages.
Next steps
Section titled “Next steps”- Authentication - obtain a token and API key before making collection calls
- Using the API - worked examples including the staged-write apply flow
- API Overview - full route inventory and OpenAPI spec location