Skip to main content
Developer Tools

How to Send Form Data and File Uploads to an API

9 min readUpdated August 17, 2026

Plenty of APIs take structured data in the request body rather than the URL — a form submission, a profile update, an upload. How that body is shaped is a separate decision from the method, and the API on the other end decides which shape it accepts.

multipart/form-data is the shape you reach for when the request carries files, or a mix of files and ordinary text fields. It splits the body into separate parts, one per field, each with its own name — which is what lets binary file content sit alongside plain text in a single request. That’s the thing JSON can’t do: JSON is text, with no native way to carry a file’s bytes. URL-encoded form data (application/x-www-form-urlencoded) can carry flat key/value text pairs but has the same problem with files.

This guide walks through both cases — a text-only multipart request and one with a file attached — with live examples you can open and send, plus the Content-Type detail that quietly breaks more multipart requests than anything else.

A multipart request with text fields only

The simplest multipart request carries no files at all — just named text fields. Here’s the one this guide starts with, sent to httpbin.org’s /anything endpoint, which accepts any method and body and echoes back exactly what it received. That makes it safe to actually send: no account, no side effects, nothing stored anywhere.

The card below summarizes the request as multipart/form-data, but read that as what the request will be sent as — not as a header it sets. Open it and the Headers tab is empty: this request deliberately carries no Content-Type header of its own, and the section further down explains why that omission is the whole point.

  • POST https://httpbin.org/anything
  • Body type: Multipart Form Data
  • name = John Doe
  • email = john@example.com

Example request

Send a multipart form with text fields

A POST with two text form fields sent as multipart/form-data to httpbin.org’s test endpoint — safe to open and actually send.

POST https://httpbin.org/anythingContent-Type: multipart/form-data
Open in API Request Builder

Text fields: a key and a value

Every multipart field is a pair — a key (the field name) and a value. The key is the name the server looks the field up by, so it has to match what the API documents, exactly, including case. The value is just the text you’re sending under that name.

On the receiving end, each part is parsed back out into a named field in whatever the server’s form-parsing layer calls its parsed-body collection. The names you send are the names it looks up; nothing else about your field ordering or formatting survives, and none of this is specific to any one backend framework or language.

  • name = John Doe — the field named "name" arrives carrying the text "John Doe".
  • email = john@example.com — the field named "email" arrives carrying that address as text.
  • A key the API doesn’t recognize is usually ignored, or rejected as an unexpected field.
  • A key the API expects but doesn’t receive is usually reported as a missing required field.

Adding a file to the request

The second example is the same endpoint with a file field added alongside a text field. It’s the mixed case most upload endpoints actually use: some metadata, plus the file itself.

One thing to know before opening it: the file field arrives empty on purpose. A file lives on your own machine, and a shareable link is just text — there is no way to serialize a real local file into a URL, and this tool’s share format deliberately drops file contents rather than pretending otherwise. So the link carries the field name, the field’s file mode, and everything else about the request, but not a file.

That means you need to choose a local file yourself after opening the request. In the Body section, the file row shows "Choose file…" — click it, pick any small file (a plain .txt file is ideal for a first test), and the row switches to showing that file’s name. Then send.

  • name = profile — an ordinary text field.
  • file = (choose a local file) — a file field, opened empty, waiting for you to attach something.

Example request

Send a multipart form with a file

A POST with one text field and one file field. The file field opens empty — choose a local file yourself before sending, since a file can’t travel inside a link.

POST https://httpbin.org/anythingContent-Type: multipart/form-data
Open in API Request Builder

File fields: a name and a locally selected file

A file field has two halves: the field name the API expects, and the actual file you pick from your machine. The field name is yours to match against the docs — avatar, file, upload, attachment, whatever that endpoint asks for. The file half is chosen through the browser’s own file picker.

What actually goes over the wire for that part is the file’s bytes, along with metadata the browser attaches — the field name, the original filename, and the file’s content type. That’s all assembled by the browser’s FormData mechanism when the request is sent. Worth being precise about: a file field does not send a path or a filename string as its value. If you type "profile.png" into a text field, the server receives the literal text "profile.png", not an image — which is a genuinely common way to end up debugging an upload that never contained a file.

  • avatar = profile.png — read this as "the field named avatar carries the file profile.png", not as a text value.
  • The field name comes from the API’s documentation; the file comes from your machine.
  • Filename and content type ride along as part metadata, set by the browser from the file you picked.

Never set the Content-Type header yourself

This is the single most common way a multipart request breaks, and it looks like the opposite of a mistake: you know the body is multipart/form-data, so you add a Content-Type header saying exactly that. The request then fails to parse on the server, usually with an unhelpful error about a missing or malformed body.

The reason is the boundary. A multipart body is a sequence of parts separated by a delimiter string, and the receiving server has to be told what that delimiter is — it travels as a parameter on the Content-Type header itself, not inside the body. The full header looks like multipart/form-data; boundary=... with a randomly generated token after the equals sign, and the server splits the body on exactly that token.

When you send a browser FormData body, the browser generates that boundary and writes the complete header for you. Set the header manually and you overwrite it with a version that has no boundary parameter, so the server has nothing to split the body on. The fix is to not set it: leave Content-Type off entirely and let the browser fill it in.

This is why the examples above carry no Content-Type header, and why the API Request Builder doesn’t add one for you in Multipart Form Data mode the way it does for a JSON body. Open either example and check the Headers tab — it’s empty. It’s the same rule in fetch() code, which is why the generated snippet further down sends the FormData object with an empty headers object.

  • Correct: send the form body with no Content-Type header at all.
  • Broken: Content-Type: multipart/form-data — no boundary, so the server can’t split the parts.
  • What the browser actually sends: multipart/form-data; boundary=(a generated token).
  • This applies to browser FormData specifically — a server-side HTTP client or cURL builds its own boundary the same way.

JSON vs. multipart/form-data

Neither format is better in the abstract — they’re answers to different questions, and in practice the API you’re calling has already picked one. Check its documentation before choosing.

  • JSON (Content-Type: application/json) — the right choice when the API expects structured JSON: nested objects, arrays, numbers, booleans. The default for most modern REST APIs, and you do set this header yourself.
  • multipart/form-data — the right choice for files, flat text form fields, or a mix of the two. Header set automatically, with a boundary.
  • Nesting is where multipart gets awkward — it’s a flat list of named parts, so representing deep structure means flattening it into field names or sending a JSON string as one field’s value.
  • Files are where JSON gets awkward — carrying a file means base64-encoding it into a string, which inflates the payload and requires the API to be designed for it.

How to test a form-data request, step by step

Building either example from scratch in the API Request Builder:

  • Open the API Request Builder.
  • Select POST as the method.
  • Enter https://httpbin.org/anything as the URL.
  • Open the Body tab.
  • Choose Multipart Form Data as the body type.
  • Click "Add field" and fill in the Key and Value columns — for example, name and John Doe.
  • Add a second field the same way for email.
  • For a file field, add the field, type its key, then click the paperclip button on that row to switch it to a file value.
  • Click "Choose file…" on that row and pick a local file — the row then shows the filename.
  • Leave the Headers tab alone: do not add a Content-Type header.
  • Send the request.
  • Inspect the response — status code, body, headers, and response time all appear once it comes back.

Text fields and a file in one request

Most real upload endpoints want both: the file, plus some fields describing it. A profile update might look like this — one file and two text fields, all in a single multipart body.

Configuring it is exactly the two steps above combined: three rows in Multipart Form Data mode, two of them left as text values, one switched to a file value with the paperclip button. No Content-Type header on any of them.

What a real endpoint does with those fields — which are required, what file types it allows, what it returns — is entirely that API’s business. The examples here point at httpbin.org purely as an inspection sandbox: it echoes back what it received so you can confirm the request was shaped correctly, and it implements no particular upload schema of its own.

  • POST /upload
  • name = John Doe (text field)
  • email = john@example.com (text field)
  • file = profile.txt (file field — selected locally)

As a cURL command

The text-only example, copied straight from the request’s code panel. Each -F flag is one form field, and cURL handles the boundary itself — note there’s no -H Content-Type flag here either:

  • curl -X POST 'https://httpbin.org/anything' \
  • --max-time 30 \
  • -F 'name=John Doe' \
  • -F 'email=john@example.com'

Adding a file in cURL

A file field uses the same -F flag with an @ prefix on the value, which tells cURL to read a file rather than send the text literally:

  • -F 'file=@demo.txt' — sends the file demo.txt from the current directory.
  • -F 'file=demo.txt' — without the @, sends the literal text "demo.txt" instead. A frequent typo.

As a JavaScript (Fetch) call

The same request as a fetch() call. The important detail is what’s missing: headers is empty, because setting Content-Type here would strip the boundary and break the request:

  • const controller = new AbortController();
  • const timeoutId = setTimeout(() => controller.abort(), 30000);
  • try {
  • const formData = new FormData();
  • formData.append("name", "John Doe");
  • formData.append("email", "john@example.com");
  • const response = await fetch("https://httpbin.org/anything", {
  • method: "POST",
  • headers: {},
  • body: formData,
  • credentials: "same-origin",
  • signal: controller.signal,
  • });
  • const data = await response.text();
  • console.log(data);
  • } finally {
  • clearTimeout(timeoutId);
  • }

Adding a file in fetch()

A file field is appended the same way, using a File object — usually straight from a file input element:

  • formData.append("file", fileInput.files[0]);
  • formData.append("file", fileInput.files[0], "custom-name.txt"); — optional third argument overrides the filename sent.

Common mistakes

Most "why is my upload failing" reports come down to one of these.

  • Setting the multipart Content-Type by hand — the boundary goes missing and the server can’t split the body into parts. Covered above; it’s worth repeating because the header looks correct.
  • Sending JSON when the API expects multipart — a JSON body with a JSON Content-Type reaching an endpoint that only parses form data typically comes back as a parse error or an unsupported-media-type response, not a helpful message about the format mismatch.
  • The wrong field name — an endpoint expecting avatar won’t find a field you named file, and vice versa. Field names are matched exactly, so this usually surfaces as "no file was uploaded" even though a file clearly was.
  • Forgetting the file field entirely — if the endpoint requires a file, sending only text fields fails validation, often with a message about the missing field rather than anything mentioning uploads.
  • The wrong file type — many endpoints restrict uploads by MIME type or extension (images only, PDFs only), and reject anything else regardless of how well-formed the request is.
  • A file that’s too large — browsers, the API itself, and any reverse proxy or CDN in between can each impose their own upload size ceiling. Which one you hit, and at what size, depends entirely on that stack’s configuration.
  • Typing a filename into a text field — sends the string, not the file. Switch the row to a file value with the paperclip button instead.

Reading the response

Once the request comes back, four things are worth checking: the status code first, then the response body, the response headers, and how long it took. httpbin.org echoes the request back, so its body is a fast way to confirm the server saw the fields you thought you sent — a file part shows up under "files" rather than "form", which is a useful sanity check that the field really was sent as a file.

When a multipart request fails, the status code narrows it down quickly:

  • 400 or 422 — the body was malformed or a required field was missing. The usual suspect is the Content-Type boundary problem, or a field name that doesn’t match.
  • 401 or 403 — an authentication or permission problem, not a format one. The upload never got as far as being parsed.
  • 413 — the payload was too large; the server or a proxy in front of it rejected it on size.
  • 415 — unsupported media type: the server won’t accept this format, either for the body as a whole or for the uploaded file specifically.

Browser requests and CORS

Multipart requests sent from a browser are subject to CORS like any other cross-origin request — and a multipart POST is never a "simple" request, so it triggers a preflight OPTIONS call the API has to answer correctly before the real upload is allowed through.

That’s worth knowing because it explains a specific symptom: an upload that works fine from cURL but fails from your own frontend’s JavaScript. cURL isn’t a browser and ignores CORS entirely, so the difference points at CORS rather than at anything wrong with the multipart body. See What Is a CORS Error, and How Do You Fix It? (linked below) for what to actually do about it.

Where your file actually goes

The API Request Builder assembles and sends the request in your browser, using the same fetch() and FormData a web page would — the file you pick is read by the browser and sent to whatever URL you entered, and nothing about it is stored by this site.

The one qualification: sending is browser-first, but if the target API blocks cross-origin browser requests, this tool’s CORS proxy fallback (or a custom proxy you’ve configured) can route the request through a third-party server instead. In that case the request — file included — does pass through that server, the way any proxy works. It would not be accurate to say a request never leaves your browser; whether it does depends on whether a proxy was involved.

The examples on this page carry no credentials, no environment values, and no reference to any file on your machine. A file field can only ever be filled by you, locally, after the request is open.

FAQ

Why shouldn’t I set the Content-Type header for a multipart request?

Because the header has to include a boundary parameter — multipart/form-data; boundary=... — that identifies the delimiter separating the parts of the body. The browser generates that boundary when it builds the FormData body and writes the complete header itself. Setting the header manually replaces it with one that has no boundary, leaving the server nothing to split the body on.

Can I share a request that already has my file attached?

No. A share link is text, and a file is binary data on your own machine — it can’t be encoded into a URL, and this tool deliberately drops file contents rather than trying. A shared multipart request carries the field names and everything else about the request, but the file field arrives empty for whoever opens it to fill in themselves.

When should I use multipart/form-data instead of JSON?

When files are involved, or when the API explicitly documents a form-data endpoint. For structured data with no files, JSON is usually the better fit and is what most REST APIs expect. The deciding factor is what the API accepts, not a general preference.

Why does my upload return 413?

413 Payload Too Large means something in the chain rejected the request on size before or during processing — the API itself, or a reverse proxy, load balancer, or CDN in front of it, each of which can enforce its own limit. The specific ceiling depends on that stack’s configuration, so check the API’s documented upload limit first.

Can I send a file with a GET request?

No. GET requests conventionally carry no body, and browsers won’t send one — the API Request Builder shows a warning if you configure a body on a GET or HEAD request, and the body is left out when the request is sent. Uploads use POST, or sometimes PUT/PATCH for replacing an existing file.

Test a multipart upload yourself.

Try the free API Request Builder

Related Guides