ASP.NET Core icon

ASP.NET Core

Put the render call behind a minimal-API endpoint and your API key never reaches the browser. Imejis returns the finished image directly in the response, so the handler is a single upstream request, with nothing to poll and no webhook to receive.

Set two environment variables first: IMEJIS_API_KEY (from your dashboard) and IMEJIS_DESIGN_ID (any design's id). The fields you override are component.property pairs: headline.text, title.color, image.image; only what you send changes, and a design's overridable fields are listed in its Share / API panel.

Each handler below comes from a real ASP.NET Core app that is booted in Docker and called end to end before this page ships.

An endpoint that returns the generated imageAn endpoint that returns the generated image

A route in your app that renders on demand and streams the image straight back, so your render key never reaches the browser. This is the shape to use behind an <img src> on a page you control. ?format= picks png, jpeg, webp or pdf, and the route answers with the content type that matches it. A PDF labelled image/png is a file the browser, the email client and Slack all refuse to open.

ASP.NET Core: An endpoint that returns the generated image
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
var apiKey = Environment.GetEnvironmentVariable("IMEJIS_API_KEY")!;
var designId = Environment.GetEnvironmentVariable("IMEJIS_DESIGN_ID")!;
var baseUrl = Environment.GetEnvironmentVariable("IMEJIS_RENDER_URL")
    ?? "https://render.imejis.io";
 
var imejis = new HttpClient();
imejis.DefaultRequestHeaders.Add("dma-api-key", apiKey);
 
// The formats the render API accepts. The routes below pass `?format=`
// through when one is asked for; with none, the render host's default (jpeg)
// applies.
string[] formats = ["png", "jpeg", "webp", "pdf"];
 
// POST overrides to the render API and return the raw response.
async Task<HttpResponseMessage> Render(string path, string query, object body)
{
    var url = baseUrl + path + (query.Length == 0 ? "" : "?" + query);
    var content = new StringContent(
        JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    return await imejis.PostAsync(url, content);
}
 
// Render on demand and stream the image straight back. The render key stays on
// the server, so it never reaches the browser — point an <img src> at this.
//
// `?format=` decides what comes back — png · jpeg · webp · pdf — and with no
// format the render host's default (jpeg) applies. Either way the response
// carries whatever type the render API set, passed straight through: a PDF
// labelled `image/jpeg` is a file the browser, the email client and Slack all
// refuse to open.
app.MapGet("/og", async (
    string? title, string? subtitle, string? format, HttpResponse response) =>
{
    var query = formats.Contains(format) ? $"format={format}" : "";
 
    var upstream = await Render($"/v1/{designId}", query, new Dictionary<string, string>
    {
        ["headline.text"] = title ?? "Hello from ASP.NET Core",
        ["subtitle.text"] = subtitle ?? "",
    });
    if (!upstream.IsSuccessStatusCode)
    {
        return Results.Json(
            new { error = $"render failed: {(int)upstream.StatusCode}" }, statusCode: 502);
    }
    var bytes = await upstream.Content.ReadAsByteArrayAsync();
    var type = upstream.Content.Headers.ContentType?.MediaType ?? "image/jpeg";
    // Renders are deterministic, so cache aggressively at the edge/CDN — and
    // every request to the render API is one metered render.
    response.Headers.CacheControl = "public, immutable, no-transform, max-age=31536000";
    return Results.File(bytes, type);
});
 
app.Run();

One endpoint every size and jpeg qualityOne endpoint, every size, and JPEG quality

A multi-size design carries several canvases: an OG image, a square post, a vertical story. ?variation=<id> picks which one the route renders, and omitting it gives the base size, so one endpoint serves every size your pages ask for. ?quality= trades file size for fidelity on JPEG only; png, webp and pdf ignore it.

ASP.NET Core: One endpoint, every size, and JPEG quality
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
var apiKey = Environment.GetEnvironmentVariable("IMEJIS_API_KEY")!;
var designId = Environment.GetEnvironmentVariable("IMEJIS_DESIGN_ID")!;
var baseUrl = Environment.GetEnvironmentVariable("IMEJIS_RENDER_URL")
    ?? "https://render.imejis.io";
 
var imejis = new HttpClient();
imejis.DefaultRequestHeaders.Add("dma-api-key", apiKey);
 
// The formats the render API accepts. The routes below pass `?format=`
// through when one is asked for; with none, the render host's default (jpeg)
// applies.
string[] formats = ["png", "jpeg", "webp", "pdf"];
 
// POST overrides to the render API and return the raw response.
async Task<HttpResponseMessage> Render(string path, string query, object body)
{
    var url = baseUrl + path + (query.Length == 0 ? "" : "?" + query);
    var content = new StringContent(
        JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    return await imejis.PostAsync(url, content);
}
 
// One design, several canvases — an OG image, a square post, a vertical
// story. `?variation=<id>` picks which one you get; with none you get the base
// size. Map YOUR OWN size names to the ids (the design's Share / API panel
// lists them) so a page asks for "story", not for an id.
//
// `?quality=` trades file size for fidelity on JPEG only — png, webp and pdf
// ignore it.
var sizes = new Dictionary<string, string?>
{
    ["og"] = null,
    ["story"] = Environment.GetEnvironmentVariable("IMEJIS_VARIATION_ID") ?? "story",
};
 
app.MapGet("/og-size", async (
    string? size, string? title, string? format, int? quality, HttpResponse response) =>
{
    // A name that is not in the map is an unknown size; one mapped to null is
    // the base canvas, which is a render with no `variation` at all.
    if (!sizes.TryGetValue(size ?? "og", out var variation))
    {
        return Results.Json(new { error = $"unknown size: {size}" }, statusCode: 404);
    }
 
    var query = new List<string>();
    if (variation is not null) query.Add($"variation={variation}");
    if (formats.Contains(format)) query.Add($"format={format}");
    if (quality is >= 1 and <= 100) query.Add($"quality={quality}");
 
    var upstream = await Render($"/v1/{designId}", string.Join("&", query),
        new Dictionary<string, string> { ["headline.text"] = title ?? "Every size" });
    if (!upstream.IsSuccessStatusCode)
    {
        return Results.Json(
            new { error = $"render failed: {(int)upstream.StatusCode}" }, statusCode: 502);
    }
    var bytes = await upstream.Content.ReadAsByteArrayAsync();
    var type = upstream.Content.Headers.ContentType?.MediaType ?? "image/jpeg";
    response.Headers.CacheControl = "public, immutable, no-transform, max-age=31536000";
    return Results.File(bytes, type);
});
 
app.Run();

An endpoint that returns a hosted or signed image urlAn endpoint that returns a hosted or signed image URL

Ask for ?delivery=hosted (a public URL) or ?delivery=signed (a private, expiring one) and hand the URL to the caller instead of the bytes, which is what you want for an email, a webhook payload, or a share card.

ASP.NET Core: An endpoint that returns a hosted or signed image URL
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
var apiKey = Environment.GetEnvironmentVariable("IMEJIS_API_KEY")!;
var designId = Environment.GetEnvironmentVariable("IMEJIS_DESIGN_ID")!;
var baseUrl = Environment.GetEnvironmentVariable("IMEJIS_RENDER_URL")
    ?? "https://render.imejis.io";
 
var imejis = new HttpClient();
imejis.DefaultRequestHeaders.Add("dma-api-key", apiKey);
 
// The formats the render API accepts. The routes below pass `?format=`
// through when one is asked for; with none, the render host's default (jpeg)
// applies.
string[] formats = ["png", "jpeg", "webp", "pdf"];
 
// POST overrides to the render API and return the raw response.
async Task<HttpResponseMessage> Render(string path, string query, object body)
{
    var url = baseUrl + path + (query.Length == 0 ? "" : "?" + query);
    var content = new StringContent(
        JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    return await imejis.PostAsync(url, content);
}
 
// Ask for a stored URL instead of bytes. `hosted` is public and permanent;
// `signed` is private and expires (`expiresIn` is minutes).
app.MapGet("/og-url", async (string? title, string? format, bool @private = false) =>
{
    var delivery = @private ? "signed" : "hosted";
    // The stored URL carries the extension of the format it was rendered in,
    // so pass the format through here too — a recipient that sniffs by
    // extension (an email client, a Slack unfurl) needs `.pdf` to say pdf.
    var chosen = formats.Contains(format) ? $"&format={format}" : "";
    var query = $"delivery={delivery}{chosen}" + (@private ? "&expiresIn=30" : "");
    var upstream = await Render($"/v1/{designId}", query, new Dictionary<string, string>
    {
        ["headline.text"] = title ?? "Shared card",
    });
    var stored = JsonNode.Parse(await upstream.Content.ReadAsStringAsync())!;
    return Results.Ok(new
    {
        url = stored["url"]!.ToString(),
        format = stored["format"]?.ToString(),
        expiresAt = stored["expiresAt"]?.ToString(),
    });
});
 
app.Run();

Bulk image generation from a routeBulk image generation from a route

One POST to /bulk renders many images, a row per record or one per size, and returns a URL for each, in the order you sent them.

ASP.NET Core: Bulk image generation from a route
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
var apiKey = Environment.GetEnvironmentVariable("IMEJIS_API_KEY")!;
var designId = Environment.GetEnvironmentVariable("IMEJIS_DESIGN_ID")!;
var baseUrl = Environment.GetEnvironmentVariable("IMEJIS_RENDER_URL")
    ?? "https://render.imejis.io";
 
var imejis = new HttpClient();
imejis.DefaultRequestHeaders.Add("dma-api-key", apiKey);
 
// The formats the render API accepts. The routes below pass `?format=`
// through when one is asked for; with none, the render host's default (jpeg)
// applies.
string[] formats = ["png", "jpeg", "webp", "pdf"];
 
// POST overrides to the render API and return the raw response.
async Task<HttpResponseMessage> Render(string path, string query, object body)
{
    var url = baseUrl + path + (query.Length == 0 ? "" : "?" + query);
    var content = new StringContent(
        JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    return await imejis.PostAsync(url, content);
}
 
// One POST renders many images and returns a URL for each, in the order sent —
// a row per record, or one per size.
app.MapPost("/cards", async (JsonNode body) =>
{
    var rows = body["rows"]!.AsArray();
    var upstream = await Render("/bulk", "", new Dictionary<string, object>
    {
        ["designId"] = designId,
        ["delivery"] = "hosted",
        ["format"] = "png",
        ["data"] = rows.Select(row => new Dictionary<string, string>
        {
            ["headline.text"] = row!["name"]!.ToString(),
            ["subtitle.text"] = row["role"]!.ToString(),
        }).ToArray(),
    });
    var bulk = JsonNode.Parse(await upstream.Content.ReadAsStringAsync())!;
    return Results.Ok(new
    {
        summary = bulk["summary"]!.ToJsonString(),
        urls = bulk["results"]!.AsArray().Select(r => r!["url"]?.ToString()).ToArray(),
    });
});
 
app.Run();

Using the image url directly in a templateUsing the image URL directly in a template

For a public design you can skip the proxy entirely and point an <img> at the render URL with the field values in the query string. Keep the proxy when the key must stay private. That is the right default.

ASP.NET Core: Using the image URL directly in a template
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
 
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
 
var apiKey = Environment.GetEnvironmentVariable("IMEJIS_API_KEY")!;
var designId = Environment.GetEnvironmentVariable("IMEJIS_DESIGN_ID")!;
var baseUrl = Environment.GetEnvironmentVariable("IMEJIS_RENDER_URL")
    ?? "https://render.imejis.io";
 
var imejis = new HttpClient();
imejis.DefaultRequestHeaders.Add("dma-api-key", apiKey);
 
// The formats the render API accepts. The routes below pass `?format=`
// through when one is asked for; with none, the render host's default (jpeg)
// applies.
string[] formats = ["png", "jpeg", "webp", "pdf"];
 
// POST overrides to the render API and return the raw response.
async Task<HttpResponseMessage> Render(string path, string query, object body)
{
    var url = baseUrl + path + (query.Length == 0 ? "" : "?" + query);
    var content = new StringContent(
        JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
    return await imejis.PostAsync(url, content);
}
 
// For a PUBLIC design you can skip the proxy and point an <img> straight at
// the render URL. Keep the proxy whenever the key must stay private — that is
// the right default, and what /og above is for.
app.MapGet("/card", (string? title) =>
{
    var query = string.Join("&", new[]
    {
        $"dma-api-key={Uri.EscapeDataString(apiKey)}",
        $"headline.text={Uri.EscapeDataString(title ?? "Straight from a URL")}",
    });
    var url = $"{baseUrl}/v1/{designId}?{query}";
    return Results.Content(
        $"<img src=\"{url}\" width=\"1200\" height=\"630\" alt=\"Generated card\" />",
        "text/html");
});
 
app.Run();

The response is the raw image (`image/jpeg` by default). Choose the format with `?format=png|jpeg|webp|pdf` (default jpeg) and set JPEG quality with `?quality=1-100`. Store the result and get a URL back with `?delivery=hosted` (or `?delivery=signed&expiresIn=60`), and render a specific size of a multi-size design with `?variation=<id>`.

ParameterValuesDefaultWhat it does
?format=png · jpeg · webp · pdfjpegOutput image format.
?quality=1–100noneCompression quality for lossy output; ignored for other formats. (JPEG only)
?delivery=stream · hosted · signedstreamstream = raw image bytes (default); hosted = store + return a public URL; signed = store + return a time-limited URL.
?expiresIn=minutes60Signed-URL lifetime in minutes (default 60, max 10080 = 7 days). (delivery=signed only)
?variation=<size id>masterWhich multi-dimension SIZE to render. Omit or use "master" for the base size; an unknown id falls back to the master. (multi-size designs only)
  • PNG (?format=png): Lossless, supports transparency. Largest file.
  • JPEG (?format=jpeg): Smaller, lossy, no transparency. Quality adjustable.
  • WebP (?format=webp): Smallest, supports transparency. Modern browsers.
  • PDF (?format=pdf): Vector page, for print and documents.

See the delivery modes and generate images with the API guides.

The console version of the same calls is on the C# page. Two details the Docker run forced: set Cache-Control on an injected HttpResponse, because endpoint metadata adds no header of its own unless the response-caching middleware is wired up, and read the render with ReadAsByteArrayAsync, which buffers it. Each endpoint hit is one render, 100 a month free, then pricing.