diff --git a/program/models.py b/program/models.py
index 5217837d807e0d84fc32a995edfd52ea1771e55b..fc926cbfc9002d432311d28518b2c696e116789e 100644
--- a/program/models.py
+++ b/program/models.py
@@ -145,6 +145,18 @@ class Image(models.Model):
     ppoi = PPOIField()
     width = models.PositiveIntegerField(blank=True, null=True)
 
+    def render(self, width: int | None = None, height: int | None = None):
+        if width is None and height is None:
+            return self.image.url
+        elif width and height:
+            return self.image.crop[f"{width}x{height}"].url
+        aspect_ratio = self.width / self.height
+        if width is None:
+            width = int(height * aspect_ratio)
+        if height is None:
+            height = int(width / aspect_ratio)
+        return self.image.thumbnail[f"{width}x{height}"].url
+
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
 
diff --git a/program/serializers.py b/program/serializers.py
index 97d8df0ac9ccec24787555e23d319dfcb24e2024..131303e8ae6cc826177e724a2b093aa2e7adbb53 100644
--- a/program/serializers.py
+++ b/program/serializers.py
@@ -342,6 +342,11 @@ class ImageSerializer(serializers.ModelSerializer):
         return instance
 
 
+class ImageRenderSerializer(serializers.Serializer):
+    width = serializers.IntegerField(required=False, min_value=1)
+    height = serializers.IntegerField(required=False, min_value=1)
+
+
 class HostSerializer(serializers.ModelSerializer):
     image_id = serializers.PrimaryKeyRelatedField(
         allow_null=True, queryset=Image.objects.all(), required=False, source="image"
diff --git a/program/views.py b/program/views.py
index 23439554013f33c15a7a653e818565d980f53975..6caeb13e3ca0792d383112d2589a387cb44a4db8 100644
--- a/program/views.py
+++ b/program/views.py
@@ -25,15 +25,23 @@ from itertools import pairwise
 from textwrap import dedent
 
 from django_filters.rest_framework import DjangoFilterBackend
-from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
+from drf_spectacular.utils import (
+    OpenApiParameter,
+    OpenApiResponse,
+    OpenApiTypes,
+    extend_schema,
+    extend_schema_view,
+)
+from rest_framework import decorators
 from rest_framework import filters as drf_filters
 from rest_framework import mixins, permissions, status, viewsets
 from rest_framework.pagination import LimitOffsetPagination
 from rest_framework.response import Response
 
+from django.conf import settings
 from django.contrib.auth.models import User
 from django.db import IntegrityError
-from django.http import HttpResponse, JsonResponse
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404
 from django.utils import timezone
 from django.utils.translation import gettext as _
@@ -61,6 +69,7 @@ from program.serializers import (
     ErrorSerializer,
     FundingCategorySerializer,
     HostSerializer,
+    ImageRenderSerializer,
     ImageSerializer,
     LanguageSerializer,
     LicenseSerializer,
@@ -347,6 +356,33 @@ class APIImageViewSet(viewsets.ModelViewSet):
 
         return Response(status=status.HTTP_204_NO_CONTENT)
 
+    @extend_schema(
+        parameters=[
+            ImageRenderSerializer,
+            OpenApiParameter(
+                name="Location",
+                type=OpenApiTypes.URI,
+                location=OpenApiParameter.HEADER,
+                description="/",
+                response=[301],
+            ),
+        ],
+        responses={301: None},
+    )
+    @decorators.action(["GET"], detail=True)
+    def render(self, *args, **kwargs):
+        image = self.get_object()
+        serializer = ImageRenderSerializer(data=self.request.GET)
+        if serializer.is_valid():
+            image_spec = serializer.validated_data
+            url = image.render(
+                width=image_spec.get("width", None),
+                height=image_spec.get("height", None),
+            )
+            return HttpResponseRedirect(settings.SITE_URL + url)
+        else:
+            return Response(status=status.HTTP_400_BAD_REQUEST)
+
 
 @extend_schema_view(
     create=extend_schema(summary="Create a new show."),