Skip to content
Log in Sign up My account

6.5 REST API Response Codes and Payloads

Blueprint 6.5: Interpret REST API response codes and results in payload using Cisco Catalyst Center and RESTCONF

  • HTTP request and response structure: method, URI, headers, body, status line
  • Status code classes and the specific codes the exam uses: 200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 415, 429, 500, 503
  • RESTCONF: URI structure, media types, method semantics, success codes, and the errors body
  • Catalyst Center: the response wrapper, task-based 202 replies, and authentication errors
  • Turning a status code plus a payload into a diagnosis

The verb is interpret. You will be shown a request and its response, or a script that branches on status_code, and asked what happened or what the script prints. Learn the codes as pairs of "what the client did" and "what the server said".

Anatomy of a REST exchange

PATCH /restconf/data/ietf-interfaces:interfaces/interface=Loopback100 HTTP/1.1   <- method, URI, version
Host: 192.168.100.1
Authorization: Basic YWRtaW46Q2lzY28xMjMh                                    <- credentials
Content-Type: application/yang-data+json                                      <- format of the body sent
Accept: application/yang-data+json                                            <- format wanted back

{"ietf-interfaces:interface": {"name": "Loopback100", "description": "core"}}  <- body (payload)

HTTP/1.1 204 No Content                                                       <- status line
Date: Wed, 07 Oct 2026 10:15:02 GMT
Part Request Response
First line Method and URI Status code and reason phrase
Headers Authorization, X-Auth-Token, Content-Type, Accept Content-Type, Location (after a create), Set-Cookie
Body Data to create or change (POST, PUT, PATCH) Data returned (GET) or an error description

Content-Type describes the body being sent. Accept asks for a format coming back. Getting either wrong is a classic cause of a 4xx response.

Methods and CRUD

CRUD HTTP method RESTCONF meaning NETCONF equivalent
Create POST Create a child resource; error if it already exists <edit-config> operation create
Read GET Retrieve the resource <get> / <get-config>
Update (replace) PUT Create the resource, or replace it entirely <edit-config> operation replace
Update (merge) PATCH Merge the supplied fields into the existing resource <edit-config> operation merge
Delete DELETE Remove the resource <edit-config> operation delete

Exam trap

PUT replaces. If you PUT an interface body that contains only name and description, any leaf you left out (an IP address, for instance) is removed, because the new resource is exactly what you sent. PATCH would have changed only the description. Questions that ask why an address "disappeared" after an API change are testing this.

Status codes

Code Name Meaning Typical cause on Catalyst Center or RESTCONF
200 OK Success, body contains a result GET returned data; Catalyst Center token request succeeded
201 Created A new resource was created RESTCONF POST, or PUT that created a new resource; Location header names it
202 Accepted Request accepted, processing continues in the background Catalyst Center asynchronous call; body contains a taskId
204 No Content Success, no body RESTCONF PATCH, PUT that replaced an existing resource, DELETE
3xx Redirection Resource is elsewhere Rare in these APIs
400 Bad Request Request is malformed or invalid Bad JSON syntax, a value that fails YANG validation, unknown leaf
401 Unauthorized Not authenticated Missing or wrong password, missing, expired or invalid token
403 Forbidden Authenticated, but not allowed User role lacks permission, such as a read-only role trying a POST
404 Not Found URI does not name an existing resource Typo in the path, wrong module name, list key that does not exist
405 Method Not Allowed Resource does not support that method POST to a resource that only supports GET
409 Conflict Request conflicts with current state RESTCONF POST of a resource that already exists (data-exists), datastore locked
415 Unsupported Media Type Server cannot process the body's Content-Type Wrong or missing Content-Type header
429 Too Many Requests Rate limit hit Catalyst Center rate limiting a busy script
500 Internal Server Error Server failed Bug or back-end failure
503 Service Unavailable Server temporarily unable to serve Service starting up or overloaded

The first digit tells you whose problem it is:

graph LR
  S["Status code"] --> A["2xx success<br/>check the body or task"]
  S --> B["3xx redirect<br/>follow Location"]
  S --> C["4xx client error<br/>fix the request"]
  S --> D["5xx server error<br/>retry later or escalate"]
  C --> C1["401: who are you"]
  C --> C2["403: you may not"]
  C --> C3["404: no such resource"]
  C --> C4["400: request is wrong"]

Exam trap

401 versus 403 is the most tested pair. 401 Unauthorized really means unauthenticated: the server does not know who you are, so fix the credentials or get a new token. 403 Forbidden means the server knows exactly who you are and refuses: fix the role or permissions. Sending the same request with a fresh token will not cure a 403.

RESTCONF in detail

URI structure

graph LR
  A["https://192.168.100.1"] --> B["/restconf"]
  B --> C["/data<br/>datastore resource"]
  B --> O["/operations<br/>RPCs"]
  C --> D["/ietf-interfaces:interfaces<br/>module:top-level node"]
  D --> E["/interface=GigabitEthernet1<br/>list=key"]
  E --> F["/description<br/>leaf"]
Element Example Rule
Root /restconf Discoverable at /.well-known/host-meta
Datastore /restconf/data Configuration and state
Operations /restconf/operations YANG RPCs
First node ietf-interfaces:interfaces Prefixed with the module name
List entry interface=GigabitEthernet1 list=key; several keys are comma-separated
Special characters in keys interface=GigabitEthernet0%2F0%2F0 A / inside a key must be percent-encoded as %2F
Query parameters ?depth=2, ?content=config, ?fields=name Defined in RFC 8040; support varies by release

Media types

Header value Meaning
application/yang-data+json YANG data encoded as JSON (RFC 7951)
application/yang-data+xml YANG data encoded as XML

Send Accept on reads and Content-Type on writes. Without an Accept header, IOS XE replies in XML.

Method semantics and success codes (RFC 8040)

Method Target URI Body Success Common failure
GET The resource None 200 with data 404 if it does not exist
POST The parent resource The new child 201 with Location 409 if the child already exists
PUT The resource itself The complete resource 201 if created, 204 if replaced 400 if the key in the URI and body differ
PATCH The resource itself Fields to merge 204 (or 200 if a body is returned) A 4xx error, typically 404, if the target does not exist
DELETE The resource itself None 204 A 4xx error, typically 404, if it does not exist

Exam trap

POST goes to the parent URI and names the new child in the body. PUT and PATCH go to the URI of the resource itself. A POST to .../interfaces/interface=Loopback100 asks the server to create something inside Loopback100, which is not what was intended.

Reading a RESTCONF error body

When a RESTCONF request fails, the body explains why in a structured errors object (RFC 8040 section 7.1). IOS XE returns a body shaped like this one, though some releases omit the ietf-restconf: prefix:

{
  "ietf-restconf:errors": {
    "error": [
      {
        "error-type": "application",
        "error-tag": "data-exists",
        "error-path": "/ietf-interfaces:interfaces/interface[name='Loopback100']",
        "error-message": "object already exists"
      }
    ]
  }
}
Field Tells you
error-type Layer of the failure: transport, rpc, protocol or application
error-tag Machine-readable reason, such as data-exists, invalid-value, malformed-message, access-denied, in-use
error-path Which node caused it
error-message Human-readable explanation

data-exists goes with 409. malformed-message and most invalid-value errors go with 400. access-denied goes with 401 or 403.

Catalyst Center payloads

Every Intent API body wraps its result in response:

{
  "response": [
    {"hostname": "dist-sw1", "managementIpAddress": "10.10.20.80",
     "reachabilityStatus": "Reachable", "softwareVersion": "17.12.4"},
    {"hostname": "acc-sw7", "managementIpAddress": "10.10.20.87",
     "reachabilityStatus": "Unreachable", "softwareVersion": "17.9.5"}
  ],
  "version": "1.0"
}
{
  "response": {
    "taskId": "0c2a9f6e-1d4b-4a77-b1c3-5e9d8f7a6b21",
    "url": "/api/v1/task/0c2a9f6e-1d4b-4a77-b1c3-5e9d8f7a6b21"
  },
  "version": "1.0"
}
{
  "response": {
    "id": "0c2a9f6e-1d4b-4a77-b1c3-5e9d8f7a6b21",
    "isError": true,
    "progress": "Template deployment failed",
    "failureReason": "Device acc-sw7 is unreachable",
    "startTime": 1791367500000,
    "endTime": 1791367530000
  },
  "version": "1.0"
}

The third tab is a trimmed task record. Note the two levels of success: the HTTP code is 200 because the task query worked, while isError: true says the deployment failed. You need both to interpret the result.

Times in Catalyst Center payloads are usually epoch milliseconds, so endTime - startTime above is 30,000 ms, or 30 seconds.

Exam trap

A 200 on a task query does not mean the task succeeded. Read isError and failureReason in the payload. Equally, a 202 on the original POST says nothing about the outcome yet.

Interpreting a script that checks responses

import json


def report(status, body_text):
    """Explain a Catalyst Center or RESTCONF reply the way an engineer would."""
    if status in (200, 201):
        body = json.loads(body_text) if body_text else {}
        if "response" in body and isinstance(body["response"], list):
            return f"OK, {len(body['response'])} item(s)"
        return "OK"
    if status == 202:
        task = json.loads(body_text)["response"]["taskId"]
        return f"accepted, poll task {task}"
    if status == 204:
        return "done, no content"
    if status == 401:
        return "authentication failed: refresh the token or fix credentials"
    if status == 403:
        return "authorised user, but the role does not allow this"
    if status == 409:
        errs = json.loads(body_text)["ietf-restconf:errors"]["error"]
        return "conflict: " + errs[0]["error-tag"]
    if 400 <= status < 500:
        return f"client error {status}: fix the request"
    return f"server error {status}: retry later"


samples = [
    (200, '{"response": [{"hostname": "dist-sw1"}, {"hostname": "acc-sw7"}], "version": "1.0"}'),
    (202, '{"response": {"taskId": "0c2a9f6e", "url": "/api/v1/task/0c2a9f6e"}, "version": "1.0"}'),
    (204, ""),
    (409, '{"ietf-restconf:errors": {"error": [{"error-type": "application", "error-tag": "data-exists"}]}}'),
    (403, ""),
    (404, ""),
    (503, ""),
]
for code, text in samples:
    print(code, report(code, text))

Output:

200 OK, 2 item(s)
202 accepted, poll task 0c2a9f6e
204 done, no content
409 conflict: data-exists
403 authorised user, but the role does not allow this
404 client error 404: fix the request
503 server error 503: retry later

Note the order of the if tests. The 401, 403 and 409 checks come before the generic 400 <= status < 500 test, so they produce specific messages. If the generic test came first, every 4xx would print "fix the request". The body for 204 is empty, which is why that branch never calls json.loads; calling it on an empty string raises JSONDecodeError.

Lab: provoke every status code

Topology: AUTO (Ubuntu, 192.168.100.50) to R1 (C8000v, Gi1 192.168.100.1/24) with RESTCONF enabled as in the Python and JSON lab. Use the lb100.json file you wrote there.

graph LR
  AUTO["AUTO<br/>curl and Python"] -- "HTTPS 443" --- R1["R1 C8000v<br/>restconf"]

Define a shell shortcut on AUTO so every command prints its status code on the last line:

R="curl -sk -u admin:Cisco123! -H Accept:application/yang-data+json -H Content-Type:application/yang-data+json -w \n%{http_code}\n"
U=https://192.168.100.1/restconf/data/ietf-interfaces:interfaces
Step Command Expected code Why
1 $R $U/interface=GigabitEthernet1 200 Resource exists, data returned
2 $R -X POST -d @lb100.json $U 201 New child created under the parent
3 Repeat step 2 409 Loopback100 already exists (data-exists)
4 $R -X PATCH -d '{"ietf-interfaces:interface":{"name":"Loopback100","description":"patched"}}' $U/interface=Loopback100 204 Merge succeeded, no body
5 $R $U/interface=Loopback999 404 No such list entry
6 Step 4 with the comma after "Loopback100" removed 400 Malformed JSON
7 Step 1 with the password changed to wrong 401 Authentication failed
8 $R -X PUT -d '{"ietf-interfaces:interface":{"name":"Loopback100","type":"iana-if-type:softwareLoopback"}}' $U/interface=Loopback100 204 Existing resource replaced
9 $R -X DELETE $U/interface=Loopback100 204 Deleted

After step 8, verify on R1 that PUT really replaced the resource:

R1# show running-config interface Loopback100
interface Loopback100
 no ip address
end

The description and the IP address from earlier steps are gone, because the PUT body did not include them. After step 9, show ip interface brief | include Loopback100 returns nothing.

Finally, save the report() script above as report.py, run it with python3 report.py, and confirm the output matches. Then add a branch for 404 that prints "check the URI and the list key", and place it so that it actually runs.

Quick check

A RESTCONF POST returns 409 with error-tag data-exists. What should the script do if the goal is to update the existing resource?

Use PATCH (merge) or PUT (replace) to the resource's own URI instead of POST to the parent.

A Catalyst Center call that worked an hour ago now returns 401 with the same script. What is the most likely cause?

The token expired. Request a new token from /dna/system/api/v1/auth/token and retry.

A GET to .../interface=GigabitEthernet0/0/0 returns 404, but the interface exists. Why?

The / characters in the key must be percent-encoded: interface=GigabitEthernet0%2F0%2F0. Unencoded, the server reads them as path separators.

References