Content-Type: application/json vs text/plain — When to Use Each (with RequestBin)

Learn the difference between application/json and text/plain Content-Types. This guide explains when to use each, common developer pitfalls, and how to debug mismatched headers or invalid payloads using RequestBin with step-by-step examples.

Content-Type: application/json vs text/plain — When to Use Each (with RequestBin)
TL;DR: Use application/json when your payload is valid JSON and will be parsed by a JSON parser. Use text/plain when you’re sending raw text that shouldn’t be parsed as JSON (logs, plaintext messages, signed payload basestrings, etc.). Avoid the non-standard application/text.

Throughout this article we’ll explain how Content-Type works, the differences between application/json and text/plain, common pitfalls that break clients and webhooks, and step-by-step ways to use RequestBin to inspect, debug, and fix issues fast.

Why Content-Type matters

The Content-Type header tells the receiver how to interpret the request body. If it’s wrong or missing, your framework may:

  • Reject the request (415 Unsupported Media Type)
  • Try to parse with the wrong parser and throw (SyntaxError: Unexpected token …)
  • Treat bytes as text (or vice versa)
  • Mis-handle charsets, signatures, or compression

Rule of thumb: the Content-Type must match the actual format of the body you send.

HTTP Application handler flow

The two media types you care about

application/json

  • What it is: Structured data encoded as valid JSON ({}, [], numbers, strings, booleans, null), typically UTF-8.
  • Use when: Your server/client expects JSON and will parse it into data structures.
  • Common contexts: REST APIs, webhooks (Stripe, GitHub, etc.), mobile/SPA backends.
  • Charset: JSON is UTF-8 by default. You can add ; charset=utf-8, but most libraries ignore it.

Example body (valid JSON):

{"event":"invoice.paid","amount":1999,"currency":"usd","metadata":{"source":"web"}}

Header:

Content-Type: application/json

text/plain

  • What it is: Unstructured or “free-form” text.
  • Use when: You’re sending plain text that shouldn’t be parsed as JSON (logs, messages), or you’re transporting a signature base string / hash / CSV snippet where JSON is not appropriate.
  • Common contexts: Diagnostics, simple webhooks that send a one-liner status, signed challenge/response handshakes, manual tests.

Example body (text):

OK

Header:

Content-Type: text/plain; charset=utf-8
⚠️ Avoid application/text: it’s non-standard, may be treated as a generic binary media type, and can confuse clients and middleware. Use text/plain for plain text.
application/json and text/plain

Quick comparison

Aspectapplication/jsontext/plain
IntentStructured data for machinesHuman-readable text / unstructured data
ParserJSON parser (strict)Read as string / stream
Typical errorsNot valid JSON, wrong charset, double-encoded JSONTreated as JSON by mistake, missing charset for special characters
Best forAPIs, webhooks, typed contractsLogs, simple status messages, signature base strings
CharsetUTF-8 by defaultSpecify charset=utf-8 for safety
Security noteVerify signature over raw bytesOften used as basestring; avoid leaking secrets in text

Common developer issues (and how to spot them with RequestBin)

  1. “I sent JSON but forgot the header”
    Body is JSON, header is missing or text/plain. The server uses the text parser, not JSON.
  2. “I set application/json but sent a string”
    Body is not valid JSON (e.g., OK, or '{"foo":"bar"}' with single quotes). JSON parsers throw.
  3. Double-encoding JSON
    You call JSON.stringify() twice; the body becomes a JSON string literal (e.g., "\"{\\\"a\\\":1}\"").
  4. BOM/whitespace issues
    Accidental BOM or leading text before { breaks strict parsers.
  5. Signature verification fails
    Provider signed the raw bytes, but the receiver re-serializes JSON before computing the HMAC. The bytes don’t match.
  6. Wrong charset
    Sending non-ASCII characters without proper encoding can cause mojibake.
Conten Type in Raw Body HTTP request

Step-by-step debugging with RequestBin

Step 1 — Create a bin

  1. Go to requestbin.net and click Create a RequestBin.
  2. You get a unique endpoint, e.g.:https://abc123.requestbin.net

Step 2 — Send JSON correctly and incorrectly

A) Correct JSON (application/json)

curl -i https://abc123.requestbin.net \
  -H "Content-Type: application/json" \
  -d '{"login":"my_login","password":"my_password"}'

B) Incorrect JSON header (text/plain but JSON body)

curl -i https://abc123.requestbin.net \
  -H "Content-Type: text/plain" \
  -d '{"login":"my_login","password":"my_password"}'

C) Invalid JSON body (application/json but not JSON)

curl -i https://abc123.requestbin.net \
  -H "Content-Type: application/json" \
  -d 'OK'

Return to RequestBin and open each request. Compare:

  • Headers: Which parser would your app choose?
  • Raw Body: Is it valid JSON? Are there stray quotes or BOM?

Step 3 — Fix your client code

JavaScript (fetch / Node)

await fetch("https://abc123.requestbin.net", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({"login":"my_login","password":"my_password"})
});

Python (requests)

import requests
r = requests.post(
    "https://abc123.requestbin.net",
    json={"login":"my_login","password":"my_password"}  # sets Content-Type and serializes
)
print(r.status_code)

Curl (safe quoting)

curl -i https://abc123.requestbin.net \
  -H "Content-Type: application/json" \
  --data-binary '{"login":"my_login","password":"my_password"}'
Use --data-binary for exact bytes; -d can transform newlines on some systems.

Server-side handling: pick the right parser

Express (Node.js)

import express from "express";
const app = express();

// Important: configure parsers by media type
app.use(express.json());                       // handles application/json
app.use(express.text({ type: "text/plain" })); // handles text/plain

app.post("/ingest", (req, res) => {
  const ct = req.get("content-type") || "";
  if (ct.startsWith("application/json")) {
    // req.body is an object
    console.log("JSON payload:", req.body);
  } else if (ct.startsWith("text/plain")) {
    // req.body is a string
    console.log("Text payload:", req.body);
  } else {
    return res.status(415).send("Unsupported Media Type");
  }
  res.send("ok");
});

app.listen(3000, () => console.log("http://localhost:3000"));

What can go wrong

  • Forgetting to register express.text → your text/plain body is empty.
  • Registering a catch-all express.text() without restricting type → it consumes everything, breaking JSON parsing.

FastAPI (Python)

from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/ingest")
async def ingest(request: Request):
    ct = request.headers.get("content-type", "")
    if ct.startswith("application/json"):
        try:
            data = await request.json()  # strict JSON parse
        except Exception:
            raise HTTPException(400, "Invalid JSON")
        return {"type": "json", "received": data}
    elif ct.startswith("text/plain"):
        text = await request.body()
        return {"type": "text", "received": text.decode("utf-8")}
    else:
        raise HTTPException(415, "Unsupported Media Type")

Tip: In FastAPI/Starlette, always check Content-Type and call the appropriate method: await request.json() for JSON, await request.body() for raw.

Signatures & “verify on raw bytes”

If your webhook provider signs requests (HMAC, Ed25519, etc.), you must verify the exact bytes received, before parsing or mutating the body—even whitespace differences change the signature.

Mismatched Content-Type can break verification because some frameworks automatically decode/normalize text.

Node example (HMAC SHA-256)

import crypto from "crypto";
import express from "express";

const app = express();
// Capture raw body for signature verification
app.use((req, res, next) => {
  let data = [];
  req.on("data", chunk => data.push(chunk));
  req.on("end", () => {
    req.rawBody = Buffer.concat(data);
    next();
  });
});

app.post("/webhooks", (req, res) => {
  const secret = Buffer.from(process.env.WH_SECRET || "whsec_xxx", "utf8");
  const ts = req.get("X-Webhook-Timestamp");
  const sig = req.get("X-Webhook-Signature"); // e.g., v1=abcd...

  const payload = Buffer.from(`${ts}.`).toString("utf8");
  const signed = Buffer.concat([Buffer.from(payload, "utf8"), req.rawBody]);

  const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
  const provided = (sig || "").split("v1=")[1] || "";

  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))) {
    return res.status(400).send("Invalid signature");
  }

  // After verify, choose parser by Content-Type:
  const ct = req.get("content-type") || "";
  if (ct.startsWith("application/json")) {
    const json = JSON.parse(req.rawBody.toString("utf8"));
    // ...
  } else if (ct.startsWith("text/plain")) {
    const text = req.rawBody.toString("utf8");
    // ...
  }

  res.send("ok");
});

app.listen(3001);

Practical decision guide

Use application/json when:

  • You need strong contracts between systems (APIs/webhooks).
  • You expect to deserialize into objects/structs.
  • You rely on schema validation, OpenAPI, or typed tooling.
  • You want automated clients (SDKs) to “just work”.

Use text/plain when:

  • You’re sending log lines, simple status/health text, or debugging output.
  • You have human-in-the-loop steps (copy/paste text).
  • You send a basestring to sign or a fixed hash (where JSON adds unnecessary quoting).
  • You intentionally don’t want downstream JSON parsers to run.

Real-world tests you can run today

1) Validate your client sets the right header

# Good: JSON body + application/json
curl -i https://abc123.requestbin.net \
  -H "Content-Type: application/json" \
  --data-binary '{"msg":"hello","count":2}'

# Good: Plain text + text/plain
curl -i https://abc123.requestbin.net \
  -H "Content-Type: text/plain; charset=utf-8" \
  --data-binary 'Status: OK'

Open RequestBin and verify:

  • Content-Type matches: JSON → JSON; text → text.
  • Raw Body is exactly what you expect (no extra quotes, no BOM, no double-encoding).

2) Catch double-encoded JSON

Buggy code (double stringify):

const payload = JSON.stringify({ a: 1 });
fetch("https://abc123.requestbin.net", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload) // ❌ encodes a JSON string literal
});

In RequestBin, the Raw Body will look like:

"{\"a\":1}"

Fix by sending payload directly, not JSON.stringify(payload).

3) Reproduce a signature mismatch

  1. Compute an HMAC over timestamp + "." + raw_body.
  2. Send headers X-Webhook-Timestamp and X-Webhook-Signature: v1=<hex>.
  3. In RequestBin, confirm the exact bytes match what you signed (use --data-binary).

If your server verifies a parsed JSON stringified again, it will fail. Always verify on raw bytes.

FAQ (quick hits)

  • Should I include charset?
    For JSON, UTF-8 is assumed; including ; charset=utf-8 is harmless. For text/plain, include it if you have non-ASCII.
  • What about application/x-ndjson?
    Use it for JSON Lines (newline-delimited JSON objects). Don’t label NDJSON as application/json.
  • Can I send JSON with text/plain?
    You can, but parsers won’t treat it as JSON automatically. Only do this if the receiver expects raw text.
  • What happens if I omit Content-Type?
    Servers guess (content sniffing) or pick a default—this is fragile. Always set it.

Summary

  • application/json → structured, machine-parsed data.
  • text/plain → human-readable or unstructured text.
  • Match the header to the actual body, or your framework will mis-parse or reject the request.
  • RequestBin is the fastest way to see exactly what you sent—headers, raw bytes, and timing—so you can fix mis-matched headers, invalid JSON, and signature issues.

Spin up an endpoint on RequestBin now, send a couple of requests with both media types, and confirm your app parses exactly what you expect. You’ll catch subtle bugs (wrong header, double-encoding, stale signatures) before they hit production.