Designing API PATCH vs PUT: The Semantic Distinction Customers Actually Use

Two HTTP methods that look similar on the wire and mean different things in practice. The difference matters once customers start building integrations that need partial updates.

HTTP gives APIs two methods for updating an existing resource: PUT and PATCH. They look similar on the wire and the textbook account of when to use each is short enough that most teams skim it and pick one based on aesthetic preference. The distinction does matter, but the version of the distinction that actually shapes customer integration code is not always the textbook version.

The textbook distinction

The textbook distinction is that PUT replaces the resource at the given URL with the body of the request, and PATCH applies a partial modification. The semantic implication is that PUT is idempotent (sending the same PUT twice produces the same end state as sending it once) while PATCH is not necessarily idempotent (it depends on how the patch is defined). The HTTP specification supports this reading.

In practice, almost every API that supports both PUT and PATCH on the same resource uses them in roughly this way: PUT requires the full representation of the resource and replaces it wholesale; PATCH accepts a subset of fields and updates only those. The wholesale-replace semantics of PUT mean that any field not present in the body is set to its default or removed; the partial-update semantics of PATCH mean that fields not present are left unchanged.

This is where the customer experience diverges. The wholesale-replace semantics of PUT mean that customer code has to send a complete representation every time it wants to update anything, which means customer code has to track the current state of the resource and merge changes locally before sending. The partial-update semantics of PATCH let customer code send only the fields that have changed, which means customer code can be stateless about the rest of the resource.

The customer integration pattern

Customer integration code, in practice, runs in one of two modes: stateful (where the integration keeps a local representation of the resource and modifies it locally before syncing) or stateless (where the integration just wants to change one field without knowing or caring about the others). PUT serves the first mode; PATCH serves the second.

Most B2B SaaS integrations are closer to the second mode than the first. A customer's webhook handler that wants to update an invoice's status does not want to fetch the invoice first, modify the status, and PUT the entire representation back; it just wants to send the new status. PATCH is the right tool for this pattern, and APIs that offer only PUT effectively force customers into a more expensive workflow than the use case requires.

This is also where the idempotency story becomes interesting. PUT is idempotent in the sense that sending the same body twice produces the same end state. PATCH is idempotent or not depending on the patch language: a JSON Merge Patch (RFC 7396) that sets specific fields is idempotent in the same sense; a JSON Patch (RFC 6902) that increments a counter is not. Most APIs that say they support PATCH actually support JSON Merge Patch semantics whether they cite the RFC or not, and those PATCH operations are idempotent in practice.

The format question

The major patch formats in production use are JSON Merge Patch (RFC 7396) and JSON Patch (RFC 6902), with JSON Merge Patch being far more common in B2B SaaS APIs. JSON Merge Patch lets the client send a partial JSON document where present fields are updated and absent fields are left unchanged. Null is the explicit signal to clear a field, which is a small gotcha but is consistent across the format.

JSON Patch is a sequence of operations (add, remove, replace, move, copy, test) applied in order. It is more expressive than JSON Merge Patch and can handle array-element manipulation and conditional updates, but the expressiveness is rarely needed in practice and the format is harder to read and write. Most APIs that need expressiveness beyond JSON Merge Patch ship dedicated endpoints for the specific operations instead of using JSON Patch.

A common middle ground is "PATCH with field-list semantics": the API document specifies that PATCH accepts a subset of the resource's fields, and unspecified fields are left unchanged. This is JSON Merge Patch in spirit without citing the RFC. It works for almost all use cases and the absence of the citation rarely produces customer confusion.

The handler implementation

The internal implementation of PATCH is more complicated than the external semantics suggest, because the partial-update semantics interact with validation, normalization, and database update generation.

Validation: a PATCH request can specify any subset of fields, but the fields that are specified still need to be validated against type and value constraints. The validation logic has to be either field-by-field (each field has its own validator that runs independently) or constraint-aware (the validator knows which fields are present and which are not and applies cross-field constraints accordingly). The field-by-field approach is simpler but misses constraints like "field A must be greater than field B"; the constraint-aware approach handles cross-field constraints but requires fetching the current resource state to validate against.

Normalization: the same fields can sometimes be specified in different formats (a date as either an ISO string or a Unix timestamp, an amount as either cents or dollars), and the normalization logic has to handle this for the fields that are present. The principle is the same as for full PUT, but the field-presence check has to be done explicitly because absent fields should not be normalized to their defaults.

Database update generation: the natural mapping of PATCH to database operations is "UPDATE table SET present_field_1 = new_value_1, present_field_2 = new_value_2, ... WHERE id = ?", with only present fields appearing in the SET clause. ORMs that default to "UPDATE all fields with their current values" produce a subtle bug where two concurrent PATCH requests can clobber each other's changes; the partial-UPDATE pattern avoids this if combined with optimistic concurrency control.

What customer-visible documentation needs

Customer-visible documentation for PATCH needs to state explicitly: which fields can be patched, what null means for each field (clear vs ignore), what missing fields mean (left unchanged), the idempotency semantics, and whether idempotency-key headers are honored. The default assumption that customers will infer all of this from "PATCH means partial update" is wrong; the assumption that customers will read RFC 7396 is wronger.

Documentation also needs to address the gotcha cases. Updating a nested object: does the patch replace the object wholesale or merge into it? Updating an array: does the patch replace the array or modify it element-by-element? Setting a field to null: is that a clear, an error, or a value? Most APIs answer these questions implicitly through their behavior, and customers learn the answers by trial and error. Documenting them explicitly saves the trial-and-error round trip.

The three patterns that fail

Three patterns I have seen in production APIs that consistently produce customer pain:

PUT-only with no PATCH support: forces customers to fetch-modify-send for every update. The cost is paid by every integration, on every update, for as long as the API exists. The fetch is also usually rate-limited separately from the write, which means concurrent integrations can run out of quota on the read side while having capacity on the write side.

PATCH with inconsistent null semantics: some fields treat null as clear and others as no-op, with no obvious pattern. Customer code that wants to handle the general case has to either send minimal patches (excluding any field it does not want to clear) or read the documentation for every field, neither of which scales.

PATCH that returns 200 on every request including no-op updates: makes it hard for customers to detect whether an update actually changed anything. This bites integrations that want to invalidate caches or send notifications only on real changes. The right behavior is 200 for changes, 304 (or 200 with a body indicating no change) for no-ops.

Our use across products

Across our four products (DocuMint, CronPing, FlagBit, WebhookVault), we use PATCH with JSON Merge Patch semantics throughout: send the fields you want to change, leave absent fields alone, null means clear. The decision was driven by customer integration patterns we observed in our own webhook handlers for upstream services like Stripe; PATCH with merge semantics is consistently what we wanted to send and consistently what worked. PUT support exists for endpoints where wholesale replacement is genuinely the right semantic (mostly configuration objects), but it is the minority.

The deeper observation is that the PUT-vs-PATCH choice is really a choice about which workflow you want customers to use. The choice is downstream of an assumption about whether customer integrations will be stateful or stateless about the resource they are updating. If most customers will be stateless about most resources, PATCH is the right default. If most customers will be stateful, PUT is the right default. Most B2B SaaS customers are stateless about most resources, which is why PATCH-as-default-with-PUT-where-it-fits has emerged as the modern API convention.

Read more