Skip to content

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.

All list endpoints return the same top-level shape:

{
"items": [ ... ],
"total": 412,
"page": 1,
"per_page": 20,
"pages": 21
}
FieldTypeDescription
itemsarrayThe objects for the current page
totalintegerTotal matching records across all pages
pageintegerCurrent page number (1-indexed)
per_pageintegerNumber of items returned on this page
pagesintegerTotal number of pages at the current per_page

The total and pages fields let you iterate without making an extra count request.


  • Type: integer
  • Default: 1
  • Minimum: 1 (values below 1 are rejected with HTTP 422)
  • 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)
Terminal window
# Get page 3 of devices, 50 per page
curl "https://freesdn.example.com/api/v1/devices/?page=3&per_page=50" \
-H "Authorization: Bearer $TOKEN"

The cap is set per resource to protect against large result sets and DB load. The most common limits are:

Endpoint prefixDefault per_pageMaximum per_page
/api/v1/users/20200
/api/v1/sites/20100
/api/v1/devices/25500
/api/v1/agents/25200
/api/v1/audit/ (all list routes)50200
/api/v1/events/50200
/api/v1/logs/50200
/api/v1/organizations/20200
/api/v1/switches/50200
/api/v1/access-points/ (AP list)50200
/api/v1/webhooks/20100
/api/v1/integrations/20100
/api/v1/marketplace/ reviews1050
Automation rules50200
Automation executions / logs20100
Terminal window
# Fetch the first page to learn how many pages there are
FIRST=$(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 remainder
for PAGE in $(seq 2 $PAGES); do
curl -s "https://freesdn.example.com/api/v1/devices/?page=$PAGE&per_page=100" \
-H "Authorization: Bearer $TOKEN"
done

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.

Terminal window
curl "https://freesdn.example.com/api/v1/devices/?site_id=6f2a1c3e-0001-4d8a-9b7c-000000000001" \
-H "Authorization: Bearer $TOKEN"

The site_id parameter is not merely a filter hint - the server enforces it as a tenancy boundary:

  1. The org-scope check runs first: the requested site must belong to the caller’s organization.
  2. If the caller is site-limited (has at least one explicit UserSiteAccess grant), they can only retrieve resources for sites they are explicitly granted. Passing a site_id outside their grant set returns an empty items list, not a 403 - to avoid leaking site existence.
  3. Passing no site_id returns all resources the caller is permitted to see across the organization (which may be restricted to their granted sites).

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.


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.

Terminal window
# 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.

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.


Beyond site_id and search, individual endpoints expose their own filter parameters. The most common patterns are:

ParameterTypeWhere usedEffect
site_idUUIDMost collection endpointsRestrict to one site
searchstringAdmin resource endpointsCase-insensitive substring match
statusstringAgents, devices, tasksFilter by lifecycle state (online, offline, pending, etc.)
feature_prefixstringPending changesFilter staged changes by feature domain (e.g., vpn.)
vendorstringgateway-vpn/changes/by-gatewayFan-out to a specific vendor adapter
protocolstringSSO providersFilter by SSO protocol (oidc, saml, ldap)

All filters are AND-combined with each other and with the tenant/site scope.


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.


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.


Parameters are independent and can be combined freely:

Terminal window
# 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.


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 Requests
Retry-After: 1

Per-minute limit exceeded (more than 600 requests in the last minute):

HTTP 429 Too Many Requests
Retry-After: 60

The X-RateLimit-Limit and X-RateLimit-Remaining headers are included on every non-rate-limited response so you can back off proactively.


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.


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.


  • 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