Home / Reference / ONTAP REST API

ONTAP REST API: the automation path

Since ONTAP 9.6 the cluster has spoken JSON over HTTPS on the management LIF: every object the CLI can touch — SVMs, volumes, snapshots, aggregates, jobs — is a REST resource under /api/. This guide shows the calls you will actually script: authentication, curl one-liners, Ansible playbooks, PowerShell, and the async job model that keeps long operations honest.

ONTAP REST API call flow from curl, Ansible, PowerShell and System Manager through the /api/ gateway to cluster resources and the job engine

What the REST API is (and what replaced)

The ONTAP REST API is the modern programmatic interface to the cluster, shipped since ONTAP 9.6. Every endpoint lives under https://<management-LIF>/api/, speaks JSON, and maps one-to-one onto the same objects the CLI manages — there is no separate "API copy" of your configuration. System Manager, BlueXP, and most NetApp tooling talk to this API under the hood, so a script you write against /api/ is doing exactly what the GUI does.

It replaces the older ZAPI XML interface. ZAPI still answers on modern releases for backward compatibility, but it is in maintenance mode: new features land as REST endpoints first (or only). New automation should target REST; see the CLI cheatsheet if you are translating CLI habits instead.

Find the contract for your exact release. ONTAP ships its own interactive API browser — Swagger UI — on the management LIF: https://<management-LIF>/docs/swagger-ui/index.html (ONTAP 9.9 and later). The full OpenAPI specification is also published in the ONTAP Automation documentation for each release. When in doubt about a field name or endpoint, read it there — the API surface grows every release (see what's new below).

Authentication: three ways in

Everything is HTTPS. ONTAP ships with a self-signed server certificate on the management LIF, so your first curl against a fresh cluster needs -k (or the client CA configured) until you install a proper certificate.

MethodSinceHow it worksBest for
Basic auth9.6Cluster or SVM admin user + password, standard HTTP Basic headerQuick tests, interactive scripts
API key9.8Long-lived generated key used as a Bearer token; scoped to a role, created per userAutomation that must run unattended
OAuth 2.0Later releasesToken exchange against an identity provider (e.g. Microsoft Entra ID); group-to-role mapping added in 9.16.1Enterprise environments with centralized identity

Creating an API key is a CLI or REST call itself — the key is shown once, so capture it at creation time:

# create a dedicated automation user with an API key (ONTAP 9.8+)
security login create -vserver cluster1 \
  -user-or-group-name svc-automation \
  -application ontapi -authmethod apikey \
  -role readonly

# output includes:  API Key: api-key-XXXX...  (shown once — save it)

# or the equivalent REST call, as the cluster admin:
curl -ks -u admin:'PASSWORD' -X POST \
  "https://10.0.0.2/api/security/authentication/keys" \
  -H "Content-Type: application/json" \
  -d '{"user": {"name": "svc-automation", "owner": {"name": "cluster1"}}, "role": {"name": "readonly"}}'
Never put a password in a script that other people can read. Basic auth in a cron job means the cluster admin password lives in a file. API keys (or OAuth tokens fetched at runtime) are the supported way to keep secrets out of playbooks; store them in your vault and inject them as variables.

curl: the read pattern that covers 90% of questions

Reads are GETs. The two query parameters you will use constantly are fields (only return the properties you want) and filter (server-side filtering, so you do not pull the whole cluster over the wire and filter client-side):

MGMT=10.0.0.2          # cluster management LIF
USER=admin
PASS='PASSWORD'

# 1. cluster identity + version
curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/cluster/version"
# {"version":{"full":"NetApp Release 9.16.1","generation":9,"major":16,"minor":1}}

# 2. all SVMs, only name + uuid
curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/svm/svms?fields=name,uuid"

# 3. volumes on SVM vs1, with real used/available space, filtered server-side
curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/storage/volumes?svm.name=vs1&fields=name,state,size,space.available&limit=50"

# 4. snapshots of one volume (needs the volume UUID from the record above)
curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/storage/volumes/0a1b2c3d-.../snapshots?fields=name,create_time,size"

# 5. every aggregate with its usable capacity, sorted
curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/storage/aggregates?fields=name,space&order_by=name"

# 6. current jobs — useful before you panic about a hung operation
curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/cluster/jobs?fields=uuid,name,state,message&state=running"

Note the ? query in call 3: svm.name=vs1 is a filter on a related object's property — ONTAP's filter syntax lets you reach into child fields with dot notation (space.available, svm.name). The response envelope is always {"records": [...], "num_records": N} for collections.

Creating things: POST, and the async job model

Writes are POST/PATCH/DELETE. Short operations complete inline (HTTP 200/201) but anything that moves data or takes time — volume creation, resizes, deletions, SnapMirror updates — is a job: ONTAP answers 202 Accepted with a Location header pointing at the job, and you poll until it reaches success or failure.

# create a 2 TB rw volume on aggregate aggr1 of SVM vs1
curl -ks -u "$USER:$PASS" -X POST \
  "https://$MGMT/api/storage/volumes?return_records=false" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "web1",
    "svm": {"name": "vs1"},
    "aggregates": [{"name": "aggr1"}],
    "size": "2t",
    "type": "rw",
    "guarantee": {"type": "volume"},
    "snapshot_policy": {"name": "default"}
  }'
# → HTTP 202, Location: /api/cluster/jobs/1a2b3c4d-...
# volume is NOT necessarily ready yet — poll the job:

curl -ks -u "$USER:$PASS" \
  "https://$MGMT/api/cluster/jobs/1a2b3c4d-..."
# {"uuid":"1a2b3c4d-...","name":"vol_create","state":"running",
#  "message":"job is running","elapsed_time":4}

# a small script-friendly poll loop (3 s interval, 60 s budget):
for i in $(seq 1 20); do
  s=$(curl -ks -u "$USER:$PASS" "https://$MGMT/api/cluster/jobs/1a2b3c4d-..." \
      | sed -n 's/.*"state":"\([a-z]*\)".*/\1/p')
  [ "$s" = success ] && echo CREATED && break
  [ "$s" = failure ] && echo FAILED && exit 1
  sleep 3
done

You can force the inline behavior with return_timeout=0 — the call then returns immediately with the job reference instead of blocking. The default is to wait (up to a timeout), which is why a slow volume create curl can appear to hang; it is not hanging, it is waiting on the job. For scripting, prefer explicit polling: it is interruptible and gives you a real error message (message + code) on failure.

Modifying and deleting

# resize (grow) volume web1 to 4 TB — PATCH, target by UUID
curl -ks -u "$USER:$PASS" -X PATCH \
  "https://$MGMT/api/storage/volumes/0a1b2c3d-..." \
  -H "Content-Type: application/json" \
  -d '{"size": "4t"}'

# take a snapshot of web1 — POST to the volume's snapshots collection
curl -ks -u "$USER:$PASS" -X POST \
  "https://$MGMT/api/storage/volumes/0a1b2c3d-.../snapshots" \
  -H "Content-Type: application/json" \
  -d '{"name": "before-deploy"}'

# delete a snapshot
curl -ks -u "$USER:$PASS" -X DELETE \
  "https://$MGMT/api/storage/volumes/0a1b2c3d-.../snapshots/before-deploy"

# delete the volume (async — poll the returned job)
curl -ks -u "$USER:$PASS" -X DELETE \
  "https://$MGMT/api/storage/volumes/0a1b2c3d-..."
Grab the UUID once, reuse it. Collection GETs return uuid in every record when you ask for fields=uuid. Most object endpoints (volumes, SVMs, aggregates) address by UUID, not name, and the same UUID stays valid for the object's lifetime — cache it instead of re-resolving by name on every call.

Ansible: the idempotent path

NetApp's Ansible collection (netapp.ontap) wraps the REST API in modules that are idempotent by design — running a playbook twice does not create two volumes. The module names are na_ontap_*; for REST-native operation use na_ontap_rest_info for reads and the resource modules (na_ontap_volume, na_ontap_snapshot, ...) for writes. Each module needs hostname, username, password (or api_key), and https: true.

# playbook: ensure web1 exists at 4 TB with a nightly snapshot
- name: Manage web volume
  hosts: localhost
  gather_facts: false
  vars:
    mgmt: 10.0.0.2
    user: admin
    api_key: "{{ lookup('env', 'ONTAP_API_KEY') }}"
  tasks:
    - name: Query existing volumes
      netapp.ontap.na_ontap_rest_info:
        hostname: "{{ mgmt }}"
        username: "{{ user }}"
        api_key: "{{ api_key }}"
        https: true
        validate_certs: false
        api: storage/volumes
        params:
          fields: name,size,state
      register: vols

    - name: Create web1 if absent
      netapp.ontap.na_ontap_volume:
        hostname: "{{ mgmt }}"
        username: "{{ user }}"
        api_key: "{{ api_key }}"
        https: true
        validate_certs: false
        state: present
        name: web1
        vserver: vs1
        aggregate_name: aggr1
        size: 4
        size_unit: tb

    - name: Ensure snapshot policy allows the nightly snap
      netapp.ontap.na_ontap_snapshot_policy:
        hostname: "{{ mgmt }}"
        username: "{{ user }}"
        api_key: "{{ api_key }}"
        https: true
        validate_certs: false
        state: present
        name: nightly
        schedule:
          - schedule: daily
            count: 7
Note: validate_certs: false is the same story as curl's -k — fine against a lab with the default self-signed certificate, wrong for production. Install a CA-signed certificate on the management LIF (or use a private CA the playbook trusts) and keep validation on.

PowerShell and Python for Windows/Linux automation

The same calls map directly onto Invoke-RestMethod and Python's requests. PowerShell's automatic JSON conversion makes the response objects immediately usable:

# PowerShell — list volumes on vs1 with their used space
$h = @{ "Authorization" = "Bearer $env:ONTAP_API_KEY" }
$r = Invoke-RestMethod -Method Get -Uri `
  "https://10.0.0.2/api/storage/volumes?svm.name=vs1&fields=name,size,space.available" `
  -Headers $h -SkipCertificateCheck
$r.records | Select-Object name, size, @{n='avail';e={$_.space.available}}
# name  size        avail
# ----  ----        -----
# web1  2199023255552  1744830464000

# PowerShell — create a snapshot
$body = @{ name = "before-patch-tuesday" } | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri `
  "https://10.0.0.2/api/storage/volumes/0a1b2c3d-.../snapshots" `
  -Headers $h -ContentType "application/json" -Body $body -SkipCertificateCheck
# Python (requests) — poll a job to completion with a clean timeout
import requests, time

BASE = "https://10.0.0.2/api"
KEY = "api-key-XXXX..."          # or basic auth: auth=("admin", password)
HDRS = {"Authorization": f"Bearer {KEY}"}

r = requests.post(f"{BASE}/storage/volumes", headers=HDRS,
                  json={"name": "web2", "svm": {"name": "vs1"},
                        "aggregates": [{"name": "aggr1"}],
                        "size": "1t"},
                  params={"return_timeout": 0}, verify=False)
job = r.headers["Location"].rsplit("/", 1)[-1]
for _ in range(30):               # up to ~5 minutes
    j = requests.get(f"{BASE}/cluster/jobs/{job}", headers=HDRS,
                     verify=False).json()
    if j["state"] in ("success", "failure"):
        print(j["state"], j.get("message", ""))
        break
    time.sleep(10)

Keep verify=False out of production scripts — it is a lab/testing convenience for ONTAP's default self-signed certificate. Use the CA bundle or the installed cluster certificate instead.

What is new in 9.16.1 (and the ASA r2 trap)

ONTAP 9.16.1 added more than two dozen REST calls, notably: Microsoft Entra ID group and role mappings (map AD groups straight onto ONTAP roles), WebAuthn administration, ARP package management (Autonomous Ransomware Protection policies and packages), optional qtree performance metrics, and S3 bucket snapshots — snapshot an S3 bucket through the API exactly as you would a FlexVol.

ASA r2 speaks a different API. NetApp explicitly warns that ASA r2 exposes a different REST API from AFF, FAS, and classic ASA systems — the object model is not identical (volume/LUN workflows written for AFF may address different resources on ASA r2). Automate defensively: negotiate against the cluster version and platform type at runtime, and test on the target platform before rolling out. This is also why this guide tells you to read the Swagger UI for your release instead of trusting a blog screenshot.

General rule for automation that outlives a single release: check /api/cluster/version (or the version field on any collection response) at startup, gate feature calls on major/minor, and never assume endpoint parity between ONTAP-based products.

Error handling and the envelope

Errors are JSON, not HTML. The shape is consistent, so you can parse it in one place:

{"error": {"message": "Volume \"web1\" with uuid \"...\" already exists",
           "code": 131076, "target": "name"}}

HTTP status codes do the coarse work: 200/201 success, 202 accepted-as-job, 400 bad request (your JSON or filter is wrong), 401 authentication, 403 authorization (role lacks the privilege), 404 wrong UUID/path, 409 conflict (e.g. object exists or is busy), 422 validation, 429 rate-limited, 5xx cluster-side. Retry 429/503 with exponential backoff; do not retry 400/401/403/404 without changing something.

Pagination bites everyone once. Collections default to a small page (limit, often 20–50). If your script processes "all volumes" and you forget to page, you silently process a fraction. Loop on next (the response carries a _links.next href when more records exist) or use limit=0 semantics where supported — better: set limit explicitly and page by offset.

CLI → REST quick map

You used to typeREST equivalent
volume show -vserver vs1GET /api/storage/volumes?svm.name=vs1
volume create -vserver vs1 -volume web1 -aggregate aggr1 -size 2tPOST /api/storage/volumes (JSON body)
volume modify -volume web1 -size 4tPATCH /api/storage/volumes/{uuid}
volume delete -volume web1DELETE /api/storage/volumes/{uuid}
snapshot create -volume web1 -snapshot before-deployPOST /api/storage/volumes/{uuid}/snapshots
vserver showGET /api/svm/svms
job show -id 123GET /api/cluster/jobs/{uuid}
aggr showGET /api/storage/aggregates
network interface showGET /api/network/interfaces

Checklist for production automation

Further reading on this site