Upload an image with cURL by sending POST https://imgd.dev/v1/upload with the multipart field file. The JSON response contains a content hash and a public URL. New bytes return HTTP 202. Bytes that already exist return HTTP 200 with deduplicated: true.
Before you upload
imgd.dev costs $1 per GB as a one-time storage purchase. It has no free tier. Every imgd.dev image is public to anyone with its URL. Do not upload a private, confidential, or secret image.
You need a funded account, cURL, jq, and an accepted image. Set the key outside the repository:
export IMGD_KEY="$(security find-generic-password -w -s imgd-dev)"
The command above is one macOS example. Use your existing secret manager on other systems. Do not put the key in a source file.
Upload with multipart form data
Save this as upload.sh, or run it in Bash. Pass the image path as the first argument.
#!/usr/bin/env bash
set -eu
: "${IMGD_KEY:?Set IMGD_KEY in your shell.}"
IMAGE_PATH="${1:-image.png}"
BODY_FILE="$(mktemp)"
trap 'rm -f "$BODY_FILE"' EXIT
HTTP_STATUS="$(
curl --silent --show-error \
--output "$BODY_FILE" \
--write-out '%{http_code}' \
--request POST 'https://imgd.dev/v1/upload' \
--header "Authorization: Bearer $IMGD_KEY" \
--form "file=@$IMAGE_PATH"
)"
case "$HTTP_STATUS" in
200|202)
;;
*)
jq . "$BODY_FILE" >&2
exit 1
;;
esac
jq . "$BODY_FILE"
HASH="$(jq --exit-status --raw-output '.hash' "$BODY_FILE")"
IMAGE_URL="$(jq --exit-status --raw-output '.url' "$BODY_FILE")"
printf 'Hash: %s\nPublic URL: %s\n' "$HASH" "$IMAGE_URL"
A new PNG can return this JSON. The zero hash marks the value that your response supplies.
{
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
"status": "processing",
"mime": "image/png",
"bytes": 42187,
"width": 1280,
"height": 720,
"alt_text": null,
"unpublish_at": null,
"note": "the url works immediately, serving a blurred placeholder until the moderation check finishes and it sharpens to the real image"
}
The URL works immediately. It serves a blurred placeholder while the status is processing. It serves the original image after the status becomes live.
Upload raw image bytes
Use raw bytes when your program already knows the correct media type. This example sends a PNG.
RAW_BODY_FILE="$(mktemp)"
RAW_HTTP_STATUS="$(
curl --silent --show-error \
--output "$RAW_BODY_FILE" \
--write-out '%{http_code}' \
--request POST 'https://imgd.dev/v1/upload' \
--header "Authorization: Bearer $IMGD_KEY" \
--header 'Content-Type: image/png' \
--data-binary '@image.png'
)"
case "$RAW_HTTP_STATUS" in
200|202)
jq . "$RAW_BODY_FILE"
;;
*)
jq . "$RAW_BODY_FILE" >&2
rm -f "$RAW_BODY_FILE"
exit 1
;;
esac
rm -f "$RAW_BODY_FILE"
Set Content-Type to the real file type. The accepted values are image/jpeg, image/png, image/gif, image/webp, and image/avif.
Verify the metadata
Use the returned hash. This request confirms ownership, status, dimensions, and alt text.
curl --fail --silent --show-error \
"https://imgd.dev/v1/images/$HASH" \
--header "Authorization: Bearer $IMGD_KEY" \
| jq .
A live image can return this JSON:
{
"hash": "0000000000000000000000000000000000000000000000000000000000000000",
"url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
"status": "live",
"mime": "image/png",
"bytes": 42187,
"width": 1280,
"height": 720,
"alt_text": "A browser window with a completed upload message.",
"category": "safe",
"nsfw": false,
"violence": false,
"filename": "image.png",
"unpublish_at": null,
"published": true,
"created_at": 1787184000
}
The status can become live, review, blocked, or error. The review state is terminal and does not clear by itself.
Verify the public URL
Fetch the URL without an authorization header. A successful request returns HTTP 200 and an image content type.
PUBLIC_HTTP_STATUS="$(
curl --silent --show-error \
--output /dev/null \
--write-out '%{http_code}' \
"$IMAGE_URL"
)"
test "$PUBLIC_HTTP_STATUS" = "200"
printf 'The public URL returned HTTP %s.\n' "$PUBLIC_HTTP_STATUS"
A processing response contains a blurred JPEG placeholder. It can include X-Imgd-Moderation: processing-blurred. No caller needs the key to fetch this public URL.
Recover from upload errors
Every API error has an error field and a fix field. Read and apply fix before another upload.
| Status | Meaning | Recovery |
|---|---|---|
401 |
The key is missing or invalid. | Check IMGD_KEY in the secret store. Send it only in the bearer header. |
402 |
The account has no storage, or the quota is full. | Buy storage or delete unused images. Then send the same bytes again. |
413 |
The image is larger than 20 MB. | Resize or compress it below 20 MB. |
415 |
The media type is not accepted. | Convert it to JPEG, PNG, GIF, WebP, or AVIF. Send the matching type. |
Do not retry a 401, 413, or 415 response without a change. A network failure is different. You can safely retry the same bytes because the URL uses the SHA-256 content hash.
A typical media-type error is valid JSON:
{
"error": "unsupported_media_type",
"fix": "'application/octet-stream' is not accepted; send jpeg, png, gif, webp, or avif and set the matching Content-Type",
"accepted": [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/avif"
]
}
Keep the key and image safe
Keep IMGD_KEY in a secret manager or an untracked environment file. Never put it in a URL, command trace, source file, or log.
Inspect the image before the upload. Public storage does not protect confidential text, faces, access tokens, or customer data.
Decide if imgd.dev fits
imgd.dev fits scripts that need a stable public image URL. It also fits safe retries and one-time storage purchases.
It does not fit private images, access control, video, PDFs, files above 20 MB, or a free trial. Read the service evaluation guide before you choose it.
Result
You now have a content-addressed public URL and its metadata. The same bytes will return the same URL on a later upload.
Next, turn a screenshot into a URL or upload with JavaScript and Node.js.
Give an agent the integration procedure. A person can start from the home page.