Many upload APIs need the binary object and its structured metadata in the same request so the server can validate, store, and index both pieces together. Sending both parts in one multipart transaction avoids workflows where a file lands successfully but the JSON that describes it arrives separately or not at all.

In cURL, every --form option becomes one part inside a multipart/form-data body. Use @file when the part should be uploaded as a file with a filename, and use <file when the part should remain a normal form field whose contents come from a local file. That distinction is important for JSON metadata because many APIs expect a field such as metadata with application/json content rather than a second file attachment.

Multipart requests usually fail because the field names, content types, or quoting do not match the API contract exactly. Keep secrets out of literal command lines, prefer a dedicated metadata.json file over long inline JSON when values come from scripts or operators, and switch from <metadata.json to @metadata.json only when the endpoint explicitly documents metadata as an uploaded file.

Steps to upload files and JSON in multipart requests with cURL:

  1. Create the file that will be uploaded.
    $ printf 'Quarterly vendor onboarding packet.\n' > sample.txt
    $ ls -l sample.txt
    -rw-r--r-- 1 user user 36 Mar 28 11:24 sample.txt

    The uploaded payload can be text or binary; the important part is that the later --form field points to the correct local file.

  2. Save the metadata in a JSON file so the multipart field can be reused without fragile shell quoting.
    $ cat <<'EOF' > metadata.json
    {
      "description": "Vendor onboarding packet",
      "tags": ["documents", "intake"],
      "expires_in_days": 30
    }
    EOF
    $ cat metadata.json
    {
      "description": "Vendor onboarding packet",
      "tags": ["documents", "intake"],
      "expires_in_days": 30
    }

    Avoid leaving credentials, tokens, or personal data in plaintext JSON files unless the directory is access-controlled and the file lifecycle is managed deliberately.

  3. Send the multipart request with @sample.txt for the uploaded file and <metadata.json for the JSON form field.
    $ curl --silent --show-error --request POST \
      --url https://httpbin.org/post \
      --form 'file=@sample.txt;type=text/plain' \
      --form 'metadata=<metadata.json;type=application/json'
    {
      "files": {
        "file": "Quarterly vendor onboarding packet.\n"
      },
      "form": {
        "metadata": "{\n  \"description\": \"Vendor onboarding packet\",\n  \"tags\": [\"documents\", \"intake\"],\n  \"expires_in_days\": 30\n}\n"
      }
    }

    @ makes the file part a file upload, while < keeps metadata as a regular multipart field that still carries the explicit application/json content type. Replace file and metadata with the exact field names required by the real API.

  4. Capture the response and verify the server saw both the uploaded file and the JSON metadata before swapping in the production API URL.
    $ curl --silent --show-error --request POST \
      --url https://httpbin.org/post \
      --form 'file=@sample.txt;type=text/plain' \
      --form 'metadata=<metadata.json;type=application/json' \
      --output response.json \
      --write-out 'HTTP %{http_code}\n'
    HTTP 200
    $ grep -q '"file"' response.json && \
      grep -q '"metadata"' response.json && \
      echo "Multipart file and JSON fields verified."
    Multipart file and JSON fields verified.

    A reliable multipart check combines a successful HTTP status with response evidence that the expected field names actually reached the server.

  5. Switch the JSON part to @metadata.json only when the API expects metadata as a second uploaded file instead of a normal form field.
    $ curl --silent --show-error --request POST \
      --url https://httpbin.org/post \
      --form 'file=@sample.txt;type=text/plain' \
      --form 'metadata=@metadata.json;type=application/json'
    {
      "files": {
        "file": "Quarterly vendor onboarding packet.\n",
        "metadata": "{\n  \"description\": \"Vendor onboarding packet\",\n  \"tags\": [\"documents\", \"intake\"],\n  \"expires_in_days\": 30\n}\n"
      },
      "form": {}
    }

    With @metadata.json, the JSON moves into the server's file-upload area because curl adds a filename to the part, which is not what most metadata fields expect.

  6. Use inline JSON only for short, fixed metadata values that are fully controlled by the command.
    $ curl --silent --show-error --request POST \
      --url https://httpbin.org/post \
      --form 'file=@sample.txt;type=text/plain' \
      --form 'metadata={"description":"Operator-approved metadata","tags":["documents","reviewed"]};type=application/json'
    {
      "files": {
        "file": "Quarterly vendor onboarding packet.\n"
      },
      "form": {
        "metadata": "{\"description\":\"Operator-approved metadata\",\"tags\":[\"documents\",\"reviewed\"]}"
      }
    }

    When the metadata string is built from unpredictable input, writing it to metadata.json and reusing <metadata.json;type=application/json is safer and easier to review than expanding the JSON directly on the shell command line.