HTTP 428 Precondition Required: What It Means
HTTP 428 means an API requires a conditional request. Learn how to use ETags, fix 428 versus 412, and prevent lost updates.

HTTP 428 Precondition Required means the server will not perform your request until you include a required condition. Most APIs use this status to demand an optimistic-concurrency validator such as an If-Match ETag. The server is saying, “prove which version you read before you change it.”
A missing condition is the key distinction: if you send a condition and it is false or stale, the usual response is 412 Precondition Failed. A 409 Conflict is an application-level conflict and has a different meaning. The practical fix for 428 is to fetch the current resource, read its validator, and repeat the state-changing request with the header required by that API.
1. What HTTP 428 means
HTTP 428 is a client-error status defined by RFC 6585. The MDN reference describes it as a response indicating that the server requires the request to be conditional. A conditional request includes a header whose value is checked against the server’s current representation.
Servers commonly require a condition on PUT, PATCH, or DELETE because an unconditional write can overwrite another user’s update. The validator records the version you fetched. If the representation changed in the meantime, the server can reject your write instead of silently losing data.
| Status | What happened | Typical action |
|---|---|---|
428 Precondition Required |
The server requires a conditional header, but you did not supply one. | Read the API contract, obtain a validator, and retry with the required header. |
412 Precondition Failed |
You supplied a condition, but it evaluated false (for example, a stale ETag). | Fetch the latest representation, reconcile changes, then retry with the new validator. |
409 Conflict |
The operation conflicts with application rules or resource state. | Handle the domain conflict described by that API; do not substitute 428 logic automatically. |
2. How conditional requests prevent lost updates
Imagine two clients read document version "v7". Client A updates it first. Client B then sends an unconditional PUT; if accepted, B can erase A’s changes. With optimistic concurrency, B sends If-Match: "v7". The server has since moved to "v8", so it returns 412 and preserves A’s update.

Strong ETags are the normal choice for If-Match. The comparison must match the current representation exactly; a weak tag such as W/"v7" is not suitable for an If-Match update. The HTTP specification’s conditional-request rules are summarized by MDN’s If-Match documentation.
Date validators are another option. If-Unmodified-Since expresses that the resource must not have changed after a supplied HTTP date. If it has changed, the server normally returns 412. Date precision, clock skew, and intermediary behavior make ETags preferable when an API provides both.
3. The reliable 428 recovery sequence
- Inspect the response. Record the status, response body, and any documentation or headers naming the required precondition.
- Fetch the current representation. Use
GETand capture itsETagorLast-Modifiedvalue. - Build the conditional write. For an ETag contract, send
If-Match. For a date contract, sendIf-Unmodified-Since. - Retry once with the validator. Keep the method, URL, body, authentication, and content type unchanged.
- Handle 412 separately. A 412 means your validator is stale or otherwise false. Refetch, merge or ask for a conflict decision, and only then issue another write.
- Make retries safe. Use an idempotency key when the API supports one, and never blindly replay a non-idempotent operation after an unknown network failure.
Raw HTTP example
GET /docs/my-document HTTP/1.1
Host: example.com
Authorization: Bearer TOKEN
HTTP/1.1 200 OK
ETag: "current-etag"
Content-Type: application/json
{"title":"Original title"}
PUT /docs/my-document HTTP/1.1
Host: example.com
Authorization: Bearer TOKEN
Content-Type: application/json
If-Match: "current-etag"
{"title":"Updated title"}
If the first PUT receives 428, add the missing conditional header. If it receives 412, do not reuse the same ETag; it no longer describes the current resource.
4. Complete client examples
cURL
#!/usr/bin/env bash
set -euo pipefail
base='https://api.example.com/docs/my-document'
token='YOUR_TOKEN'
etag=$(curl --fail-with-body -sS -D - "$base" \
-H "Authorization: Bearer $token" \
-o document.json \
| awk 'BEGIN{IGNORECASE=1} /^ETag:/{gsub("\\r", "", $2); print $2}')
curl --fail-with-body -sS -X PUT "$base" \
-H "Authorization: Bearer $token" \
-H 'Content-Type: application/json' \
-H "If-Match: $etag" \
--data '{"title":"Updated title"}'
Python
import requests
url = "https://api.example.com/docs/my-document"
headers = {"Authorization": "Bearer YOUR_TOKEN"}
current = requests.get(url, headers=headers, timeout=30)
current.raise_for_status()
etag = current.headers.get("ETag")
if not etag:
raise RuntimeError("The API did not return an ETag; check its conditional-write contract")
write_headers = {
**headers,
"Content-Type": "application/json",
"If-Match": etag,
}
updated = requests.put(
url,
headers=write_headers,
json={"title": "Updated title"},
timeout=30,
)
if updated.status_code == 412:
raise RuntimeError("The document changed; refetch and reconcile before retrying")
updated.raise_for_status()
print(updated.json())
Node.js
const url = 'https://api.example.com/docs/my-document';
const auth = { Authorization: 'Bearer YOUR_TOKEN' };
const current = await fetch(url, { headers: auth });
if (!current.ok) throw new Error(`GET failed: ${current.status}`);
const etag = current.headers.get('etag');
if (!etag) throw new Error('The API did not return an ETag');
const updated = await fetch(url, {
method: 'PUT',
headers: {
...auth,
'Content-Type': 'application/json',
'If-Match': etag,
},
body: JSON.stringify({ title: 'Updated title' }),
});
if (updated.status === 412) {
throw new Error('Stale ETag: refetch and reconcile the document');
}
if (!updated.ok) throw new Error(`PUT failed: ${updated.status}`);
console.log(await updated.json());
Use the exact header spelling, quoting, and validator format shown by the API. Do not trim an ETag’s quotes unless the API explicitly documents an unquoted format.
5. Choosing the right conditional header
| Header | Use it for | Important behavior |
|---|---|---|
If-Match |
Protecting an update or delete against overwriting a known version. | Uses strong ETag comparison; a mismatch generally returns 412. |
If-None-Match |
“Only do this if the representation does not match.” Useful for cache validation and create-if-absent patterns. | Behavior depends on method; follow the API contract. |
If-Unmodified-Since |
Protecting a write with a date validator. | Subject to HTTP-date precision and clock issues; failure generally returns 412. |
If-Modified-Since |
Conditional retrieval when a cached copy may still be current. | Usually applies to GET or HEAD and can produce 304. |
If-Range |
Combining range downloads with a validator. | Relevant to partial responses, not ordinary JSON updates. |
MDN’s conditional-header family covers these headers. The API’s own contract wins when it narrows the allowed methods, validators, or wildcard behavior.
6. Troubleshooting HTTP 428
| Symptom | Likely cause | Fix |
|---|---|---|
| 428 on every PUT | The API requires If-Match or another condition. |
Perform GET first, capture the validator, and include it on the write. |
| 428 even though you sent a header | Wrong header name, wrong method, missing quotes, or a proxy removed it. | Inspect the outbound request with verbose logging; compare it with the API documentation. |
| 412 after adding If-Match | The resource changed between GET and PUT, or you used a cached ETag. | Refetch with cache controls if appropriate, merge changes, and retry with the new tag. |
| ETag is absent | The endpoint may use dates, a custom version field, or a separate metadata endpoint. | Read the API contract; do not invent an ETag. |
| Works locally, fails through a gateway | The gateway strips conditional headers or serves stale cached GET responses. | Allow the header through, bypass caching for the validator fetch, and log request/response headers safely. |
| Intermittent 428 in a worker | Concurrent workers read and write the same resource. | Serialize updates per resource, use a compare-and-swap loop, or adopt the API’s conflict-resolution endpoint. |
| Retries create duplicate side effects | A non-idempotent operation was replayed after an uncertain response. | Use an idempotency key and retry policy documented by the service. |
7. Production patterns and edge cases
Cache freshness
A cached GET can return an old representation and validator. For a write-critical read, request revalidation according to the service’s guidance (for example, appropriate Cache-Control directives) and verify that the ETag belongs to the body you are editing.

Parallel edits
A simple loop of GET, PUT, GET, PUT can still lose intent if you overwrite fields you did not mean to change. Prefer PATCH with a narrowly scoped document, or perform a three-way merge between the fetched version, your local base, and the latest server version.
Deleted resources
If a resource disappears after your GET, the subsequent conditional write may return 404, 410, or an API-specific conflict. Treat that as a domain decision rather than converting it into another 428 retry.
Creation and “must not exist” rules
Some APIs use If-None-Match: * to express create-if-absent. Others expose a dedicated idempotency or conditional-create mechanism. Confirm support before sending a wildcard.
Security and observability
ETags can reveal version or timing information, so avoid logging sensitive response bodies and authorization headers. Log status, method, resource identifier, validator age, and retry count. Redact tokens and personal data.
8. Performance, reliability, and cost
The extra GET required by a 428 contract adds a round trip and bandwidth. Keep connections alive, use regional endpoints when available, and avoid fetching large representations when the API offers a lightweight metadata or HEAD endpoint that still returns the validator. Do not cache validators longer than the resource’s consistency requirements.
For reliability, distinguish errors that are safe to retry (428 after adding the required condition, or a transient transport failure before the server accepted the write) from errors requiring reconciliation (412, validation errors, and unknown outcomes for non-idempotent calls). Apply exponential backoff to transient 5xx and rate-limit responses, but do not back off and blindly repeat a stale 412.
HTTP 428 itself has no special monetary cost. Your cost comes from API calls, transfer, compute, and any provider billing policy. A conditional GET plus a conditional write may cost two requests instead of one, so measure the endpoint’s pricing and consider metadata endpoints or batching where supported.
9. Or skip the browser setup
If you are diagnosing a 428 from a web application and need clean reference screenshots of the response page, ScreenshotNeo can capture a URL with one request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Each response reports the result through X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options. This is a runnable one-call capture:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Free accounts include 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
10. FAQ
Is 428 a server error?
No. It is a 4xx client-error response: the request does not satisfy a condition the server requires.
Can I fix 428 by adding If-Match: *?
Only if the API explicitly documents wildcard support and the semantics you need. Otherwise, obtain the actual validator.
Should a client automatically retry 428?
It may retry after obtaining the required condition, but it should cap attempts and stop for unclear contracts. A 412 requires reconciliation, not an unchanged retry.
Does Last-Modified replace ETag?
It can when the API specifies date-based conditions, but ETags usually provide finer-grained version checks. Use the validator the endpoint documents.
Why did the API choose 428 instead of 400?
428 communicates a standardized conditional-request requirement, allowing clients and intermediaries to distinguish a missing concurrency guard from malformed input.
Where did status 428 originate?
It was specified in RFC 6585, published by the Internet Engineering Task Force in April 2012.