Skip to content

REST API & OpenAPI reference

Danbyte exposes its full IPAM/DCIM data model as a REST API under /api/, with an interactive, self-describing OpenAPI 3 reference built in.

Where it lives

URL What it is
/api/ Redirects to the docs below.
/api/docs/ Swagger UI - interactive reference with "try it out" against your live tenant.
/api/docs/redoc/ ReDoc - a clean, readable reference view.
/api/schema/ The raw OpenAPI 3 schema (YAML) - the machine-readable source of truth.

All four require authentication (session cookie or an API token), consistent with the rest of the API's default-closed posture.

The reference is generated by drf-spectacular from the actual viewsets, serializers, and view annotations, so it never drifts from the running code. Its assets are bundled locally (drf-spectacular-sidecar), so the interactive docs work on airgapped installs with no CDN access.

Authenticating

  • API token - create one under Settings → Preferences → API tokens, then send it as a header: Authorization: Token <key>. In Swagger UI, click Authorize and paste the token. Tokens are scoped to a tenant. Tick Read only when creating one and every write is refused with 403 no matter what the owning user may do - the right choice for reporting, inventory pulls, and AI assistants.
  • Session - when you are logged into the SPA, the same session cookie authorizes API calls from the browser.

Every request is scoped to your active tenant and enforced by RBAC - the schema describes the shape of each endpoint, but access is always checked server-side.

Grouping

Operations are grouped by domain object (Prefixes, IP addresses, Devices, VLANs, Circuits, Monitoring, …) so you can find an endpoint by the thing it acts on. The CRUD object endpoints come from the router-registered viewsets; the remaining operational endpoints (monitoring, compliance, import/export, system, settings, …) are annotated with @extend_schema so they appear with proper summaries, request bodies, and responses.

Asking the API what it accepts

These endpoints describe the API's own vocabulary so a client never has to hard-code a list that can go stale:

  • GET /api/dcim/choices/ - the option lists behind the long taxonomy dropdowns (interface and cable types, duplex, 802.1Q modes, PoE, connector fibre counts, common speeds). Each option carries value, label and a group so a UI can render sub-categorised dropdowns.
  • GET /api/editable-fields/ - which fields of a model a field-level write path may set, and what editor each needs. ?model=interface (registry slug or api.interface) returns descriptors:
{"key": "mtu",       "label": "MTU",           "kind": "int",    "nullable": true}
{"key": "type",      "label": "Type",          "kind": "choice", "choices": "interface_types"}
{"key": "status_id", "label": "Status",        "kind": "status", "status_model": "device"}
{"key": "site_id",   "label": "Site",          "kind": "object", "object_model": "site",
                                               "endpoint": "/api/sites/"}

kind names the editor to render; choices/suggestions name a list in /api/dcim/choices/. Omitting ?model= lists the covered models. The response is filtered by the caller's change permission - it describes writes, so a caller who cannot change devices does not see device at all.

The field names come from each viewset's own write allow-list (the same declaration bulk-update enforces); everything else - label, editor kind, option list, nullability - is derived from the model definition, so this endpoint cannot drift from what a write will actually accept.

  • GET /api/list-fields/?path=/api/devices/ - which fields a list's rows carry that can be shown as table columns. path is the list endpoint the table fetches - any routed list, including /api/routing/…, /api/monitoring/… and plugin lists. The answer:
{"path": "/api/devices/", "model": "api.device", "slug": "device", "cf_model": "device",
 "fields": [
   {"key": "asset_tag",   "label": "Asset tag", "kind": "text",   "group": "fields"},
   {"key": "airflow",     "label": "Airflow",   "kind": "choice", "group": "fields",
    "options": [{"value": "front-to-rear", "label": "Front to rear"}], "setting": "airflow"},
   {"key": "site.region", "label": "Region",    "kind": "object", "group": "related",
    "related": "api.region", "via": "Site"},
   {"key": "config_template", "path": "config_template.resolved", "label": "Config template",
    "kind": "object", "group": "related", "related": "api.exporttemplate"}],
 "custom_fields": [{"key": "owner", "label": "Owner", "type": "text", "choices": [], …}]}

key is a dotted path into a row (path when the value sits deeper); kind is one of text, longtext, ip, number, bool, choice, date, datetime, color, object, objects, tags or auto (render by the value's shape). A long choice list names its /api/dcim/choices/ key in choices instead of inlining options. related is the app.model an object points at. setting names the device-field switch (Settings → Device fields) that hides the field. custom_fields are the model's visible custom-field definitions.

Everything is derived from the list's own serializer: the catalog never adds data, it describes what the list already returns. Left out are ids and plumbing (id, numid, permissions, raw foreign-key ids), structures (JSON, lists), nested lists of records that have no name, get_<x>_display twins, @detail_only getters and names in the serializer's list_columns_exclude. The request is gated exactly like the list - 403 without view permission on it, 404 for a path that is not a list or a list that is not enabled.

Bulk calls

Every bulk-delete/ and bulk-update/ takes a JSON object whose ids is a non-empty list of object ids (some lists take at most 1000 per call). An id that is not one, or a body that is not an object, answers 400 with {"ids": "«nope» is not an id."} and touches nothing. Ids outside the active tenant or the caller's permissions are left out, as in a list.

More ids than a call takes answer 400 ({"ids": "At most 1000 ids per call."}) and touch nothing. Send them in consecutive calls of at most that many, as the web UI does with a big selection (see Large selections).

A value a field cannot take answers 400 too - {"non_field_errors": [...]} when no serializer named the field - never a server error.

Generating the schema offline

To export the schema to a file (for client generation, diffing, or CI):

.venv/bin/python manage.py spectacular --file schema.yaml --validate