Skip to main content
RunBook Academy

OPNsenseXLII · API and AutomationAPI and automation fundamentals

API endpoints overview — the taxonomy, the namespaces, and where to find what you need

Intermediate⏱ ~14 mincurljqfirefox

What you'll learn

  • Describe the URL taxonomy of the OPNsense REST API and the commands each object supports
  • Locate endpoints by module without reading the source code
  • Distinguish read commands (search/get) from write commands (add/set/del/toggle) and from the separate apply step
  • Discover endpoints for an unfamiliar configuration area from the GUI network traffic or the controller source

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

The OPNsense REST API surface is large — every GUI page is, in principle, an endpoint. The full surface is hundreds of routes organised by namespace. For an automation author, the question is not “what are all the endpoints” but “where do I find the endpoint for the configuration change I want to make, and what verbs does it support”. The answer is a small amount of taxonomy and a small amount of pattern.

This lesson covers the URL taxonomy, the namespaces, the verbs each endpoint supports, and three reliable strategies for discovering an endpoint the operator has not memorised.

The URL shape

Every OPNsense API URL has the shape https://<host>/api/<module>/<controller>/<command>, optionally followed by positional parameters:

/api/core/firmware/status
/api/firewall/filter/addRule
/api/firewall/filter/setRule/f1e2d3c4-b5a6-7890-1234-567890abcdef
/api/interfaces/overview/interfacesInfo
/api/wireguard/client/searchClient

Four parts:

  • api — fixed prefix for the REST surface.
  • module — a major area of configuration: core (firmware, backups, services, system), firewall (filter rules, aliases, NAT), interfaces, diagnostics (read-only diagnostics), auth (users, groups, privileges), trust (certificates, CAs), and one module per service — unbound, dnsmasq, kea, wireguard, ipsec, openvpn, routes, routing, and so on.
  • controller — within a module, a sub-area. core/firmware is firmware; core/system is halt/reboot/status; firewall/filter is the automation rule set; firewall/alias is aliases; firewall/source_nat is outbound NAT.
  • command — the action, plus any positional parameters it takes. Commands that operate on one record take the UUID in the path rather than in the body: setRule/{uuid}, delRule/{uuid}, toggleRule/{uuid}/{enabled}.

The verb is usually a <verb><Object> pair, because one controller can manage several kinds of record. The firewall filter controller is the clearest case: searchRule (list), getRule/{uuid} (read one), addRule (create), setRule/{uuid} (update), delRule/{uuid} (delete), toggleRule/{uuid}/{enabled} (enable or disable), and apply (load the staged rules into pf). The alias controller uses the same pattern against a different object — searchItem, addItem, setItem/{uuid}, delItem/{uuid}, toggleItem/{uuid} — and finishes with reconfigure instead of apply.

Common namespaces

The namespaces the operator returns to repeatedly:

ModulePurposeCommon controllers
coreFirmware, system, services, backups, menufirmware, system, service, backup, snapshots, menu
authUsers, groups, privilegesuser, group, priv
firewallAutomation rules, aliases, NAT, categoriesfilter, alias, alias_util, category, source_nat, one_to_one, npt, group
interfacesInterface overview and assignmentoverview, settings, vlan_settings, vip_settings, lagg_settings
diagnosticsRead-only diagnosticsfirewall, interface, log, system, activity, dns, traceroute, packet_capture
unbound, dnsmasq, keaPer-service configuration; each service is its own modulesettings, service
wireguard, ipsec, openvpnVPN configuration and status; again one module eachclient, server, general, service
trustCertificates, CAs, CRLscert, ca, crl, settings
routes, routingStatic routes and gatewaysgateway, routes, settings
idsSuricatasettings, service

There is no services or vpn grouping module: Unbound is /api/unbound/..., WireGuard is /api/wireguard/..., and a plugin adds its own top-level module when it is installed. Users and groups live under auth, not core.

A few endpoints are particularly common because they appear in nearly every automation task:

EndpointPurpose
GET /api/core/firmware/statusCurrent version, latest available
GET /api/core/system/statusSystem status summary
GET /api/interfaces/overview/interfacesInfoInterface list and addresses
GET /api/firewall/filter/searchRuleList firewall rules
POST /api/firewall/filter/addRuleAdd an automation rule
POST /api/firewall/filter/setRule/{uuid}Update an existing rule
POST /api/firewall/filter/toggleRule/{uuid}/{enabled}Enable or disable a rule
POST /api/firewall/filter/delRule/{uuid}Delete a rule
POST /api/firewall/filter/applyLoad the staged rules into pf
GET /api/firewall/alias/searchItemList aliases
POST /api/firewall/alias/reconfigureReload alias tables
GET /api/diagnostics/interface/getInterfaceStatisticsPer-interface counters
GET /api/routes/gateway/statusGateway reachability
GET /api/core/backup/download/thisDownload the running config.xml
Read-only / Safesearch automation firewall rules
$ curl -sk -u "$KEY:$SECRET" https://localhost/api/firewall/filter/searchRule | jq '.rows[0:2] | .[] | {uuid, description, action, interface, source_net, destination_net, enabled}'
{
"uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"description": "CI runners to build cache",
"action": "pass",
"interface": "lan",
"source_net": "ci_runners",
"destination_net": "10.20.0.0/24",
"enabled": "1"
}
{
"uuid": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"description": "Block guest to management",
"action": "block",
"interface": "guest",
"source_net": "guest",
"destination_net": "mgmt_net",
"enabled": "1"
}

Illustrative output

The verb conventions

The same verbs apply consistently across namespaces. The mental model:

VerbMethodPurpose
search<Object>GET or POSTList objects with optional filters and pagination
get<Object>/{uuid}GETGet one object by UUID (returns the full record)
add<Object>POSTCreate a new object (UUID is server-assigned)
set<Object>/{uuid}POSTUpdate an existing object
del<Object>/{uuid}POSTDelete an object
toggle<Object>/{uuid}/{enabled}POSTEnable or disable an object
applyPOSTLoad the staged configuration into the running service
reconfigurePOSTWrite the templates and restart the service
statusGETService status (running, stopped, etc.)

The two write halves matter more than the naming. A set or add puts the object into /conf/config.xml and returns {"result": "saved"} — at that point nothing has changed in the data plane. The apply or reconfigure call is what regenerates the configuration files and reloads the service. Automation that saves and never applies produces a firewall whose configuration and behaviour disagree, which is a genuinely unpleasant thing to debug.

A set against a UUID that does not exist fails rather than creating one; add always assigns its own UUID and ignores anything the caller supplies. Validation failures return 400 with a validations object naming each offending field, so the caller does not have to know the schema up front — the error says what was wrong.

Configuration changeadd an automation firewall rule
$ curl -sk -u "$KEY:$SECRET" -X POST -H "Content-Type: application/json" https://localhost/api/firewall/filter/addRule -d '{"rule":{"enabled":"1","action":"pass","interface":"lan","direction":"in","ipprotocol":"inet","protocol":"any","source_net":"any","destination_net":"any","description":"Lab rule"}}'
{
"result": "saved",
"uuid": "f1e2d3c4-b5a6-7890-1234-567890abcdef"
}

Illustrative output

Discovering endpoints

Three strategies in increasing depth:

  1. The official documentation. docs.opnsense.org/development/api.html publishes a generated table of endpoints per module, with the HTTP method and the parameters each takes. This is the first place to look, and it covers most of what an operator needs.

  2. The GUI’s own traffic. Every configuration page in OPNsense is a thin client over the same API. Open the page that does the thing you want to automate, open the browser’s developer tools on the network tab, and perform the action. The request the page makes — URL, method, and body — is exactly the call your script should make. This is the fastest way to learn the payload shape for an unfamiliar object, and it is always current with the running firmware.

  3. The source code. git clone https://github.com/opnsense/core and read the controllers directory. OPNsense/Firewall/Api/FilterController.php covers everything under /api/firewall/filter/*; the model beside it, OPNsense/Firewall/Filter.xml, is the authoritative list of field names and their permitted values. This is the deepest reference and the one to reach for when the docs and the observed behaviour disagree.

For most operators, strategy 1 covers the common cases and strategy 2 answers “what does the body look like” in about a minute. Strategy 3 is for the long tail and for pinning down exact field constraints.

Rate limits and concurrency

The OPNsense API does not publish a rate limit, but the PHP worker pool serving it is finite and shared with the GUI. A flurry of concurrent requests from a misbehaving script can occupy every worker and leave an operator unable to load the web interface — on the firewall they are trying to fix. Two practical rules:

  1. Sequential writes, parallel reads. The framework locks /conf/config.xml around each individual write, so two concurrent calls do not lose each other’s changes. It does not lock a whole change-set, so a script making several writes can still have another writer — or an operator in the GUI — interleave between them. Keep one change-set in flight per firewall. Reads (search, get, status) can be parallelised freely.
  2. Back off on 5xx. A 5xx or a refused connection means the request could not be served — often because a long-running apply is holding a worker. Exponential backoff (1s, 2s, 4s) is the right response. A 4xx is a client error and retrying it unchanged is pointless.

Summary

  • API URLs have the shape /api/<module>/<controller>/<command>, with any UUID or flag the command needs appended as positional path segments.
  • The commands follow a <verb><Object> convention — searchRule, addRule, setRule/{uuid}, delRule/{uuid} — plus apply or reconfigure to push the saved configuration into the running service.
  • The major modules are core, auth, firewall, interfaces, diagnostics, trust, and one module per service (unbound, wireguard, ipsec, and so on). There is no services or vpn grouping module.
  • Firewall rules an automation writes go to /api/firewall/filter/*, the automation rule set, which is separate from the interface rules an operator edits by hand.
  • Read first, then write. searchRule returns the UUIDs a setRule call needs.
  • Discovery is via the official docs, the GUI’s own network traffic in developer tools, or the open-source controller and model files.
  • The framework serialises individual writes but not change-sets, so keep one change-set in flight per firewall. Reads can be parallel. Treat a 5xx as a signal to back off, and a 4xx as a signal to fix the request.

Knowledge check · 4 questions

  1. Q1. You need the UUID of an automation firewall rule so you can update it. Which call do you make first?

  2. Q2. API endpoint names and request schemas are guaranteed stable across OPNsense firmware versions, so scripts written today will work indefinitely without testing against new firmware.

  3. Q3. A script has just posted a new rule to /api/firewall/filter/addRule and received {"result": "saved"} with a UUID. Which statements about the firewall at that moment are correct? Select all that apply.

  4. Q4. A burst of concurrent POST requests from a misbehaving script starts returning 5xx and refused connections. What is the most appropriate response?

Passing score: 75%. Answers are checked in this browser.