OPNsenseXLII · API and AutomationAPI and automation fundamentals
API endpoints overview — the taxonomy, the namespaces, and where to find what you need
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
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/firmwareis firmware;core/systemis halt/reboot/status;firewall/filteris the automation rule set;firewall/aliasis aliases;firewall/source_natis 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:
| Module | Purpose | Common controllers |
|---|---|---|
core | Firmware, system, services, backups, menu | firmware, system, service, backup, snapshots, menu |
auth | Users, groups, privileges | user, group, priv |
firewall | Automation rules, aliases, NAT, categories | filter, alias, alias_util, category, source_nat, one_to_one, npt, group |
interfaces | Interface overview and assignment | overview, settings, vlan_settings, vip_settings, lagg_settings |
diagnostics | Read-only diagnostics | firewall, interface, log, system, activity, dns, traceroute, packet_capture |
unbound, dnsmasq, kea | Per-service configuration; each service is its own module | settings, service |
wireguard, ipsec, openvpn | VPN configuration and status; again one module each | client, server, general, service |
trust | Certificates, CAs, CRLs | cert, ca, crl, settings |
routes, routing | Static routes and gateways | gateway, routes, settings |
ids | Suricata | settings, 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:
| Endpoint | Purpose |
|---|---|
GET /api/core/firmware/status | Current version, latest available |
GET /api/core/system/status | System status summary |
GET /api/interfaces/overview/interfacesInfo | Interface list and addresses |
GET /api/firewall/filter/searchRule | List firewall rules |
POST /api/firewall/filter/addRule | Add 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/apply | Load the staged rules into pf |
GET /api/firewall/alias/searchItem | List aliases |
POST /api/firewall/alias/reconfigure | Reload alias tables |
GET /api/diagnostics/interface/getInterfaceStatistics | Per-interface counters |
GET /api/routes/gateway/status | Gateway reachability |
GET /api/core/backup/download/this | Download the running config.xml |
$ 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:
| Verb | Method | Purpose |
|---|---|---|
search<Object> | GET or POST | List objects with optional filters and pagination |
get<Object>/{uuid} | GET | Get one object by UUID (returns the full record) |
add<Object> | POST | Create a new object (UUID is server-assigned) |
set<Object>/{uuid} | POST | Update an existing object |
del<Object>/{uuid} | POST | Delete an object |
toggle<Object>/{uuid}/{enabled} | POST | Enable or disable an object |
apply | POST | Load the staged configuration into the running service |
reconfigure | POST | Write the templates and restart the service |
status | GET | Service 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.
$ 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:
-
The official documentation.
docs.opnsense.org/development/api.htmlpublishes 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. -
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.
-
The source code.
git clone https://github.com/opnsense/coreand read the controllers directory.OPNsense/Firewall/Api/FilterController.phpcovers 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:
- Sequential writes, parallel reads. The framework locks
/conf/config.xmlaround 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. - Back off on 5xx. A 5xx or a refused connection means the request could not be served — often because a long-running
applyis 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}— plusapplyorreconfigureto 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 noservicesorvpngrouping 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.
searchRulereturns the UUIDs asetRulecall 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
Q1. You need the UUID of an automation firewall rule so you can update it. Which call do you make first?
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.
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.
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.