POST vs PUT

Both of these can create a resource, which is why the comparison exists and why the usual summary — “POST creates, PUT updates” — is wrong often enough to cause real bugs. The honest distinction is about who chooses the identifier, and everything else follows: the URL you send to, whether a retry is safe, which status code comes back, and what happens when a customer hits the button twice. Try each of them against a live endpoint from the API tester as you read.

The question that actually separates them

Ask this before anything else: at the moment the request is sent, does the client already know the URL the resource will live at?

If it does not — because the server will mint a database key, a sequence number, a slug derived from the content — then the client cannot address the resource, because the resource has no address yet. All it can do is hand the data to something that does have an address, namely the collection, and ask it to take custody. That request is a POST, and the server's job includes telling the client where the result ended up.

If it does — because the client generated a UUID, or the identifier is a natural key such as an ISBN, a country code or a filename — then there is no need for the intermediary. The client can address the resource directly and state what it should contain. That request is a PUT, and whether it created something or replaced something is an implementation detail from the client's point of view. It asked for a state and got it.

So the two methods are not “create” and “update”. They are delegate the naming and declare the state at a name I already have. Once you see it that way, the rest of the differences stop looking like arbitrary protocol trivia.

POSTPUT
Sent toA collection: /v1/ordersA resource: /v1/orders/9f2c…
Identifier chosen byThe serverThe client
Meaning of the bodyData for the collection to processThe complete desired state of that resource
IdempotentNoYes
Safe to retry blindlyNot without an idempotency keyYes
Success status201 with Location201 on create, 200 or 204 on replace
Repeat the same request twiceTwo resourcesOne resource

The bottom row is the one to keep. It is not a subtlety; it is the difference between one order and two.

Creating with POST: the server names it

The collection is the resource being acted on. You are not addressing the order you are about to create, because it does not exist and has no URL; you are addressing the list and asking it to grow.

POST /v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "sku": "TEA-004", "quantity": 2 }

→ 201 Created
  Location: /v1/orders/ord_9f2c41
  Content-Type: application/json

  { "id": "ord_9f2c41", "sku": "TEA-004", "quantity": 2, "status": "pending" }

Three things in that response deserve attention.

The Location header is the whole point of the exchange. It is how the server answers the question the client could not answer itself. Omitting it — and plenty of APIs do — forces every client to dig the identifier out of the body and reconstruct the URL by string concatenation, which means every client now hard-codes your URL structure and breaks when you change it. Send the header.

The body is the created resource, not the request echoed back. It carries the fields the server filled in: the identifier, a default status, timestamps, anything normalised. A client that reads this body does not need a follow-up GET.

And the status is 201, not 200. If creation is asynchronous — queued for a worker, awaiting approval — then 202 Accepted is the honest answer, ideally with a Location pointing at a status resource the client can poll.

Note also what happens if you send that identical request again. Nothing stops it. The collection takes the data and adds another member, because that is what you asked for, and you now have two orders for the same tea. The server has done nothing wrong.

Creating with PUT: the client names it

Now suppose the client generates the identifier before it sends anything. It can address the resource directly, so there is no collection in the request at all:

PUT /v1/orders/018f3c7a-6b21-7e44-9d2e-5c1a0f8b7e33 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{ "sku": "TEA-004", "quantity": 2 }

→ 201 Created

Send it again — same URL, same body — and the server replaces the state at that address with the same state it already holds, then answers 200 or 204 because nothing new came into existence. One order, however many times the request arrives. This is what people mean when they call PUT an upsert: create if absent, replace if present, and the client does not have to know or care which happened.

That property is worth a great deal in unreliable conditions. A mobile client that queues writes while offline and flushes them when the signal returns can replay its whole queue without deduplication logic. A sync job that copies records from one system to another can be re-run from the start after a crash. A retry policy in a service mesh can fire without anyone auditing whether it is safe.

What client-chosen identifiers cost

It is not free, and the trade-offs are worth stating before you adopt it everywhere.

  • The client can pick a bad name. If identifiers are sequential integers, a client can guess or collide with somebody else's. Require an unguessable format — a UUID, ideally version 7 so the keys still sort by creation time — and validate the format server-side, rejecting anything else with 400.
  • The server no longer owns its namespace. Compact sequential primary keys, custom slug schemes and identifier formats you might want to change later all become harder once clients supply them.
  • You may need to forbid overwriting. A plain PUT to an existing resource replaces it. If “create only” is what you mean, add the precondition If-None-Match: *, which tells the server to apply the request only when nothing exists at that URL and to answer 412 Precondition Failed otherwise. That turns the upsert back into a strict create without giving up idempotency, since replaying it still yields one resource and one outcome.
  • Not everything has a natural place to live. “Send an email”, “run a report”, “refund this charge” do not describe a document at a knowable URL, and forcing them into one is worse than using POST.

Why POST cannot be idempotent

The property falls straight out of the target. Idempotence asks whether repeating a request leaves the server in the same state as sending it once — a statement about stored state, not about the responses, which is covered at length in the guide to idempotency.

For PUT, the request names one resource and supplies its entire contents. “Make the thing at this address equal this” is an assignment, and assigning the same value twice is indistinguishable from assigning it once. Nothing in the request refers to the current state, so the current state cannot influence the outcome.

For POST, the request names the collection and supplies a member to add. “Add this” is an accumulation, and accumulation is inherently sensitive to how many times it happens. There is no way to make the generic form of POST idempotent, because the instruction genuinely means something different the second time. You can bolt safety on top — the next section does exactly that — but the method itself cannot carry the guarantee, which is why RFC 9110 does not claim it.

A related trap: this is also why HTTP infrastructure treats the two differently. Proxies, load balancers, service meshes and client libraries will often retry an idempotent method automatically after a network failure and will refuse to retry POST. Nobody on your team wrote that policy, and it is correct — but it means a POST endpoint you have quietly made idempotent gets no benefit unless clients know.

The duplicate submission problem

This is where the abstraction earns its keep, because it is the failure users actually see: two orders, two charges, two support tickets, from one intention.

The mechanism is almost always the same. The client sends a POST. The server receives it, processes it fully, and starts writing the response. Something on the way back fails — a dropped connection, a proxy timeout, a browser tab closed, a user who saw no spinner and clicked again. The client has no response and therefore no information whatsoever about whether the operation happened. Its two options are to retry, risking a duplicate, or to give up, risking a lost order. Neither is acceptable, and you cannot pick correctly without more information than the protocol gives you.

Idempotency keys

The standard fix, and the one payment APIs settled on. The client generates a unique value for each logical operation and sends it in a header:

POST /v1/orders HTTP/1.1
Content-Type: application/json
Idempotency-Key: 018f3c7a-6b21-7e44-9d2e-5c1a0f8b7e33

{ "sku": "TEA-004", "quantity": 2 }

The server records the key alongside the outcome. If a request arrives with a key it has already completed, it does not execute anything — it replays the stored response. The critical detail is that the key must be generated once per intention and reused on every retry of that intention. A client that generates a fresh key inside its retry loop has implemented an expensive way of changing nothing.

Uniqueness constraints

Sometimes the data already contains something that must not repeat: an invoice number, an email address, a customer's reference for the order. Enforce it in the database and answer a second attempt with 409 Conflict, ideally naming the conflicting field and linking the existing resource. This is cheaper than a key store and works even for clients that know nothing about idempotency headers — but it only helps where such a field genuinely exists.

POST/Redirect/GET, for browsers

For form submissions rendered by a server, the old pattern is still the right one: answer a successful POST with 303 See Other and a Location pointing at the result page. The browser then issues a GET, so the URL in the address bar is safe to reload, bookmark or navigate back to. It does not protect against a double-click before the response arrives, so it complements an idempotency key rather than replacing it.

Or move the creation to PUT

If the client can generate the identifier, the whole problem dissolves. There is no key store to maintain and no retry policy to audit, because a replayed PUT is a no-op by construction. It is the cleanest answer whenever the resource is a document rather than an event, and it is the reason offline-first applications tend to use client-generated identifiers throughout.

POST is also the method for things that are not creation

PUT only ever means one thing. POST means “process this”, which is deliberately broad — it is the method the specification uses for operations that do not fit any other verb.

  • Actions on an existing resource. POST /v1/orders/ord_9f2c41/refunds, POST /v1/articles/42/publish. Sent to a resource rather than a collection, this asks that resource to do something. Notice it still tends to create a record of the action, which is why modelling the action as a sub-collection reads naturally.
  • Searches with a body. A query too long or too structured for a URL is often sent as POST /v1/search. It creates nothing and it is not cacheable, which is the real cost — see REST vs GraphQL for where that trade-off leads.
  • Non-idempotent submissions generally. Sending a message, triggering a build, recording an event. There is no document being named, so there is nothing to PUT.
  • Receiving webhooks. Providers deliver events by POST, usually with at-least-once semantics, so your receiver must expect repeats — the webhook tester and the idempotency guide both cover deduplicating them.

Two habits to avoid. Do not tunnel reads through POST for convenience: you lose caching, logging clarity and the safety guarantee that lets anything replay the request. And do not use POST for a partial update because PUT felt too destructive — the method for partial updates is PATCH, and PUT vs PATCH covers the patch document formats, concurrency control and status codes that go with it.

Choosing, by scenario

ScenarioMethodWhy
Sign-up form creating a user with a database IDPOST /v1/usersThe server owns the identifier; return it in Location
Uploading a file to a path the client already knowsPUT /v1/files/reports/q3.csvThe path is the identifier; re-uploading is harmless
Mobile app creating records while offlinePUT with a client UUIDThe queue can be flushed and re-flushed without duplicates
Taking a paymentPOST + Idempotency-KeyServer-assigned reference, but retries must never double-charge
Importing rows from a spreadsheet keyed by a natural IDPUT per rowRe-running the import converges instead of duplicating
Creating strictly, never overwritingPUT + If-None-Match: *Idempotent create; an existing resource yields 412
Refunding an orderPOST /v1/orders/{id}/refundsAn action, expressed as a sub-collection that records it
Replacing a settings document wholesalePUTThe client holds the complete desired state
Changing one field of a large recordPATCHNeither method here fits; see PUT vs PATCH

See it for yourself

The fastest way to internalise this is to watch a server report what it received. Open the API tester, send POST https://httpbin.org/post with a small JSON body, then send the identical body to https://httpbin.org/put with the PUT method. The echo service shows you the method, the headers and the parsed body, so you can confirm your client sent the Content-Type you expected and that the body survived intact rather than being re-encoded as form data.

Then do the more instructive experiment against your own API: send the same creation request twice and count the rows. If you get two, you have a POST endpoint and you now know exactly which protection it needs. From here, the HTTP methods reference puts both verbs in context with the other seven, what a REST API is covers the resource modelling that decides which collections exist in the first place, and the status code reference has every code named above.

Frequently asked questions

Can both POST and PUT create a resource?

Yes, and that is why the comparison confuses people. They create in different places. POST goes to a collection and asks it to add a member, leaving the server to mint the identifier and report it in Location. PUT goes to the resource's own URL and says “make the thing at this address look like this”, creating it if nothing is there. The difference is who decided the address.

Why is POST not idempotent when PUT is?

Because a POST targets the collection, not the new member. Every POST to /v1/orders is a fresh instruction to add an order, so ten identical ones produce ten orders. A PUT names one resource and supplies its complete state, so ten identical ones leave exactly one resource holding exactly that state.

Should the client or the server generate the ID?

Server-assigned is the safe default — the server owns its namespace and never trusts an outside identifier. Client-assigned is worth the cost when clients must create offline, when the same logical record arrives from several sources, or when you want retries to be naturally safe. If clients do choose, require an unguessable format such as a UUID.

What status code should a successful creation return?

201 Created with a Location header, and usually the new representation in the body. Use 202 when creation is queued. A PUT that replaced something existing returns 200 or 204 and no Location, because nothing new was created.

How do I stop a retried POST creating a duplicate?

Send an Idempotency-Key generated once per logical operation and reused on every retry of it; the server stores the key with the outcome and replays the stored response. Otherwise enforce a natural uniqueness constraint and answer 409 Conflict, or move the creation to PUT at a client-chosen URL.

Is it wrong to POST to a resource URL rather than a collection?

Not wrong — it means something else. POST to a specific resource asks that resource to process the enclosed data, which is how actions outside create/read/update/delete are expressed, such as POST /v1/orders/abc/refunds. What it does not mean is “create a resource at this URL”; that instruction is PUT.