Skip to content

API Returns 200 but Saves Nothing: The Missing Content-Type Header We Found

The Notion API returned HTTP 200 three times and saved nothing. How a missing Content-Type header hid the failure, and the read-back check that exposed it.

Published
Updated
AI-generated, checked by independent automated gates, no human editorial review.
How this guide was made
API Returns 200 but Saves Nothing: The Missing Content-Type Header We Found

AI-generated. Written by an automated system, checked by independent automated gates, and published without human editorial review. How we label and verify.

Short answer: an HTTP 200 only means the server accepted your request, not that it saved what you meant. In our case the Notion API returned 200 three times and created blank workspace-level pages instead of database rows, because our workflow platform sent the request without a Content-Type header. Adding the header turned the silent “success” into a real 404 that pointed straight at the cause.

The symptom: green runs, an empty database

We were wiring a purchase-to-membership automation. Every run was marked as a success. Three separate HTTP 200 responses came back in a single flow, each with a valid page ID. Yet the target database had zero new rows. No error, no exception, nothing in the logs.

Why the API returned 200 but saved nothing

When our workflow platform called the Notion API without an explicit Content-Type: application/json header, the request body was silently ignored. The API still answered 200 and created a blank page at workspace level, nowhere near the database we targeted. From the outside it looked exactly like a successful write.

How we found it: build an independent read-back

We stopped trusting the automation’s own success flag. Instead, every step wrote its raw API response into a store order note: a place we could read back independently of the automation. The saved responses showed what the 200s really contained: blank pages with the workspace as parent, not database rows. Comparing what we asked for with what the API actually did turned guessing into a targeted fix.

The fix, step by step

  1. Add an explicit Content-Type: application/json header to the request.
  2. Run the same request again. In our case it now failed loudly with a 404 access error instead of a false 200.
  3. Follow the error: the 404 pointed to a missing database share. Grant the integration access to the database.
  4. Run again and read the result back. The row was created and the log database filled.

The header did not fix everything on its own. What it did was force the server to validate the body, which turned silence into an actionable message. The whole chain was fixed in minutes after that.

A second “healthy but broken” case: WooCommerce webhooks that never fire

In the same session we hit a different failure with the same symptom. Our store’s outbound order events sat undelivered for more than eighteen minutes while the webhook was still reported as active. The queue never flagged a problem. Forcing synchronous delivery, one line of code in our outbound path, made the events process immediately and cleared the backlog. Different cause, same lesson: a status that says healthy is not proof that work is being done.

The rule we work by now

  • 200 means accepted, not applied. A success status confirms the server took the request, not that it did what you meant.
  • Every write needs an independent read-back. Read the saved record from outside the tool that reported success.
  • Silence is not health. A queue or webhook marked active can still be doing nothing.

FAQ

Why does an API return 200 when nothing was saved?

Because 200 only reports that the request was accepted. In our test the Notion API accepted a request whose body it could not read (no Content-Type header) and created a blank page instead of the row we asked for.

How do I check that an API write actually worked?

Read the result back independently. We stored each raw API response in an order note and compared it with the record we expected. That is what exposed the blank workspace-level pages.

Did adding the Content-Type header fix it immediately?

No. It turned the silent 200 into a 404 access error. That error pointed to a missing database share; granting access completed the fix.

Is a stuck webhook queue the same problem?

No. The WooCommerce events were never processed until we forced synchronous delivery. It looks the same from the outside, but the cause is different.

The tested, runnable version of our read-back check and the exact automation patch are in the members’ library.

What a 200 status actually promises (RFC 9110)

The HTTP specification is narrower than most dashboards suggest. RFC 9110 says the 200 code indicates that the request has succeeded, and what the body means depends on the method you used. It says nothing about whether the server stored the fields you intended to send. That judgment belongs to your code, not to the status line.

The other success codes carry more specific meanings, and they are worth knowing because an API that uses them gives you better signals:

Asynchronous work deserves extra care. Google’s API design guidance for long-running operations argues that blocking while a slow task runs is often a poor user experience, so those methods return an operation object you poll instead of the final result. Either way, the first response describes the request, not the finished work.

Why a missing Content-Type can turn a write into a no-op

A JSON body is just bytes until the server knows how to parse it. MDN describes the request side of the header plainly: in a POST or PUT, the client uses the Content-Type header to specify the type of content being sent. Leave the header out and the server has to guess.

RFC 9110 spells out what guessing means. If a Content-Type header field is not present, the recipient may treat the data as generic binary or try to work out the type by examining it. A server that does neither can simply skip the body and process the rest of the request with defaults. That is how a write can come back green and empty at the same time.

Some servers handle this more strictly. HTTP has a dedicated status, 415 Unsupported Media Type, and MDN notes that the format problem can come from the request’s Content-Type or Content-Encoding header or from inspecting the data itself. A 415 is the honest answer. A 200 with defaults is the dangerous one, and nothing in HTTP prevents a server from choosing it.

Version headers matter in the same way. Notion’s API reference, for example, says requests carry a Notion-Version header and that setting this header is required. When you debug an empty write, check every header the provider documents as mandatory, not just authentication.

How to verify an API write: a read-back pattern

The fix in our incident came from reading the result back outside the tool that reported success. Here is the same idea as a generic routine you can apply to any API:

  1. Log the exact request. Capture the method, URL, every header and the raw body as sent on the wire, not as configured in the tool’s form.
  2. Log the raw response. Store the status, headers and full body somewhere you can read later, separate from the automation’s own run history.
  3. Compare the echo with the intent. Many create endpoints return the new object. Check that the fields you sent came back, and that the parent or container is the one you targeted.
  4. Fetch the record with a separate GET. Use the ID from the response and read it back. If the API exposes a list or query endpoint, confirm the record appears where users will look for it.
  5. Assert, then alert. Turn the comparison into a check that fails the run when a field or parent does not match, instead of a log line nobody reads.

Retries need the same discipline. MDN’s glossary notes that a client can safely retry requests that use idempotent methods, while POST and PATCH are not guaranteed to be idempotent. Blindly retrying a create can produce duplicates. Stripe solves this with idempotency keys: Stripe saves the status code and body of the first request made with a given key, and later requests with that key return the saved response, including 500 errors. If your provider offers a similar mechanism, use it before you add automatic retries to a write path.

Other common causes of “success” with nothing saved

  • Wrong parent or target. The record was saved, just not where you look. Check the parent field in the response before anything else.
  • Missing access that surfaces later. Notion’s status-code reference describes 404 object_not_found responses whose message tells you to make sure the relevant pages and databases are shared with your integration. A fixed header can uncover a permission problem that was hidden behind the silent success, which is what happened to us.
  • Accepted but queued. A 202 or an internal queue can report success before any work happens. Our stuck webhook case above is one example.
  • Unknown fields ignored. As a general pattern, some APIs drop field names they do not recognize instead of rejecting them. A typo in a property name then looks like a successful write with a missing value. Your read-back step catches this.

Should I treat a 200 differently from a 201?

Yes, when the API uses both. RFC 9110 defines 201 as a request that resulted in one or more new resources being created, while 200 only says the request succeeded. Neither replaces a read-back, but a create call that returns 200 when the docs promise 201 is worth a closer look.

Is it safe to retry a write that returned 200 but saved nothing?

Only if you know the request is idempotent or the API supports idempotency keys. POST requests are not guaranteed to be idempotent, so a retry may create a second record. Fix the request first, read back what the earlier attempts actually created, and clean up any stray records before rerunning.


How we know

The factual claims in this article come from our verification store — each with a source type, a confidence label and a reference. The method is documented on How we know.

– The empty-body failure produced no error signal anywhere: the automation run was marked success, the HTTP status was 200, and a valid page ID came back. The only symptom was that the target database stayed empty. | source: first-hand experience | conf: observed | ref: Cowork-økt 2026-07-28: tre separate 200-svar, null rader i måldatabasen
– After adding the Content-Type header, the same request finally failed loudly with a 404 access error — a real, actionable message that pointed to the missing database share and let us fix the whole chain in minutes. | source: first-hand experience | conf: verified | ref: Cowork-økt 2026-07-28: v1.4-kjøring ga eksplisitt 404 etter header-fiks; DB-flytting løste tilgang
– We only found the root cause by building ourselves a read path: each automation step wrote its raw API response into a store order note that we could read back independently of the automation’s own success report. | source: first-hand experience | conf: verified | ref: Cowork-økt 2026-07-28: ordre-notat-diagnosen (NOTION-DIAG 1–4) avslørte parent=workspace
– HTTP 200 does not mean your data was saved. In our test, three separate 200 responses each created a blank page nowhere near the database we targeted — success status only means the server accepted the request, not that it did what you meant. | source: first-hand experience | conf: mythbuster | ref: Cowork-økt 2026-07-28: Success Mirage bekreftet på nytt API-lag (Notion), samme mønster som WP REST-funnene
– A message queue that is never processed looks identical to a healthy one from the outside: our store’s outbound order events sat undelivered for over eighteen minutes with the webhook still reported as active. The fix was one line of code forcing synchronous delivery. | source: first-hand experience | conf: observed | ref: Cowork-økt 2026-07-28: Woo async-levering fyrte aldri; sync-filter lagt i outbound-v1.0-fila
– When our workflow platform called the Notion API without an explicit Content-Type header, the request body was silently ignored: the API returned HTTP 200 and created a blank workspace-level page instead of the database row we asked for. | source: first-hand experience | conf: verified | ref: Cowork-økt 2026-07-28: Fase A-bygg, diag-runde 1–2 (read-back via ordre-notater)

Related guides

Verification & Governance · Published

The Gate Rejected One Draft, Approved Another Same Day

One draft got rejected for inventing a detail. Another, on the same day, passed clean. Here's what that proves about the check in between.

Verification & Governance · Published

We Proved Our AI Never Got Its Instructions — With One Word

A one-line test that shows whether your AI actually received its instructions -- before a green status hides the gap.

Verification & Governance · Published

Four Ways We Checked the Same Work, Four Different Things We Caught

We ran the same AI-written files through four separate checks. Each layer caught something real that every layer before it had missed.