Amondo Docs

Overview

Every CloudinaryMedia object in the API exposes a delivery field with server-built URL templates for sized delivery. Instead of downloading the full-size publicUrl, substitute a width into a template and fetch a variant that matches the size you actually render. This gives smaller downloads, automatic format negotiation (WebP/AVIF for images, best codec for video) and automatic quality tuning.

media {
  publicUrl
  width
  height
  delivery {
    __typename
    ... on ImageDelivery {
      image { urlTemplate widths }
      avatar { urlTemplate widths }
    }
    ... on VideoDelivery {
      video { urlTemplate widths }
      poster { urlTemplate widths }
    }
  }
}

The field is available on both endpoints (gql.amondo.com and pub.amondo.com) wherever CloudinaryMedia appears — tile media, author avatars, form images, backgrounds and so on.

Template fields

delivery is a union: ImageDelivery for IMAGE media, VideoDelivery for VIDEO media. Switch on __typename.

TypeFieldUse for
ImageDeliveryimageRendering the image at layout size
ImageDeliveryavatarSmall round author avatars (fixed widths 20, 30, 40, 60, 80)
VideoDeliveryvideoVideo playback at layout size
VideoDeliveryposterJPG still of the first video frame — placeholder before playback

Each of these is a DeliveryTemplate:

FieldTypeDescription
urlTemplateString!Absolute URL containing the literal token {w}
widths[PositiveInt!]!Valid substitutions for {w}, ascending

Example urlTemplate values:

image:  https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:good,w_{w}/v1718000000/amondo/wpib2xtdf.jpg
avatar: https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:eco,w_{w}/v1718000000/amondo/wpib2xtdf.jpg
video:  https://media.amondo.com/amondo-media/video/upload/c_limit,f_auto,q_auto:good,w_{w}/v1718000000/amondo/k2n4fh1q.mp4
poster: https://media.amondo.com/amondo-media/video/upload/c_limit,f_auto,q_auto:low,so_0,w_{w}/v1718000000/amondo/k2n4fh1q.jpg

delivery is null for passthrough media (Instagram CDN, S3, base64, uploads still processing). Always handle this case: render publicUrl as-is.

Choosing a width

To build a URL:

  1. Take the width the media is rendered at, in layout units (CSS px, iOS points, Android dp).
  2. Multiply by the device pixel ratio, clamped to 2 — beyond 2× the extra pixels are not worth the bytes.
  3. Pick the smallest widths entry ≥ that product, or the largest entry if none is.
  4. Substitute it for the {w} token in urlTemplate.

Only the listed widths are supported — do not substitute arbitrary values.

function deliveryUrl(template, renderedWidth, pixelRatio) {
  const target = renderedWidth * Math.min(pixelRatio, 2)
  const width = template.widths.find(w => w >= target) ?? template.widths.at(-1)
  return template.urlTemplate.replace('{w}', String(width))
}

The widths ladder runs in 100px steps and is capped at the source media width (rounded up to the next 100) and at 2000 — e.g. a 1080px-wide source yields [100, 200, …, 1100]. The ladder is per-media; always read it from the response.

Scaling and crop rules

Delivery URLs scale, they never crop. Every template uses c_limit:

  • Aspect ratio is always preserved. The returned file is the source scaled down so its width fits {w}; height follows the source aspect ratio.
  • Never upscaled. If {w} exceeds the source width you get the source size. Don't assume the returned file is exactly {w} pixels wide.
  • Never cropped server-side. Fitting media into a box of a different aspect ratio (a square grid cell, a 9:16 story) is always done by the renderer.

Tiles tell you which fit the editor chose. MediaTileAttributes (and the other tile attribute types) carry two TileCrop settings:

  • crop — how media displays in the grid: FILL crops to fill the cell, FIT letterboxes.
  • cropFrame — the same choice for the expanded frame view.

Map FILL/FIT to your platform's fit modes:

TileCropCSSUIKitSwiftUIAndroid ViewJetpack Compose
FILLobject-fit: cover.scaleAspectFill + clipsToBounds.scaledToFill().clipped()centerCropContentScale.Crop
FITobject-fit: contain.scaleAspectFit.scaledToFit()fitCenterContentScale.Fit

Uploaded Cloudinary media is ingested with a maximum 4K resolution preset. Larger source uploads may be normalised at ingest before delivery templates are generated.

Web — CSS only

No JavaScript needed: expand the ladder into srcset and let the browser pick, then crop with object-fit.

<img
  src="https://…/c_limit,f_auto,q_auto:good,w_800/…/photo.jpg"
  srcset="https://…/w_400/…/photo.jpg 400w, https://…/w_800/…/photo.jpg 800w, https://…/w_1100/…/photo.jpg 1100w"
  sizes="(max-width: 600px) 100vw, 400px"
  style="width: 100%; height: 100%; object-fit: cover"
  alt=""
/>

Generate the srcset string directly from widths:

const srcset = template.widths.map(w => `${template.urlTemplate.replace('{w}', String(w))} ${w}w`).join(', ')

For background-image, use background-size: cover (FILL) or contain (FIT).

With srcset the browser applies the full device pixel ratio (3× on many phones) and may pick one ladder step higher than the clamp-to-2 rule would. Every listed width is valid, so this is fine. If you want the bandwidth-optimal 2× cap, select the width yourself with the JS helper above.

iOS

Select the width from the view's point size and screen scale, then let the image view crop:

func deliveryURL(template: DeliveryTemplate, pointWidth: CGFloat, scale: CGFloat) -> URL {
  let target = pointWidth * min(scale, 2)
  let width = template.widths.first { CGFloat($0) >= target } ?? template.widths.last!
  return URL(string: template.urlTemplate.replacingOccurrences(of: "{w}", with: String(width)))!
}

UIKit:

imageView.contentMode = .scaleAspectFill // FILL; use .scaleAspectFit for FIT
imageView.clipsToBounds = true

SwiftUI:

AsyncImage(url: url) { image in
  image.resizable().scaledToFill() // FILL; .scaledToFit() for FIT
} placeholder: {
  Color.gray.opacity(0.2)
}
.frame(width: 160, height: 160)
.clipped()

Android

View widths are already in physical pixels; rescale them so densities above 2× don't fetch oversized files:

fun deliveryUrl(template: DeliveryTemplate, viewWidthPx: Int, density: Float): String {
  val target = viewWidthPx / density * minOf(density, 2f)
  val width = template.widths.firstOrNull { it >= target } ?: template.widths.last()
  return template.urlTemplate.replace("{w}", width.toString())
}

Views (with Coil or Glide):

<!-- FILL; use fitCenter for FIT -->
<ImageView
  android:scaleType="centerCrop"
  ... />

Jetpack Compose:

AsyncImage(
  model = url,
  contentDescription = null,
  contentScale = ContentScale.Crop, // FILL; ContentScale.Fit for FIT
  modifier = Modifier.size(160.dp),
)

Videos and posters

For VideoDelivery, pick widths for video and poster with the same algorithm, sized to the player:

  • Show the poster URL as the placeholder (<video poster="…">, AVPlayerLayer overlay, Compose placeholder) while the video loads. It is a JPG of the first frame at low quality — cheap and instant.
  • Feed the video URL to your player. Format and codec are negotiated automatically via f_auto.

Fallback

Code the delivery == null path from day one — it is not an edge case. Media served from Instagram's CDN, direct S3 objects and uploads that are still processing all return null. In that case render publicUrl unchanged (it may be full-size) with the same client-side FILL/FIT treatment.

Full example

Query a published imprint on pub.amondo.com (no auth) and render its media tiles:

query {
  imprintPublication(id: "e4f7a1b2-3c4d-5e6f-7a8b-9c0d1e2f3a4b") {
    imprint {
      layoutV2(device: MOBILE) {
        sections {
          elements {
            tile {
              ... on MediaTile {
                mediaTileAttributes {
                  crop
                  cropFrame
                  media {
                    publicUrl
                    width
                    height
                    delivery {
                      __typename
                      ... on ImageDelivery {
                        image {
                          urlTemplate
                          widths
                        }
                        avatar {
                          urlTemplate
                          widths
                        }
                      }
                      ... on VideoDelivery {
                        video {
                          urlTemplate
                          widths
                        }
                        poster {
                          urlTemplate
                          widths
                        }
                      }
                    }
                  }
                  avatar {
                    publicUrl
                    delivery {
                      ... on ImageDelivery {
                        avatar {
                          urlTemplate
                          widths
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Example media payload:

{
  "publicUrl": "https://media.amondo.com/amondo-media/image/upload/v1718000000/amondo/wpib2xtdf.jpg",
  "width": 1080,
  "height": 1350,
  "delivery": {
    "__typename": "ImageDelivery",
    "image": {
      "urlTemplate": "https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:good,w_{w}/v1718000000/amondo/wpib2xtdf.jpg",
      "widths": [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100]
    },
    "avatar": {
      "urlTemplate": "https://media.amondo.com/amondo-media/image/upload/c_limit,f_auto,q_auto:eco,w_{w}/v1718000000/amondo/wpib2xtdf.jpg",
      "widths": [20, 30, 40, 60, 80]
    }
  }
}

Rendering this tile in a 400 CSS px column on a 3× phone: target = 400 × min(3, 2) = 800 → pick w_800. The tile's crop is FILL, so the image gets object-fit: cover.