// INTEGRATIONS

Drop into your stack. One URL, no SDK.

Every endpoint is one HTTPS call. Point at a source, add query params, ship the result. No token to manage, no upload step, no package to install — if your framework can render an <img> or call fetch, it can run img apis.

// THE PRIMITIVE

Every integration is the same URL.

Your account has a namespace. Once you bind a custom domain, it disappears from the URL entirely. src is either a full URL on your allowed-external list or a named source you configured in the dashboard.

Cache hits and errors never bill. Every URL is deterministic, so a warm CDN serves the same params for free, for a year.

bash·URL shape
https://<namespace>.imgapis.com/<endpoint>?src=<source>&<params>

# or, with a custom domain:
https://images.acme.com/<endpoint>?src=<source>&<params>
EndpointPurposeCredits
/info

Metadata, colors, palette, BlurHash — one response.

0.5
/info/{variant}

Single-field shortcut: metadata · colors · blurhash. Shared cache.

0.5
/transform

Resize, reformat, blur, mark, background-remove.

1
/grid

Compose 2–5 sources into a single image.

2 / source
/placeholder/{kind}

Render a real BlurHash or solid-color placeholder image.

0.5
// NEXT.JS — APP ROUTERReact
01 / 09

Custom loader, wired through next/image.

The cleanest fit is a custom image loader. Next handles srcset, lazy loading and placeholders. img apis handles the actual bytes.

1

Configure the loader

Point Next at a tiny loader file. No build plugin, no SDK.

javascript·next.config.js
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './libs/img-apis-loader.ts'
  }
};

module.exports = nextConfig;
2

Implement it in five lines

Build a URL from the loader args. Format negotiation is automatic from the Accept header — no format= needed unless you want to pin one.

typescript·libs/img-apis-loader.ts
// libs/img-apis-loader.ts
const HOST = 'https://acme.imgapis.com';

type LoaderArgs = {
  quality?: number;
  src: string;
  width: number;
};

const loader = ({ quality, src, width }: LoaderArgs) => {
  const params = new URLSearchParams({
    src,
    w: String(width),
    quality: String(quality ?? 80)
  });

  return `${HOST}/transform?${params.toString()}`;
};

export default loader;
3

Use next/image like normal

Pass either a full URL or a configured named source as src. Everything else is stock Next.

tsx·app/page.tsx
// app/page.tsx
import Image from 'next/image';

const Page = () => {
  return (
    <Image
      alt="Hero"
      height={900}
      sizes="(max-width: 768px) 100vw, 1600px"
      src="mycdn/marketing/hero.jpg"
      width={1600}
    />
  );
};

export default Page;
4

Render server-side BlurHash placeholders

Fetch /info/blurhash on the server, decode it to a data URL, hand it to placeholder="blur". Or skip decoding entirely and point placeholder at /placeholder/blurhash as a real image.

tsx·app/photo/[id]/page.tsx
// app/photo/[id]/page.tsx
import { decode } from 'blurhash';
import Image from 'next/image';

const HOST = 'https://acme.imgapis.com';

type Props = { params: { id: string } };

const fetchBlurDataUrl = async (src: string) => {
  const url = new URL(`${HOST}/info/blurhash`);
  url.searchParams.set('src', src);

  const res = await fetch(url, { next: { revalidate: 86400 } });
  const { blurhash } = (await res.json()) as { blurhash: string };

  // decode → small canvas → data URL (helper omitted)
  return blurhashToDataUrl(blurhash, 32, 32);
};

const PhotoPage = async ({ params }: Props) => {
  const src = `mycdn/photos/${params.id}.jpg`;
  const blurDataUrl = await fetchBlurDataUrl(src);

  return (
    <Image
      alt="Photo"
      blurDataURL={blurDataUrl}
      height={1200}
      placeholder="blur"
      src={src}
      width={1600}
    />
  );
};

export default PhotoPage;
5

Color-theme a route from /info/colors

The /info family shares its cache — calling /info/colors on a URL already touched by /info is a cache hit. Zero credits.

tsx·app/article/[slug]/page.tsx
// app/article/[slug]/page.tsx
const HOST = 'https://acme.imgapis.com';

const ArticlePage = async ({ params }: { params: { slug: string } }) => {
  const heroSrc = `mycdn/articles/${params.slug}/hero.jpg`;

  const url = new URL(`${HOST}/info/colors`);
  url.searchParams.set('src', heroSrc);
  url.searchParams.set('pairs', 'true');

  const res = await fetch(url, { next: { revalidate: 86400 } });
  const { palette } = await res.json();

  return (
    <article style={{ background: palette.darkVibrant.hex }}>

    </article>
  );
};
// NEXT.JS — PAGES ROUTERReact
02 / 09

Same loader contract, getStaticProps / getServerSideProps.

Same image loader file, same envvars. Server-side data fetching moves into the pages-router data hooks.

1

Wire the loader (identical)

No difference from App Router for the image config.

javascript·next.config.js
// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './libs/img-apis-loader.ts'
  }
};
2

Fetch /info inside getServerSideProps

The fetch shape doesn't change between routers — only where it lives.

tsx·pages/photo/[id].tsx
// pages/photo/[id].tsx
import type { GetServerSideProps } from 'next';
import Image from 'next/image';

const HOST = 'https://acme.imgapis.com';

type Props = {
  blurDataUrl: string;
  src: string;
};

export const getServerSideProps: GetServerSideProps<Props> = async ({
  params
}) => {
  const src = `mycdn/photos/${params!.id}.jpg`;

  const url = new URL(`${HOST}/info/blurhash`);
  url.searchParams.set('src', src);

  const res = await fetch(url);
  const { blurhash } = await res.json();
  const blurDataUrl = blurhashToDataUrl(blurhash, 32, 32);

  return { props: { blurDataUrl, src } };
};

const PhotoPage = ({ blurDataUrl, src }: Props) => {
  return (
    <Image
      alt="Photo"
      blurDataURL={blurDataUrl}
      height={1200}
      placeholder="blur"
      src={src}
      width={1600}
    />
  );
};

export default PhotoPage;
// REACT — VITE, CRA, ANYTHINGVite / CRA
03 / 09

No loader plumbing — just an <img>.

Build the URL, drop it on src. For a placeholder, point the background-image at /placeholder/blurhash and let the real image paint over it.

1

A <Photo> component you actually want to use

The BlurHash placeholder sits as the element background. The browser swaps to the transformed image when ready — no JS, no library.

tsx·src/components/photo.tsx
// src/components/photo.tsx
const HOST = 'https://acme.imgapis.com';

type Props = {
  alt: string;
  height: number;
  src: string;
  width: number;
};

const Photo = ({ alt, height, src, width }: Props) => {
  const params = new URLSearchParams({
    src,
    w: String(width),
    h: String(height),
    fit: 'cover',
    quality: '80'
  });

  const transformed = `${HOST}/transform?${params.toString()}`;
  const placeholder = `${HOST}/placeholder/blurhash?src=${encodeURIComponent(src)}&w=32&h=32`;

  return (
    <img
      alt={alt}
      height={height}
      loading="lazy"
      src={transformed}
      style={{
        backgroundImage: `url(${placeholder})`,
        backgroundSize: 'cover'
      }}
      width={width}
    />
  );
};

export default Photo;
2

Responsive srcset is a one-liner

Generate an entry per breakpoint. Every URL is cached independently for a year.

typescript·responsive srcSet
const widths = [400, 800, 1200, 1600];

const srcSet = widths
  .map(w => {
    return `${HOST}/transform?src=${encodeURIComponent(src)}&w=${w} ${w}w`;
  })
  .join(', ');
// REMIX & REACT ROUTER 7React Router 7
04 / 09

Resolve /info in the loader.

Fetch metadata where Remix already wants you to fetch data. Hand the JSON to the route component as-is.

Loader + route component

The full response — colors, palette, BlurHash, dimensions — is yours in one round-trip.

tsx·app/routes/photo.$id.tsx
// app/routes/photo.$id.tsx
import { json, useLoaderData } from 'react-router';

import type { LoaderFunctionArgs } from 'react-router';

const HOST = 'https://acme.imgapis.com';

export const loader = async ({ params }: LoaderFunctionArgs) => {
  const src = `mycdn/photos/${params.id}.jpg`;

  const url = new URL(`${HOST}/info`);
  url.searchParams.set('src', src);

  const res = await fetch(url);
  const info = await res.json();

  return json({ info, src });
};

const PhotoRoute = () => {
  const { info, src } = useLoaderData<typeof loader>();

  return (
    <figure style={{ background: info.palette.muted.hex }}>
      <img
        alt="Photo"
        height={info.metadata.height}
        src={`${HOST}/transform?src=${encodeURIComponent(src)}&w=1600`}
        width={info.metadata.width}
      />
    </figure>
  );
};

export default PhotoRoute;
// NUXT 3 / VUEVue
05 / 09

A 20-line provider for <NuxtImg>.

@nuxt/image ships a custom provider system. Drop a tiny adapter in and <NuxtImg> handles the rest.

1

Write the provider

typescript·providers/img-apis.ts
// providers/img-apis.ts
import { joinURL } from 'ufo';

import type { ProviderGetImage } from '@nuxt/image';

export const getImage: ProviderGetImage = (
  src,
  { modifiers = {}, baseURL } = {}
) => {
  const { format, height, quality, width } = modifiers;
  const params = new URLSearchParams({ src });

  if (width) { params.set('w', String(width)); }
  if (height) { params.set('h', String(height)); }
  if (format) { params.set('format', format); }
  if (quality) { params.set('quality', String(quality)); }

  return {
    url: joinURL(baseURL!, `/transform?${params.toString()}`)
  };
};
2

Register it

typescript·nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
  image: {
    providers: {
      imgApis: {
        provider: '~/providers/img-apis',
        options: { baseURL: 'https://acme.imgapis.com' }
      }
    }
  }
});
3

Use <NuxtImg> as normal

html·Vue template
<template>
  <NuxtImg
    alt="Hero"
    height="900"
    provider="imgApis"
    src="mycdn/marketing/hero.jpg"
    width="1600"
  />
</template>
// SVELTEKITSvelte
06 / 09

Build URLs server-side, in +page.svelte.

Use the load function for /info work, then render a plain <img> with the public host envvar.

1

Load /info

typescript·+page.server.ts
// src/routes/photo/[id]/+page.server.ts
import type { PageServerLoad } from './$types';

const HOST = 'https://acme.imgapis.com';

export const load: PageServerLoad = async ({ params }) => {
  const src = `mycdn/photos/${params.id}.jpg`;

  const url = new URL(`${HOST}/info`);
  url.searchParams.set('src', src);

  const res = await fetch(url);
  const info = await res.json();

  return { info, src };
};
2

Render with palette-driven theming

svelte·+page.svelte
<!-- src/routes/photo/[id]/+page.svelte -->
<script lang="ts">
  const HOST = 'https://acme.imgapis.com';

  export let data;

  $: transformed = `${HOST}/transform?src=${encodeURIComponent(data.src)}&w=1600`;
</script>

<figure style="background: {data.info.palette.darkVibrant.hex}">
  <img alt="Photo" src={transformed} />
</figure>
// ASTROIslands
07 / 09

Server-rendered by default.

Astro resolves /info at build time for static pages, on demand for dynamic routes. Either way, the request is one fetch.

Frontmatter fetch + JSX

For dynamic routes, set export const prerender = false and Astro fetches on each request — still cached at the img apis edge.

tsx·src/pages/photo/[id].astro
---
// src/pages/photo/[id].astro
const HOST = 'https://acme.imgapis.com';

const { id } = Astro.params;
const src = `mycdn/photos/${id}.jpg`;

const infoRes = await fetch(`${HOST}/info?src=${encodeURIComponent(src)}`);
const info = await infoRes.json();

const transformed = `${HOST}/transform?src=${encodeURIComponent(src)}&w=1600`;
---

<figure style={`background: ${info.palette.muted.hex}`}>
  <img
    alt="Photo"
    height={info.metadata.height}
    src={transformed}
    width={info.metadata.width}
  />
</figure>
// PLAIN HTML / VANILLA JSVanilla
08 / 09

Copy, paste, ships.

No build step, no framework. Two snippets and you have a placeholder-to-real-image swap with no JavaScript.

1

An ordinary <img>

This is the entire integration when you don't need placeholders. The URL is the API.

html·Static <img>
<img
  alt="Hero"
  height="900"
  src="https://acme.imgapis.com/transform?src=mycdn/marketing/hero.jpg&w=1600&quality=85"
  width="1600"
/>
2

Add a BlurHash background that fades on load

The /placeholder/blurhash endpoint emits a real PNG the browser renders directly. No decoder library.

html·BlurHash background swap
<img
  alt="Hero"
  loading="lazy"
  onload="this.style.background='none'"
  src="https://acme.imgapis.com/transform?src=mycdn/hero.jpg&w=1600"
  style="background: url('https://acme.imgapis.com/placeholder/blurhash?src=mycdn/hero.jpg&w=32&h=32') center/cover"
/>
// SERVER RUNTIMESNode / Bun / Workers
09 / 09

Same call shape where fetch works.

Node, Bun, Workers, Deno — the request is one fetch and the response is JSON. No streaming or decoding plumbing.

Fetch /info once, type it forever

typescript·Typed /info helper
const HOST = 'https://acme.imgapis.com';

const fetchInfo = async (src: string) => {
  const url = new URL(`${HOST}/info`);
  url.searchParams.set('src', src);

  const res = await fetch(url);

  if (!res.ok) {
    throw new Error(`img apis: ${res.status}`);
  }

  return res.json() as Promise<{
    blurhash: string;
    colors: {
      area: number;
      hex: string;
      hsl: string;
      intensity: number;
      rgb: string;
    }[];
    metadata: {
      byteSize: number;
      contentType: string;
      format: string;
      height: number;
      width: number;
    };
    palette: Record<
      | 'darkMuted'
      | 'darkVibrant'
      | 'lightMuted'
      | 'lightVibrant'
      | 'muted'
      | 'vibrant',
      { hex: string }
    >;
  }>;
};

What server-side /info unlocks

  • Upload gating. Reject SVGs, enforce minimum dimensions, refuse oversized files before they hit your storage.
  • Index-time color extraction. Persist palette JSON next to the asset row so list pages skip the per-image call.
  • Pre-computed placeholders. Store the BlurHash string in the same row — render placeholders with zero img apis traffic at view time.
// CROSS-CUTTING

Notes that apply everywhere.

The five things worth keeping in your head, no matter which framework you picked above.

Headers worth a peek

x-img-apis-cache: HIT or MISS tells you whether the call was billed. Inspect content-type to confirm the negotiated format.

CORS, not keys

Browser requests must come from an origin on your allow-list. Server-to-server with no Origin passes through. No token header — auth is origin plus namespace.

src allow-listing

src=https://… must match your allowed-external-sources. Named sources (src=mycdn/path) bypass the check — they’re already gated by being configured in your account.

Long-lived URLs

Transforms, grids and placeholders are immutable and cached for one year. Build URLs deterministically (same params, same URL) so the edge actually hits.

No SDK to drag along

Every example is plain fetch + URLSearchParams. The whole API surface is the URL — nothing to upgrade when we ship new features.

// 60 SECONDS TO YOUR FIRST IMAGE

200 free credits.No card. No catch.

No tokens to manage, no migrations to run. Make your first API call before the kettle boils.

7-day trial · No card · No auto-charge · One-line bill