diff --git a/api/features/feature_routes.go b/api/features/feature_routes.go
index 644f1ad2818cd5b37a3302c228c02aa900171e7e..2bd328ae8ead810c2c181c2be8cf0f37a8bd9159 100644
--- a/api/features/feature_routes.go
+++ b/api/features/feature_routes.go
@@ -5,6 +5,7 @@ import (
 )
 
 const MSC2448UploadRoute = "/_matrix/media/unstable/xyz.amorgan/upload"
+const MSC2448AltRenderRoute = "/_matrix/media/unstable/io.t2bot.msc2448/blurhash/{blurhash:[^/]+}"
 
 func IsRoute(r *http.Request, route string) bool {
 	uri := r.URL.Path
diff --git a/api/unstable/blurhash_render.go b/api/unstable/blurhash_render.go
new file mode 100644
index 0000000000000000000000000000000000000000..9c46c78c17bb6bb63094471ab6e5d720d0af77b3
--- /dev/null
+++ b/api/unstable/blurhash_render.go
@@ -0,0 +1,68 @@
+package unstable
+
+import (
+	"bytes"
+	"image/png"
+	"net/http"
+	"strconv"
+
+	"github.com/buckket/go-blurhash"
+	"github.com/gorilla/mux"
+	"github.com/turt2live/matrix-media-repo/api"
+	"github.com/turt2live/matrix-media-repo/api/r0"
+	"github.com/turt2live/matrix-media-repo/common/rcontext"
+	"github.com/turt2live/matrix-media-repo/util"
+)
+
+func RenderBlurhash(r *http.Request, rctx rcontext.RequestContext, user api.UserInfo) interface{} {
+	params := mux.Vars(r)
+
+	hash := params["blurhash"]
+
+	width := rctx.Config.Features.MSC2448Blurhash.MaxRenderWidth / 2
+	height := rctx.Config.Features.MSC2448Blurhash.MaxRenderHeight / 2
+
+	wstr := r.URL.Query().Get("width")
+	hstr := r.URL.Query().Get("height")
+
+	if wstr != "" {
+		i, err := strconv.Atoi(wstr)
+		if err != nil {
+			return api.BadRequest("width must be an integer")
+		}
+		width = i
+	}
+	if hstr != "" {
+		i, err := strconv.Atoi(hstr)
+		if err != nil {
+			return api.BadRequest("height must be an integer")
+		}
+		height = i
+	}
+
+	if width > rctx.Config.Features.MSC2448Blurhash.MaxRenderWidth {
+		width = rctx.Config.Features.MSC2448Blurhash.MaxRenderWidth
+	}
+	if height > rctx.Config.Features.MSC2448Blurhash.MaxRenderHeight {
+		height = rctx.Config.Features.MSC2448Blurhash.MaxRenderHeight
+	}
+
+	img, err := blurhash.Decode(hash, width, height, rctx.Config.Features.MSC2448Blurhash.Punch)
+	if err != nil {
+		rctx.Log.Error(err)
+		return api.InternalServerError("Unexpected error rendering blurhash")
+	}
+	buf := &bytes.Buffer{}
+	err = png.Encode(buf, img)
+	if err != nil {
+		rctx.Log.Error(err)
+		return api.InternalServerError("Unexpected error rendering blurhash")
+	}
+
+	return &r0.DownloadMediaResponse{
+		ContentType: "image/png",
+		Filename:    "blurhash.png",
+		SizeBytes:   int64(buf.Len()),
+		Data:        util.BufferToStream(buf), // convert to stream to avoid console spam
+	}
+}
diff --git a/api/webserver/webserver.go b/api/webserver/webserver.go
index 8b8506ffa7f75424c6ee946ac8d4516679ed4329..f2fa2f0f03b3505b63ff5c528a15281c56e5cc30 100644
--- a/api/webserver/webserver.go
+++ b/api/webserver/webserver.go
@@ -74,6 +74,7 @@ func Init() *sync.WaitGroup {
 	appendToImportHandler := handler{api.RepoAdminRoute(custom.AppendToImport), "append_to_import", counter, false}
 	stopImportHandler := handler{api.RepoAdminRoute(custom.StopImport), "stop_import", counter, false}
 	versionHandler := handler{api.AccessTokenOptionalRoute(custom.GetVersion), "get_version", counter, false}
+	blurhashRenderHandler := handler{api.AccessTokenRequiredRoute(unstable.RenderBlurhash), "render_blurhash", counter, false}
 
 	routes := make(map[string]route)
 	// r0 is typically clients and v1 is typically servers. v1 is deprecated.
@@ -140,6 +141,7 @@ func Init() *sync.WaitGroup {
 
 	if config.Get().Features.MSC2448Blurhash.Enabled {
 		routes[features.MSC2448UploadRoute] = route{"POST", uploadHandler}
+		routes[features.MSC2448AltRenderRoute] = route{"GET", blurhashRenderHandler}
 	}
 
 	for routePath, route := range routes {
diff --git a/config.sample.yaml b/config.sample.yaml
index 4b0f87f5bcc298b7fbcb7f3eb05525f517f29647..74c207757413688b46848643f224ead3ce2253da 100644
--- a/config.sample.yaml
+++ b/config.sample.yaml
@@ -402,7 +402,8 @@ featureSupport:
     # Whether or not this MSC is enabled for use in the media repo
     enabled: false
 
-    # Maximum dimensions for converting a blurhash to an image
+    # Maximum dimensions for converting a blurhash to an image. When no width and
+    # height options are supplied, the default will be half these values.
     maxWidth: 1024
     maxHeight: 1024