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

All product names, logos, and brands are property of their respective owners. FreeSDN is an independent project and is not affiliated with or endorsed by the vendors it integrates with. See Trademarks.