You built an endpoint that takes a file. A user uploads a profile picture to POST /avatars, or your app pushes a signed PDF to POST /documents. The route works in your head. Now you need to prove it works over HTTP: pick a real file, attach it to a form field, send the request, and check the response.
This is where a lot of API tools get fiddly. File uploads use multipart/form-data, not JSON, so you can’t paste a body and hit send. You need a request builder that understands file fields, and a test runner that can find the file when the test runs later. Apidog handles both, and this guide walks the whole path: sending a single upload, sending a file alongside JSON, asserting the response, and then the honest part nobody warns you about, which is what happens when that same upload step runs headless in the Runner or CLI and can’t find the file. If you want the background on the format itself first, the file upload in APIs primer covers how multipart requests are structured. The MDN reference on FormData is a good companion for the browser side.
What multipart/form-data is and why uploads need it
An API request body can take several shapes. In Apidog’s request Body section you can choose form-data, x-www-form-urlencoded, JSON, XML, raw, or binary. Most of the time you reach for JSON. File uploads are the exception.
The form-data body type maps to the Content-Type: multipart/form-data header. It’s the format built for uploading files along with other data. Instead of one blob, the body is split into parts, each with its own name and its own content. One part can be a plain string like a caption, another part can be the raw bytes of an image. That’s why a photo upload and its metadata can travel in the same request.
The near cousin is x-www-form-urlencoded. It looks similar in the editor, key-value pairs sent in the body, but it’s meant for simple forms without files. If your endpoint takes a file, form-data is the one you want. Reach for x-www-form-urlencoded only when every field is a short scalar and no bytes are involved.
In form-data, Apidog shows each parameter as a key-value pair, and every parameter carries a type: string, integer, file, and so on. That per-parameter type is the whole trick. Set a field to file and Apidog treats its value as a file to attach rather than text to send.
Send a single file upload and assert the response
Say you’re testing POST /avatars. It takes one field, avatar, containing an image, and returns JSON with the stored URL. Here’s the walkthrough.
1. Open the Body section and choose form-data. In your endpoint or a new request, set the method to POST and the URL to your avatars route. Open the Body tab and select the form-data body type. Apidog sets Content-Type: multipart/form-data for you.
2. Add the file parameter and set its type to file. Add a parameter with the key avatar. Next to the key, use the type selector to change its type from string to file. The value cell turns into a file picker instead of a text box.
3. Click Upload and choose a local file. Click Upload on the avatar row and pick an image from your machine, say jane-profile.png. Apidog records the path to that file.
4. Send the request. Hit Send. Apidog reads the file from the stored local path, builds the multipart body, and sends it. Worth knowing up front: Apidog sends the file in the request but does not store the file in the cloud. It saves only the local path, not the bytes. That detail matters later, so hold onto it.
A successful call comes back with something like this:
{
"id": "usr_8842",
"avatarUrl": "https://cdn.example.com/avatars/usr_8842.png",
"sizeBytes": 48210,
"contentType": "image/png"
}
5. Assert the response. A send that returns 200 isn’t a passing test on its own. Add assertions so the check is real. In Apidog you add these as post-request assertions on the endpoint or scenario step. In plain terms you want to confirm the status and that the body carries a usable URL:
status code == 200
$.avatarUrl exists
$.contentType == "image/png"
Those map to Apidog’s assertion UI directly: one assertion on the status code, one on the JSONPath $.avatarUrl being present, one on $.contentType. If you’re new to assertions, the API assertions guide shows the full set of operators and how JSONPath targets a field.
For a quick reality check outside the tool, the same upload in curl looks like this:
curl -X POST https://api.example.com/avatars \
-F "avatar=@jane-profile.png"
The -F flag is curl’s way of building a multipart part, and @ tells it to read file contents. Apidog’s form-data file parameter does the same thing with a picker instead of a flag.
Send a file and JSON together
Real endpoints rarely take a bare file. POST /documents might want the file plus metadata: a title, a category, maybe a tags array. You have two clean ways to do this in one multipart request.
The simple case is scalar fields. Add more form-data parameters next to your file field and leave them as string or integer. A title string, a category string, a file set to type file. All three ride in the same request.
When the metadata is structured, like a nested object or an array, you send it as JSON inside a string part. Add a form-data parameter named metadata, keep its type as string, and paste the JSON straight into the value:
{
"title": "Q3 Invoice",
"category": "billing",
"tags": ["invoice", "2026", "paid"]
}
So the request has two parts: file (type file) carrying q3-invoice.pdf, and metadata (type string) carrying that JSON. The server reads the file from one part and parses the JSON from the other. Many public APIs take uploads this exact way; the Stripe file upload docs are a good example of a real multipart endpoint that pairs a file part with plain fields. This pattern is common enough that Postman users hit it too; if you’re migrating, the walkthrough on uploading a file and JSON data in Postman maps cleanly onto Apidog’s form-data fields.
Need to attach more than one file? Add another parameter with type file. A POST /documents that accepts a main file and a thumbnail gets two file rows, file and thumbnail, each with its own Upload button. There’s no special multi-file mode; you just add file-type parameters until you’ve covered every part the endpoint expects.
Turn the request into a repeatable test scenario
A single send proves the endpoint works once. To catch regressions you want the upload inside a saved test scenario that runs on demand or on a schedule. Chain the steps: upload the avatar, capture the returned id, then call GET /users/{id} and assert the avatar URL persisted.
Build this the same way you built the single request, then save it as a step in a scenario. The how to write a test scenario with Apidog guide covers step chaining and passing values between steps. Once the upload lives in a scenario, you can run it against staging every deploy, add conditional branches with conditional logic in API test scenarios, or put it on a timer with scheduled API tests.
Everything above runs fine on your machine, because your machine has the file. That assumption is exactly what breaks next.
The gotcha: uploads that run somewhere else
Here’s the part the happy path hides. Apidog stores the file path, not the file. On your laptop that’s invisible, because the path resolves to a real file every time. The moment the same step runs on a different machine, the path points at nothing.
You’ll meet this in two places.
Team collaboration. When a teammate opens your POST /avatars request, they see the file parameter and the path you picked, say /Users/jane/pics/jane-profile.png. They can see the request, but they can’t send it, because that file lives on your disk, not theirs. The path is local to the machine that chose it.
Runner and CLI runs. This is the one that bites in automation. Your upload scenario passes locally, you schedule it in the Runner or fire it from the CLI, and the file-upload step fails. Nothing is wrong with your assertions. The runner just can’t find a file at the path your laptop saved, because that path doesn’t exist on the runner’s host.
The fix follows from the cause. The file has to exist on the machine doing the sending, and the step’s path has to point at it there.
For the Runner: the Runner reads files from a host directory mounted into its volume. You set that mount when you deploy the Runner, using the -v flag. Copy your upload file into that mounted host directory. Then open the file-upload step’s step details in the scenario, click the Batch Edit button in the top-right corner, and replace the file field’s value with the path inside the Runner’s directory, for example:
/opt/runner/jane-profile.png
For the CLI: same shape. Put the file on the CLI machine, then use Batch Edit on the step to point the path at its location there, for example:
/opt/apidog/runner/jane-profile.png
Cleaner than hardcoding: use a variable. Instead of pinning a literal path in the step, replace the value with a variable and set the variable’s value to the actual file path per environment. Then the same scenario runs on your laptop, the Runner, and CI without editing the step each time. You point the variable at /Users/jane/pics/jane-profile.png locally and /opt/runner/jane-profile.png on the runner, and the step itself never changes.
One prerequisite worth stating plainly: the Runner only reaches host files that live under the directory you mounted with -v at deploy time. If your file isn’t under that mount, no path will find it. That’s a deployment setup detail, not a plan limit. The Apidog docs on file upload requests spell out the mount and bulk-edit steps if you want the canonical version.
Automate the workflow with the Apidog CLI
Once your upload scenario is saved, you can run it headless in CI. Install the CLI and authenticate:
npm install -g apidog-cli
apidog login --with-token <YOUR_ACCESS_TOKEN>
Then run the saved scenario by id, pointing it at an environment:
apidog run --access-token $APIDOG_ACCESS_TOKEN -t <scenario_id> -e <env_id> -r cli
Here -t is the test scenario id, -e is the environment id, and -r is the reporter (use cli, html, or junit, comma-separated for several). The CLI runs your saved scenarios from the cloud project and reports pass/fail with exit codes, which is what lets it gate a pipeline. Setup details live in the Apidog CLI installation guide.
One honest caveat, and it’s the same one from the last section: a scenario with a file-upload step needs the file present on the CLI machine, and the step’s path has to point at it there. Put the file on the runner, then Batch Edit the path (or use a variable) before the run. Skip that and the upload step fails to find the file even though the rest of the scenario is fine. For a fuller CI setup, including passing per-row inputs, see data-driven testing with the Apidog CLI.
FAQ
Why can’t my teammate send my file upload request? Apidog stores the local file path, not the file itself, and it never uploads the file to the cloud. Your teammate sees the request and the path you picked, but that path resolves to a file on your disk, not theirs. Have them put a copy of the file on their machine and point the field at their own path. The same mechanic explains why scheduled tests and Runner jobs need the file staged where they run.
How do I send JSON along with a file in the same request? Keep the body type as form-data. Add your file field with type file, then add another parameter with type string and paste the JSON into its value. The server gets both parts in one multipart request: the file in one part, the JSON string in another. This is the standard way to attach metadata to an upload.
What path should I use for a file in the Runner? Use a path inside the host directory you mounted into the Runner’s volume with the -v flag at deploy time, for example /opt/runner/yourfile.jpg. Copy the file into that mounted directory, then open the step, click Batch Edit, and set the field’s value to that path. The CLI equivalent looks like /opt/apidog/runner/yourfile.jpg.
Is there a file size limit or an allowed file type list? The upload behavior in Apidog is about how the request is built and where the file is read from. Your actual limits on size and type come from the API you’re testing, so check your server’s own validation rules and write assertions against the responses it returns for oversized or rejected files.
Should I use form-data or x-www-form-urlencoded for uploads? Use form-data. It maps to multipart/form-data and is built to carry files. x-www-form-urlencoded is for simple forms of short scalar fields with no file, so it won’t carry your image or PDF.
Wrapping up
File upload testing comes down to two things: build the multipart request right, and make sure the file is reachable wherever the test runs. In Apidog you set the Body to form-data, flip your field’s type to file, click Upload, add any JSON as a string part, then send and assert. When you move the same scenario to the Runner or CLI, stage the file on that machine and repoint the path with Batch Edit or a variable, and the automated run behaves like your local one.
Want to try it against your own endpoint? Download Apidog, point a form-data request at your upload route, and watch the response come back. It’s free to start, no credit card required.



