Uploading Images and Media with a REST API
Learn when to use raw bytes, multipart, or resumable uploads, with runnable cURL, Python, and Node.js examples plus fixes for common errors.
Uploading an image to a REST API means sending the file bytes in the request shape that the specific endpoint documents. There is no universal REST upload format. Most APIs use one of four patterns: a raw binary body, multipart/form-data, multipart/related, or a resumable upload session.
Before writing client code, confirm the endpoint’s HTTP method and URL, authentication scheme, request content type, file field name, accepted MIME types, size limit, and response behavior. Some services return a completed media resource; others return an upload token or a processing state that you must use in a second request.
Choose the request shape
| Pattern | Use it when | Typical response |
|---|---|---|
| Raw binary | The endpoint expects the file itself as the request body. | Uploaded resource or upload token. |
multipart/form-data |
The file is a form field, often with other fields such as a title. | Uploaded resource. |
multipart/related |
Metadata and media must be sent as ordered parts in one request. | Resource containing metadata and media. |
| Resumable or chunked | Files are large or interruptions are likely. | Session URL, then a final resource. |
OpenAPI 3.0.2 states: “To upload multiple files, a multipart media type MUST be used.” The specification describes the request contract; the service documentation still decides the exact field names and limits. See the OpenAPI request-body specification.
1. Read the endpoint contract
- Find the upload operation and verify whether it uses
POST,PUT, or another method. - Identify authentication: bearer token, API key, OAuth access token, or a signed upload URL.
- Check the exact top-level
Content-Type. Do not replacemultipart/relatedwithmultipart/form-dataor vice versa. - Check the accepted media types, maximum size, filename rules, and whether multiple files are allowed.
- Read the response schema. Record whether success is synchronous, returns an upload token, or starts asynchronous processing.
- Check idempotency, retry, and rate-limit guidance before adding automatic retries.
2. Upload with multipart/form-data
Use multipart when the API names a file form field, such as file or image. The client library creates the boundary and per-part headers. Do not set a bare Content-Type: multipart/form-data header yourself unless you also provide the boundary; most libraries do this automatically.
cURL
curl -X POST 'https://api.example.com/v1/images' \\
-H 'Authorization: Bearer YOUR_TOKEN' \\
-F 'file=@./photo.jpg;type=image/jpeg' \\
-F 'alt_text=Product photo'
Python
from pathlib import Path
import requests
endpoint = 'https://api.example.com/v1/images'
headers = {'Authorization': 'Bearer YOUR_TOKEN'}
with Path('photo.jpg').open('rb') as image_file:
response = requests.post(
endpoint,
headers=headers,
files={'file': ('photo.jpg', image_file, 'image/jpeg')},
data={'alt_text': 'Product photo'},
timeout=90,
)
response.raise_for_status()
print(response.json())
Node.js
import { createReadStream } from 'node:fs';
import { basename } from 'node:path';
import { FormData } from 'undici';
const form = new FormData();
form.append('file', createReadStream('./photo.jpg'), {
filename: basename('./photo.jpg'),
contentType: 'image/jpeg'
});
form.append('alt_text', 'Product photo');
const response = await fetch('https://api.example.com/v1/images', {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_TOKEN' },
body: form
});
if (!response.ok) {
throw new Error(`${response.status}: ${await response.text()}`);
}
console.log(await response.json());
Replace the URL, token, field names, and MIME type with the provider’s documented values. Cloudflare Images, for example, documents a single multipart POST and a 10 MB image limit; that limit applies to that service, not to REST APIs generally. See the Cloudflare Images upload reference.
3. Send a raw binary body
Some endpoints want only the bytes in the request body. Google Photos documents this pattern: the top-level type is application/octet-stream, while X-Goog-Upload-Content-Type declares the media type. The binary response is an upload token used by a later media-creation call.
cURL
curl -X POST 'https://photoslibrary.googleapis.com/v1/uploads' \\
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \\
-H 'Content-type: application/octet-stream' \\
-H 'X-Goog-Upload-Content-Type: image/jpeg' \\
--data-binary '@photo.jpg'
Python
from pathlib import Path
import requests
with Path('photo.jpg').open('rb') as image_file:
response = requests.post(
'https://photoslibrary.googleapis.com/v1/uploads',
headers={
'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/octet-stream',
'X-Goog-Upload-Content-Type': 'image/jpeg',
},
data=image_file,
timeout=90,
)
response.raise_for_status()
print(response.text)
Node.js
import { readFile } from 'node:fs/promises';
const bytes = await readFile('./photo.jpg');
const response = await fetch('https://photoslibrary.googleapis.com/v1/uploads', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'Content-Type': 'application/octet-stream',
'X-Goog-Upload-Content-Type': 'image/jpeg'
},
body: bytes
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
console.log(await response.text());
A raw upload is not interchangeable with multipart. If the server expects a form field, sending bytes directly usually produces a missing-field or unsupported-media-type error.
4. Send metadata and media with multipart/related
Google Drive and Gmail document multipart/related when metadata and file content belong to one request. The metadata part comes first, followed by the media part. Each part has its own content type and the outer request has a boundary.
--BOUNDARY
Content-Type: application/json; charset=UTF-8
{"name":"photo.jpg","description":"Product photo"}
--BOUNDARY
Content-Type: image/jpeg
(binary JPEG bytes)
--BOUNDARY--
Use an SDK or a carefully constructed streaming request when possible; hand-written boundaries are easy to corrupt. Google Drive’s upload guide documents simple, multipart, and resumable modes. Its simple and multipart modes are intended for files of 5 MB or less.
5. Use resumable uploads for large or interruption-prone transfers
Resumable protocols separate session creation from byte transfer. A typical sequence is:
- Start a session with the file metadata and total size.
- Read the session URL returned by the service.
- Send the file in one or more chunks, usually with
PUTand a byte-range or upload-offset header. - Persist the session URL and last confirmed offset so a process restart can continue.
- Handle the final response, which may be a resource or an asynchronous processing state.
Google Drive recommends resumable uploads for files greater than 5 MB or when a network interruption is likely. Google Photos supports splitting media into sections and uploading them one at a time. These thresholds and headers are provider-specific. Follow the current endpoint documentation rather than copying another service’s protocol.
6. Handle asynchronous processing
An HTTP success response can mean “accepted for processing,” not “the image is ready.” Mastodon’s media endpoint is an example where larger media can be processed asynchronously. Read the response schema and API version: look for a processing status, polling URL, job identifier, or webhook. Store the returned media ID and poll at the documented interval. Do not repeatedly upload the same file while processing is pending.
Authentication and request details
- Send bearer tokens in
Authorization: Bearer ...unless the provider specifies another scheme. - Keep API keys and OAuth tokens on the server. Never put long-lived credentials in browser JavaScript or public image URLs.
- Set the MIME type from the actual file format, not only the filename extension. Common values include
image/jpeg,image/png,image/webp,image/gif,video/mp4, andapplication/pdf; accept only types documented by the endpoint. - Use a streaming file handle for large files so the entire object is not duplicated in memory.
- Preserve the server’s request ID and response body in structured logs, but redact tokens and private media URLs.
Validation and edge cases
- Empty files: reject zero-byte input before making a request.
- Wrong extension: inspect magic bytes or decode the image when security matters; a renamed executable is not a JPEG.
- Unicode filenames: send the documented filename encoding and test names containing spaces, accents, and non-Latin characters.
- Multiple files: use multipart when supported. OpenAPI requires a multipart media type for multiple file uploads, but the field naming convention (repeated field versus array) is endpoint-specific.
- Duplicate retries: use an idempotency key if the API supports one. Otherwise, record a client upload ID and reconcile the response before retrying a timed-out request.
- Proxies: check reverse-proxy body limits, timeout settings, and buffering. A proxy can reject a request before the application sees it.
- Browser uploads: use a short-lived signed upload URL or a server-side relay when the provider does not support safe browser authentication. Configure CORS only for the origins you need.
- Privacy: remove sensitive EXIF metadata when appropriate and confirm retention and public-access behavior in the provider’s policy.
Performance, reliability, and cost
- Measure total upload time, time to first response, and processing time separately.
- Choose a region or upload host close to the producer when the service offers that choice.
- Compress images before upload only when the resulting quality and dimensions meet the API’s requirements. Avoid repeatedly recompressing already-compressed JPEGs.
- For large files, stream and use resumable chunks. A chunk size that fits your network and provider guidance reduces restart waste.
- Retry transient 408, 429, and 5xx responses with exponential backoff and jitter. Honor
Retry-After; do not retry authentication or validation failures blindly. - Set an explicit connect and read timeout. A timeout does not prove the server rejected the upload; check idempotency or query the resource before retrying.
- Budget for the provider’s request, storage, bandwidth, and transformation charges. The 5 MB Google Drive, 10 MB Cloudflare Images, and below-50 MB Google Photos guidance are service-specific examples, not pricing or universal size rules.
Or skip the browser setup
If the file you need is a screenshot of a web page, you can capture the image first and then send those bytes to your own upload endpoint. ScreenshotNeo returns a PNG, JPEG, WebP, or PDF from one GET request, so there is no browser automation project to maintain.
cURL
curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options. Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 400 missing file | Wrong multipart field name or malformed boundary. | Copy the documented field name and let your library generate the boundary. |
| 415 unsupported media type | Top-level or part MIME type is not accepted. | Use the exact content type listed by the endpoint; do not assume an extension is enough. |
| 401 or 403 | Expired token, missing scope, or wrong authentication header. | Refresh credentials, verify scopes, and check the provider’s auth example. |
| 413 request too large | Provider or proxy size limit exceeded. | Check documented limits, resize the file, or switch to resumable upload. |
| Upload times out | Slow connection, proxy timeout, or synchronous processing. | Stream the body, increase read timeout within reason, and use resumable or asynchronous processing if supported. |
| Request succeeds but no media appears | Processing is asynchronous or the resource is private. | Poll the returned job or media ID and verify permissions. |
| Duplicate media after retry | The first request completed but its response was lost. | Use idempotency keys or reconcile by client upload ID before retrying. |
Short FAQ
Is multipart required for every image upload?
No. Use the format in the endpoint contract. Raw bytes, multipart/form-data, multipart/related, and resumable sessions are all documented patterns.
Should I base64-encode the image?
Only if the API explicitly requests base64 in JSON. Otherwise, base64 increases payload size and adds unnecessary encoding work.
Can I upload directly from a browser?
Yes, when the service provides a safe signed URL or browser-compatible authentication and CORS configuration. Keep permanent provider credentials off the client.
How do I upload a 100 MB video?
Use the provider’s resumable or chunked protocol, persist the session state, and confirm the service’s media type and size limits first.
What should a successful response contain?
It may contain a media resource, an upload token, or an asynchronous job state. Code against the documented response schema rather than assuming the bytes are immediately available.


