imageupload.io
2026-07-18 · By imageupload.io editorial

Image Upload API Guide: curl, JavaScript, and Python

Image Upload API Guide: curl, JavaScript, and Python

An image upload API turns a local file into a hosted URL without requiring a person to operate a browser. It is useful for support tools, publishing workflows, monitoring screenshots, documentation systems, and applications that need to return a shareable image.

imageupload.io accepts a single image as multipart/form-data at POST https://imageupload.io/api/upload. Authenticated integrations send an API key in the bearer authorization header. A successful JSON response includes a human-facing share URL, a direct image URL, dimensions, stored size, expiration details, embed snippets, and current API quota information.

This article focuses on implementation decisions and complete working examples. Keep the API reference open for the authoritative field and error tables, and visit the image upload API overview for the product-level capabilities.

1. Create and protect an API key

Sign in and copy the API key from the dashboard profile. Keys begin with iu_live_. Treat the key like a password because requests authenticated with it operate as your account and use your storage and monthly API quota.

Store it in an environment variable rather than source code:

export IMAGEUPLOAD_API_KEY='iu_live_replace_with_your_key'

Do not commit the value to Git, put it into browser-side JavaScript, include it in a query string, or print it in application logs. Server-side code can read the environment variable and add the header at request time:

Authorization: Bearer iu_live_...

If a key is exposed, rotate it from the account dashboard and replace the secret in the deployment environment.

2. Understand the multipart request

The request body must contain exactly one file part. Supported image formats are PNG, JPEG, WebP, GIF, and AVIF. The server validates the declared type and decodes the image, so renaming arbitrary bytes to .jpg does not make them a valid upload.

Optional text fields include:

  • expiration: 1d, 1w, 1mo, 3mo, burn, views, forever, or custom
  • view_limit: an integer from 1 to 999 when expiration=views
  • custom_expires_at: a future ISO date/time for eligible plans when expiration=custom
  • password: an optional viewer password up to 256 characters
  • folder: a folder slug owned by the authenticated user
  • custom_slug: an eligible plan’s custom link segment

The default expiration is 1d when the field is omitted. Plan restrictions apply to longer, permanent, and custom expiration choices. The expiration reference lists the current availability.

3. Upload with curl

curl handles multipart boundaries automatically when you use -F. Do not set a manual Content-Type: multipart/form-data header because the generated boundary must match the encoded body.

curl --fail-with-body   --request POST   --url https://imageupload.io/api/upload   --header "Authorization: Bearer $IMAGEUPLOAD_API_KEY"   --form "[email protected];type=image/jpeg"   --form "expiration=1w"

To add password protection:

curl --fail-with-body   --request POST   --url https://imageupload.io/api/upload   --header "Authorization: Bearer $IMAGEUPLOAD_API_KEY"   --form "[email protected];type=image/png"   --form "expiration=1d"   --form "password=$IMAGE_VIEW_PASSWORD"

If jq is installed, print only the direct URL:

curl --silent --show-error --fail-with-body   https://imageupload.io/api/upload   -H "Authorization: Bearer $IMAGEUPLOAD_API_KEY"   -F "[email protected];type=image/jpeg"   -F "expiration=1mo" | jq -r '.direct_url'

Use share_url instead when the result will be opened by a person, especially if the upload is password-protected or view-limited.

4. Upload with JavaScript on Node.js

Modern Node.js provides fetch, FormData, and Blob. The following complete script reads a JPEG, uploads it, checks HTTP errors, and prints both URLs:

import { readFile } from 'node:fs/promises';

const apiKey = process.env.IMAGEUPLOAD_API_KEY;
if (!apiKey) throw new Error('IMAGEUPLOAD_API_KEY is required');

const bytes = await readFile('photo.jpg');
const form = new FormData();
form.append('file', new Blob([bytes], { type: 'image/jpeg' }), 'photo.jpg');
form.append('expiration', '1w');

const response = await fetch('https://imageupload.io/api/upload', {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
  body: form,
});

const payload = await response.json().catch(() => ({}));
if (!response.ok) {
  const retryAfter = response.headers.get('retry-after');
  throw new Error(
    `Upload failed (${response.status}): ${payload.error || 'unknown error'}` +
    (retryAfter ? `; retry after ${retryAfter}s` : '')
  );
}

console.log('Share page:', payload.share_url);
console.log('Direct image:', payload.direct_url);
console.log('Remaining quota:', response.headers.get('x-quota-remaining'));

Do not add your own multipart content type. fetch sets it with the correct boundary when the body is a FormData instance.

Browser-side JavaScript must not contain a private bearer key. If a browser feature needs uploads tied to your service account, send the file to your own authenticated backend and let that backend call the image API. This keeps the secret out of page source, developer tools, browser extensions, and public network logs.

5. Upload with Python

The Requests library accepts file and form fields separately. A context manager ensures the file is closed even if the request fails.

import os
import requests

api_key = os.environ.get("IMAGEUPLOAD_API_KEY")
if not api_key:
    raise RuntimeError("IMAGEUPLOAD_API_KEY is required")

with open("photo.jpg", "rb") as image_file:
    response = requests.post(
        "https://imageupload.io/api/upload",
        headers={"Authorization": f"Bearer {api_key}"},
        files={"file": ("photo.jpg", image_file, "image/jpeg")},
        data={"expiration": "1w"},
        timeout=(10, 120),
    )

try:
    payload = response.json()
except ValueError:
    payload = {}

if not response.ok:
    message = payload.get("error", "unknown error")
    retry_after = response.headers.get("Retry-After")
    suffix = f"; retry after {retry_after}s" if retry_after else ""
    raise RuntimeError(f"Upload failed ({response.status_code}): {message}{suffix}")

print("Share page:", payload["share_url"])
print("Direct image:", payload["direct_url"])
print("Size:", payload["size"], "bytes")

The connect and read timeout tuple prevents a broken network connection from hanging the process forever while still allowing enough time to transfer and process a valid image.

6. Read the success response

A successful request returns HTTP 200 and a JSON object shaped like this:

{
  "ok": true,
  "slug": "aB3xQf9K",
  "ext": "jpg",
  "filename": "photo.jpg",
  "mime": "image/jpeg",
  "size": 184732,
  "width": 1920,
  "height": 1080,
  "expiration": "1w",
  "expires_at": 1776288000,
  "view_limit": null,
  "password_protected": false,
  "folder": null,
  "share_url": "https://imageupload.io/i/aB3xQf9K",
  "direct_url": "https://imageupload.io/f/aB3xQf9K.jpg",
  "embed": {
    "html": "<img src="https://imageupload.io/f/aB3xQf9K.jpg" alt="photo.jpg" />",
    "bbcode": "[img]https://imageupload.io/f/aB3xQf9K.jpg[/img]",
    "markdown": "![](https://imageupload.io/f/aB3xQf9K.jpg)"
  },
  "upload_source": "api",
  "quota": {
    "used": 4,
    "limit": 10,
    "remaining": 6,
    "plan": "free",
    "unlimited": false
  }
}

The values above illustrate the documented response shape. Your slug, size, dimensions, timestamps, URLs, and quota values come from the actual upload.

Store the slug if your application will later manage the image through supported account tools. Display share_url to a human and place direct_url in an image renderer. The expires_at value is a Unix timestamp or null for an eligible non-expiring upload.

Quota state is available both in the JSON quota object and in X-Quota-Used, X-Quota-Limit, and X-Quota-Remaining response headers for bearer-authenticated calls.

7. Handle errors deliberately

Do not assume every non-200 response has the same cause. Common status codes include:

  • 400 for a missing file, invalid multipart body, invalid image bytes, or invalid expiration
  • 401 for a missing or invalid API key
  • 403 for an expiration or custom feature unavailable to the account plan
  • 413 when the file or account storage exceeds its limit
  • 415 for an unsupported image type
  • 429 for a rate limit or exhausted monthly API quota
  • 503 when upload processing capacity is temporarily full

Log the status code and safe error message, but never log the authorization header. A Retry-After header means the client should wait before another attempt. Quota exhaustion is not a transient network failure, so repeated automatic retries only create noise.

8. Retry without causing a storm

Retry temporary failures such as a network interruption, 429, or 503 with exponential backoff and random jitter. For example, wait approximately 1, 2, 4, then 8 seconds, adjusted by a small random amount. Honor Retry-After when present.

Do not retry 400, 401, 403, 413, or 415 unchanged. Fix the request, credential, plan choice, file size, or file format first. Limit the total number of attempts and make the failure visible to the calling workflow.

Uploads are not documented as idempotent. If a connection fails after the server accepted the image but before your client received the response, retrying may create another image. Applications where duplicates matter should record each successful slug and provide a reconciliation step rather than retrying indefinitely.

9. Choose direct and share URLs correctly

Use direct_url in an HTML img element, Markdown image syntax, or a program that explicitly expects image bytes. Use share_url for a person, a password prompt, or a controlled viewing flow.

The image-to-URL guide explains embed formats and debugging. For sensitive deliveries, combine the share URL with the practices in the password-protected image guide.

10. Production integration checklist

Before deploying your integration:

  • Keep the API key in a secret manager or protected environment variable.
  • Validate file size and type before transfer for faster user feedback.
  • Set connect and read timeouts.
  • Parse JSON defensively and check the HTTP status first.
  • Handle quota headers and Retry-After.
  • Retry only transient failures with a strict attempt limit.
  • Do not expose the bearer key to browser code.
  • Select expiration explicitly instead of depending on a default you may later forget.
  • Store returned slugs and URLs your application needs.
  • Use the share page for protected human viewing and the direct URL for rendering.

With those controls, the endpoint is a small and predictable building block. Test with a low-risk image, confirm the resulting URLs, and then consult the full REST and MCP documentation as you add folders, view limits, custom expiration, or agent workflows.