---
title: "Make Image Upload Retries Safe With Content-Addressed URLs"
description: "Use bounded retries for uncertain image uploads and verify that identical bytes return one stable imgd.dev URL."
slug: image-upload-retry-content-hash
date: 2026-08-20
updated: 2026-08-20
last_tested: 2026-08-20
summary: "Retry only network failures, HTTP 429, and server failures. Reuse identical bytes so imgd.dev returns the same SHA-256 hash and public URL."
cluster: Engineering
intent: engineering
sources:
  - title: "imgd.dev agent reference"
    url: https://imgd.dev/llms.txt
  - title: "imgd.dev OpenAPI specification"
    url: https://imgd.dev/openapi.json
---

# Make Image Upload Retries Safe With Content-Addressed URLs

Retry only network failures, HTTP `429`, and server failures. Keep the same bytes for every attempt.
imgd.dev uses the SHA-256 content hash, so identical bytes return the same hash and public URL.
Stop on `401`, `402`, `413`, and `415` until the caller changes something.

Every imgd.dev image is public. Do not upload private, confidential, or secret material.

imgd.dev charges a one-time **$1 per GB** storage price. Purchases stack and do not expire.
There is no free tier, so fund the account before the first upload.

## Why an uncertain result is safe

A connection can fail after the server accepts the bytes. The caller then does not know whether the upload succeeded.

A random object name can create two stored objects after a retry. A content hash avoids that result.
imgd.dev uses the image bytes as the identity:

```text
same bytes -> same SHA-256 hash -> same public URL
```

A repeated upload can return HTTP `200` with `"deduplicated": true`.
A new upload normally returns HTTP `202` with `"status": "processing"`.
Both responses identify the same bytes with the same hash and URL.

## Use the bounded Node.js example

The complete example is in `examples/engineering/retry-upload.mjs`.
It uses Node.js built-ins and makes at most four attempts for one upload operation.
It then uploads the same bytes again and compares both results.

Set the key outside the repository:

```sh
# IMGD_KEY must already exist in this shell.
node examples/engineering/retry-upload.mjs ./screenshot.png
```

The script never prints `$IMGD_KEY`. It sends the key only in the `Authorization` header.
Do not put a key in a URL, source file, command history, or log.

The retry policy is small and explicit:

```js
const MAX_ATTEMPTS = 4;
const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);

for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
  const request = makeRequest();
  let response;
  try {
    response = await fetch("https://imgd.dev/v1/upload", request);
  } catch (networkError) {
    if (attempt === MAX_ATTEMPTS) throw networkError;
    await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
    continue;
  }

  const parsed = await response.json().catch(() => ({}));
  const body = parsed && typeof parsed === "object" ? parsed : {};
  if (response.ok) return body;

  const message = body.fix ?? `upload failed with HTTP ${response.status}`;
  if (!RETRYABLE_STATUS.has(response.status) || attempt === MAX_ATTEMPTS) {
    throw new Error(message);
  }

  await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** (attempt - 1)));
}
```

The maintained example also rebuilds `FormData` for each attempt.
It computes the local SHA-256 hash and checks the public URL response.

## Expected result

A successful run prints JSON like this:

```json
{
  "hash": "<64-character-sha256>",
  "url": "https://i.imgd.dev/i/<64-character-sha256>",
  "first_http_status": 202,
  "repeated_http_status": 200,
  "repeated_deduplicated": true,
  "same_hash": true,
  "same_url": true,
  "public_url_http_status": 200
}
```

The second response can have another success status when concurrent work exists.
The required checks are `same_hash`, `same_url`, and a successful public URL request.

The public URL works while moderation has `processing` status.
It serves a strong blurred placeholder until the check allows recognizable bytes.

## Branch on the status before a retry

| Status | Action |
| --- | --- |
| Network error | Retry the same bytes within the attempt limit. |
| `429` | Wait, honor `Retry-After` when present, and retry within the limit. |
| `500`, `502`, `503`, `504` | Use backoff and retry within the limit. |
| `401` | Stop. Fix the missing or invalid key. |
| `402` | Stop. Buy storage or delete stored images, then start a new attempt. |
| `413` | Stop. Resize the image below the current limit, then start again. |
| `415` | Stop. Convert the file or correct its media type, then start again. |
| Other `4xx` | Stop. Read `error` and show `fix` without a blind retry. |

Do not retry every `4xx` response. Most client errors require a changed request or account state.
See [how actionable API errors help agents recover](/blog/actionable-api-errors-ai-agents/) for the full branch model.

## Verify the stored result

The example performs three checks:

1. It computes SHA-256 from the local bytes.
2. It checks both API hashes against that value.
3. It fetches the returned public URL and requires an image response.

You can also inspect the image record:

```sh
curl "https://imgd.dev/v1/images/$HASH" \
  -H "Authorization: Bearer $IMGD_KEY"
```

A terminal status is `live`, `review`, or `blocked`.
A `review` state is terminal and does not clear through more polls.

## Recovery rules

- Preserve the original bytes until the operation finishes.
- Do not recompress or rewrite the file between attempts.
- Keep the retry count bounded.
- Show the server `fix` value exactly when the request needs a caller change.
- Record the returned hash and URL, but never record `$IMGD_KEY`.
- Start a new operation only after a required caller change.

If an expiry matters, send it on every upload of the same bytes.
A repeated upload replaces the prior expiry choice.
Read [how temporary URLs keep the same link](/blog/temporary-image-url-api/) before you combine retries with expiry.

## When this design fits

Use this design for CI screenshots, agent output, documentation images, and other public images.
It fits callers that can retain the same byte array across a short retry window.

Do not use imgd.dev for private images, video, a free trial, or full digital-asset management.
Use another service when you need access control or a broad media workflow.

For a basic Node.js upload, read [upload an image with JavaScript and Node.js](/blog/upload-image-javascript-nodejs/).
For an agent-led setup, use [`/integrate.md`](/integrate.md).
For a human setup, open [Get started](/#start).
