HTTP 201 Created

Created

HTTP 201 is Created: the request made one or more new resources. RFC 9110 section 15.3.2 says the main one is named by the Location header or, when there’s no Location, by the request URL itself. The body usually describes or links to what was made.

The usual exchange

POST /orders HTTP/1.1
Content-Type: application/json

{"sku": "duck-07", "qty": 2}

HTTP/1.1 201 Created
Location: /orders/4182
ETag: "v1"
Content-Type: application/json

{"id": 4182, "sku": "duck-07", "qty": 2, "status": "pending"}

Location may be relative; the client resolves it against the request URL. Validators such as ETag on a 201 describe the new representation, so the client can send If-Match: "v1" with its next update.

When it applies, and when it doesn’t

  • POST that inserts. 201 with Location.
  • PUT to a URL that had nothing yet. RFC 9110 section 9.3.4 says the server must answer 201. A PUT that replaces something answers 200 or 204.
  • Work that creates something later, such as an export job: 202 Accepted with a URL to poll. 201 promises the resource exists now.
  • An update. A PATCH or PUT on an existing record changes it rather than making one: 200 or 204.

Duplicates and retries

A client whose POST timed out doesn’t know whether the row exists, and retrying blindly makes two. Two common fixes: let the client choose the id and use PUT, so a repeat finds the resource and answers 200 or 204; or accept an Idempotency-Key header, as Stripe’s API does, and replay the first response for a repeated key. A unique-constraint clash on create is 409 Conflict, not a second 201.

Caching

201 isn’t heuristically cacheable, and responses to POST are rarely stored anyway. A successful POST does make caches invalidate the stored response for the request URL, and they may invalidate the Location URL too (RFC 9111 section 4.4).

In your stack

res.status(201).location(`/orders/${order.id}`).json(order);      // Express
return order, 201, {"Location": f"/orders/{order['id']}"}        # Flask

@app.post("/orders", status_code=201)                             # FastAPI
def create_order(new: NewOrder, response: Response):
    order = save(new)
    response.headers["Location"] = f"/orders/{order.id}"
    return order

In Django, JsonResponse(data, status=201), then set response["Location"].

w.Header().Set("Location", fmt.Sprintf("/orders/%d", order.ID)) // headers first
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)

In Go, headers set after WriteHeader are silently dropped, which is the usual reason a 201 arrives without its Location.

Try it in the HTTP Status Codes

201 Created

  1. 201Created2xx Success

    The request made one or more new resources. The Location header names the main one; without it, the URL the request was sent to identifies it.

    When to use it

    A POST that inserts a row or a file, and a PUT that created something that did not exist yet.

    Common mistakes

    Using it for an update, which changes a resource rather than making one, and leaving out Location after a POST to a collection, which tells the client the collection itself is what was created.

    Not cacheable by defaultRFC 9110

Every code, by class

Codes marked unofficial come from one server’s own convention — nginx, Cloudflare, cPanel, Microsoft — and are not in the IANA registry.

Runs in your browser. Nothing you enter is sent anywhere.