Media

http
POST /v1/products/:ref/image
POST /v1/banners/:ref/image
POST /v1/logo

Attach images without multipart uploads: give the API a URL to fetch (sourceUrl) or inline base64 bytes. The server downloads/decodes, validates the actual bytes, optimizes (WebP/PNG resize) and stores the image — the same pipeline the app's own uploads use. Available on every plan with an active subscription.

:ref accepts your externalId or the opaque id (products/banners; unknown ref → 404 with PRODUCT_NOT_FOUND / BANNER_NOT_FOUND).

Request body#

Exactly one of sourceUrl or base64 (both/neither → 400 INVALID_IMAGE_SOURCE):

FieldMeaning
sourceUrlPublic https:// URL the server fetches (see the rules below)
base64Inline image bytes (plain base64 or a data: URI); the media routes accept bodies up to 21 MB, enough for the full 15 MB binary cap
cropOptional, "square" — server-side center-crop before processing. Products: optional. Logo: effectively required for non-square sources — the logo pipeline expects a ~1:1 input

sourceUrl rules#

The fetch is strictly sandboxed; violations return machine codes:

  • https:// only, default port, no credentials in the URL → 400 INVALID_SOURCE_URL
  • The host must resolve to a public address — private/internal ranges are rejected → 403 BLOCKED_SOURCE_URL
  • Redirects are re-validated per hop (max 3)
  • ~10 s total fetch timeout; 15 MB hard size cap → 413 SOURCE_TOO_LARGE
  • Content is checked by real file signature — JPEG, PNG, WebP or HEIC (headers/extensions are ignored; GIF not supported) → 400 INVALID_IMAGE

After acquisition, the standard per-type limits of the upload pipeline apply — the same ones the app UI enforces:

TargetMax file sizeDimensions
Product image5 MB
Banner image5 MBmin 1280×720, aspect 16:9
Logo5 MBmin 128×128, max 2048×2048, ~square (1:1)

15 MB transport ≠ 5 MB per-type cap

The fetch/decode layer accepts up to 15 MB so agents can point at originals, but every image type is then capped at 5 MB by the shared upload pipeline (FILE_TOO_LARGE). Re-compress large photos before sending — the pipeline converts to WebP anyway, so a 2–3 MB source loses nothing.

For best performance: pre-optimize before sending

Server-side processing runs through a shared queue, so heavy originals wait longer. You'll get the fastest uploads by sending images that are already close to what the pipeline produces:

  • WebP format — the server re-encodes everything to WebP anyway;
  • target dimensions — products max 1024×1024, banners max 1920×1080, logo 512×512;
  • well under the 5 MB cap — a 200–500 KB WebP at target size is ideal.

This is a recommendation, not a requirement — every upload is still validated and re-encoded server-side regardless (magic-byte checks and the SSRF rules above always apply).

Example — product image by URL#

bash
curl -X POST https://api.duck-hub.com/v1/products/prod-margherita/image \
  -H "Authorization: Bearer dk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "sourceUrl": "https://images.example.com/margherita.jpg", "crop": "square" }'

Example — logo from local bytes (base64)#

bash
BASE64=$(base64 -w0 logo.png)
curl -X POST https://api.duck-hub.com/v1/logo \
  -H "Authorization: Bearer dk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d "{ \"base64\": \"$BASE64\", \"crop\": \"square\" }"

Product/banner responses return { mediaUrl, mediaType }; uploading again replaces the previous image. The logo response returns { logoUrl } and regenerates the favicon set.

Errors#

CodeStatusMeaning
INVALID_IMAGE_SOURCE400Neither or both of sourceUrl/base64
INVALID_SOURCE_URL400Not https, bad port/credentials, unresolvable host, bad redirect, non-2xx
BLOCKED_SOURCE_URL403Host resolves to a non-public address
SOURCE_TOO_LARGE413Over the 15 MB fetch/decode cap
INVALID_IMAGE400Bytes are not JPEG/PNG/WebP/HEIC
FILE_TOO_LARGE400Image over the 5 MB per-type cap (after fetch/decode)
IMAGE_TOO_SMALL400Below the target's minimum dimensions
IMAGE_DIMENSIONS_TOO_LARGE400Logo over 2048×2048 px
IMAGE_NOT_SQUARE400Logo is not ~1:1 — send crop: "square"
IMAGE_WRONG_ASPECT_RATIO400Banner image is not 16:9
IMAGE_UNREADABLE / IMAGE_PROCESSING_FAILED400Dimensions unreadable / processing failed
MEDIA_BUSY503Processing queue full during a burst — honour Retry-After (a few seconds) and resend
PRODUCT_NOT_FOUND / BANNER_NOT_FOUND404Unknown :ref