Generate alt text during an upload by posting the image, reading its hash, and polling GET /v1/images/:hash. Stop when the status is live, review, blocked, or error. Use alt_text only after live. Limit the poll count so a failed process cannot wait forever.

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 an image that needs private or confidential storage.

You need a funded account, cURL, jq, and an accepted image below 20 MB. Load IMGD_KEY from a secret manager.

The public URL works immediately with a blurred placeholder during processing. Poll only when your task needs the final status or alt text.

Upload and poll with a fixed limit

This Bash script makes at most 20 metadata requests. It waits three seconds between processing responses.

#!/usr/bin/env bash
set -eu

: "${IMGD_KEY:?Set IMGD_KEY in your shell.}"
IMAGE_PATH="${1:-image.png}"
WORK_DIR="$(mktemp -d)"
UPLOAD_BODY="$WORK_DIR/upload.json"
METADATA_BODY="$WORK_DIR/metadata.json"
trap 'rm -rf "$WORK_DIR"' EXIT

UPLOAD_HTTP_STATUS="$(
  curl --silent --show-error \
    --output "$UPLOAD_BODY" \
    --write-out '%{http_code}' \
    --request POST 'https://imgd.dev/v1/upload' \
    --header "Authorization: Bearer $IMGD_KEY" \
    --form "file=@$IMAGE_PATH"
)"

case "$UPLOAD_HTTP_STATUS" in
  200|202)
    ;;
  *)
    jq . "$UPLOAD_BODY" >&2
    exit 1
    ;;
esac

HASH="$(jq --exit-status --raw-output '.hash' "$UPLOAD_BODY")"
IMAGE_URL="$(jq --exit-status --raw-output '.url' "$UPLOAD_BODY")"
printf 'Public URL: %s\n' "$IMAGE_URL"

PUBLIC_HTTP_STATUS="$(
  curl --silent --show-error \
    --output /dev/null \
    --write-out '%{http_code}' \
    "$IMAGE_URL"
)"
test "$PUBLIC_HTTP_STATUS" = '200'

TERMINAL_STATUS=''
for ATTEMPT in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
  METADATA_HTTP_STATUS="$(
    curl --silent --show-error \
      --output "$METADATA_BODY" \
      --write-out '%{http_code}' \
      "https://imgd.dev/v1/images/$HASH" \
      --header "Authorization: Bearer $IMGD_KEY"
  )"

  if [ "$METADATA_HTTP_STATUS" != '200' ]; then
    jq . "$METADATA_BODY" >&2
    exit 1
  fi

  STATUS="$(jq --exit-status --raw-output '.status' "$METADATA_BODY")"
  case "$STATUS" in
    processing)
      if [ "$ATTEMPT" -lt 20 ]; then
        sleep 3
      fi
      ;;
    live|review|blocked|error)
      TERMINAL_STATUS="$STATUS"
      break
      ;;
    *)
      printf 'Unknown image status: %s\n' "$STATUS" >&2
      exit 1
      ;;
  esac
done

if [ -z "$TERMINAL_STATUS" ]; then
  printf 'The image stayed in processing after 20 checks. Stop and inspect it later.\n' >&2
  exit 1
fi

case "$TERMINAL_STATUS" in
  live)
    ALT_TEXT="$(
      jq --raw-output '
        if (.alt_text | type) == "string" and (.alt_text | length) > 0
        then .alt_text
        else ""
        end
      ' "$METADATA_BODY"
    )"
    if [ -n "$ALT_TEXT" ]; then
      printf 'Alt text: %s\n' "$ALT_TEXT"
    else
      printf 'The image is live, but alt text is not available. Add manual alt text.\n' >&2
    fi
    ;;
  review)
    printf 'The image needs human review and stays blurred. This state is terminal.\n' >&2
    jq '.moderation' "$METADATA_BODY" >&2
    exit 2
    ;;
  blocked)
    printf 'The image was blocked and its public link is unavailable.\n' >&2
    jq '.moderation' "$METADATA_BODY" >&2
    exit 3
    ;;
  error)
    printf 'The image process ended with an error.\n' >&2
    exit 4
    ;;
esac

The loop ends after about one minute at most. It does not assume that review will change later.

Read the upload response

A new upload returns HTTP 202 with alt_text: null while the check runs.

{
  "hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
  "status": "processing",
  "mime": "image/png",
  "bytes": 86420,
  "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"
}

Identical bytes can return HTTP 200. A deduplicated response can already contain the final status and alt text.

Do not treat HTTP 202 as a failure. It means the new bytes were accepted and the automated check is not complete.

Read alt text after live

A live metadata response can contain this JSON:

{
  "hash": "0000000000000000000000000000000000000000000000000000000000000000",
  "url": "https://i.imgd.dev/i/0000000000000000000000000000000000000000000000000000000000000000",
  "status": "live",
  "mime": "image/png",
  "bytes": 86420,
  "width": 1280,
  "height": 720,
  "alt_text": "A terminal window shows a successful image upload command.",
  "category": "safe",
  "nsfw": false,
  "violence": false,
  "filename": "image.png",
  "unpublish_at": null,
  "published": true,
  "created_at": 1787184000
}

Treat alt_text as optional even after live. The script checks its type and length. It asks for manual text when the value is null, absent, or empty.

Review generated text before publication. Correct names, context, and important details that the automated text misses.

Handle every terminal state

live means the public URL serves the original image. This is the only state where this guide uses generated alt text.

review means the automated check was not confident. The public URL stays blurred. This state is terminal and never clears by itself.

blocked means the service removed the bytes and the public URL is dead. Do not upload the same bytes again.

error means the service could not complete the process. Stop the loop and report the failure. Retry once only when the server guidance permits it.

A review response can include this moderation object:

{
  "state": "review",
  "terminal": true,
  "detail": "an automated check was not confident this image is suitable for public serving, so it is served blurred",
  "recourse": "email abuse@imgd.dev with the hash to have a person review it"
}

Use the recourse from the current response. Do not keep polling a review result.

Verify the result

On success, confirm these facts:

  1. The upload returned HTTP 200 or 202.
  2. The public URL returned HTTP 200 without a key.
  3. The metadata loop stopped within 20 requests.
  4. The terminal status was live before use of alt_text.
  5. Missing alt text produced a safe manual-text message.

You can request the metadata again later with this command:

curl --fail --silent --show-error \
  "https://imgd.dev/v1/images/$HASH" \
  --header "Authorization: Bearer $IMGD_KEY" \
  | jq .

Recover from upload errors

The upload response uses a stable error value and a plain-language fix value.

Status Cause Action
401 The bearer key is missing or invalid. Reload IMGD_KEY from the secret manager. Do not log it.
402 The account has no storage or lacks enough quota. Buy storage or delete unused images. Then retry.
413 The image is larger than 20 MB. Resize or compress it below 20 MB.
415 The image type is not accepted. Use JPEG, PNG, GIF, WebP, or AVIF with the correct type.

Do not retry these errors without a change. You can retry the same bytes after an uncertain network result. Content addressing returns the same URL.

If the poll reaches its limit, stop. Keep the hash and request metadata later instead of starting an endless loop.

Security rules

Keep IMGD_KEY only in the authorization header. Do not put it in query parameters, source files, or status logs.

The image and its generated description can reveal public content. Inspect both before you publish the description in a document.

Do not use an unguessable public URL as an access-control method. Every image is public.

Decide if generated alt text fits

This process fits public images that need a first alt-text draft. It also fits automation that can inspect a terminal status.

It does not fit private images, strict human-authored descriptions, unsupported files, images above 20 MB, or free-tier needs. Read the service evaluation guide first.

Result

The bounded loop returns generated alt text only for a live image. It stops safely for review, blocked, error, and long processing states.

Start with the cURL upload guide. Use the result in the command-line screenshot guide.

Give an agent the integration procedure. A person can start from the home page.