Top-text meme API

Image effects APIs

Explode — photo to explosion GIF

POST https://trippn.lol/api/v1/memes/explode with Bearer authentication and multipart fields image and text (both required). Caption: 1–500 characters; long text may be rejected if it cannot fit. The image holds briefly, progressively bulges for 420 milliseconds, flashes, then cuts to the explosion clip with your uppercase white outlined caption kept on top. Output is a looping image/gif.

PNG/JPEG/WebP/GIF accepted; only the first frame is used for this template. Scales to fit 448 × 448, keeping the image aspect ratio. Both dimensions must remain at least 64 pixels after scaling. Transparent intro areas are preserved, but the explosion footage is opaque. Normal 10 MiB upload and 16-megapixel limits apply. Explode is a standalone template, not a chain step. It has no intensity control.

// Java/JDA: reuse source attachment bytes and key from the example below.
RequestBody form = new MultipartBody.Builder().setType(MultipartBody.FORM)
    .addFormDataPart("text", "MY FRIEND EXPLODES")
    .addFormDataPart("image", "upload", RequestBody.create(
        MediaType.parse("application/octet-stream"), source)).build();
Request request = new Request.Builder()
    .url("https://trippn.lol/api/v1/memes/explode")
    .header("Authorization", "Bearer " + key).post(form).build();

Effect chaining

POST https://trippn.lol/api/v1/memes/chain with the same Bearer key. Send multipart image and effects, where effects is a JSON-encoded array. Steps run in array order. Choose 1–4 steps; repeats are allowed. Each step has its own intensity (default 60) and distortion mode (default bulge). An optional caption step must be last and counts toward the four-step limit. It is rendered after all effects, so even shake leaves the caption fixed. Caption steps use style/text/bottom_text instead of intensity/mode.

[
  {"effect":"distort","intensity":75,"mode":"swirl"},
  {"effect":"deepfry","intensity":60},
  {"effect":"shake","intensity":40},
      {"effect":"caption","style":"header","text":"When the server finally works"}
]

Still input returns PNG unless any step is shake, which produces a looping 12-frame GIF. GIF input retains its timing and loop settings. Transparency is preserved subject to GIF’s binary transparency. Effects run on each decoded frame before final encoding. A chain counts as one request and one successful render.

Existing upload, frame and output limits apply, plus a cumulative 32-million processing-pixel limit across every step/frame. Oversized chains return 400; shorten the GIF or use fewer steps. Each step scales to fit 640 × 640 before processing; ASCII can change the dimensions. Step order therefore matters.

JSON Schema for the effects array

{
  "$schema":"https://json-schema.org/draft/2020-12/schema",
  "type":"array",
  "minItems":1,
  "maxItems":4,
  "items":{
    "oneOf":[
      {
        "type":"object","additionalProperties":false,"required":["effect"],
        "properties":{
          "effect":{"enum":["glitch","ascii","distort","shake","jpeg","deepfry"]},
          "intensity":{"type":"integer","minimum":1,"maximum":100,"default":60},
          "mode":{"enum":["bulge","pinch","swirl"],"default":"bulge"}
        }
      },
      {
        "type":"object","additionalProperties":false,"required":["effect","text"],
        "properties":{
          "effect":{"const":"caption"},
          "style":{"enum":["header","overlay"],"default":"header"},
          "text":{"type":"string","minLength":1,"maxLength":500},
          "bottom_text":{"type":"string","maxLength":500,"default":""}
        }
      }
    ]
  }
}

Additional server validation: caption must be last, text must be nonblank, and nonblank bottom_text requires overlay style. GIF caption colors may be slightly quantized during encoding. The white-header style adds height; overlay draws directly on the finished image.

Java/JDA example: reuse the existing attachment download and binary response handling. source is the downloaded image; the JSON goes in one multipart text field, not the entire request body.

String effects = """
    [
      {"effect":"distort","intensity":75,"mode":"swirl"},
      {"effect":"deepfry","intensity":60},
      {"effect":"shake","intensity":40}
    ]
    """;
RequestBody form = new MultipartBody.Builder().setType(MultipartBody.FORM)
    .addFormDataPart("effects", effects)
    .addFormDataPart("image", "upload", RequestBody.create(
        MediaType.parse("application/octet-stream"), source)).build();
Request request = new Request.Builder()
    .url("https://trippn.lol/api/v1/memes/chain")
    .header("Authorization", "Bearer " + key).post(form).build();

POST to https://trippn.lol/api/v1/memes/ followed by glitch, ascii, distort, shake, jpeg, or deepfry. All use your existing Bearer key and shared 30-request-per-minute quota.

Send multipart image (required), intensity (optional integer 1–100, default 60), and mode (distort only: bulge/pinch/swirl, default bulge). No caption is required. ASCII intensity controls character detail. Use the chain endpoint above to combine effects.

Input formats and upload/GIF safety limits match the caption API. Effects scale frames to at most 640 × 640 without enlarging. ASCII uses an aspect-adjusted character grid capped at 100 columns and 100 rows. GIF input returns GIF with original timing and loop settings. Other inputs return PNG, except shake, which returns GIF. Transparent areas are retained. GIF supports fully transparent or opaque pixels only; partially transparent edges are thresholded. The header style still adds an opaque white header.

JSON Schema — effect settings

This validates bot-side settings; serialize these fields as multipart form fields alongside the binary attachment. It is not a JSON HTTP body. The operation determines the URL.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["operation"],
  "properties": {
    "operation": {"enum": ["glitch", "ascii", "distort", "shake", "jpeg", "deepfry"]},
    "intensity": {"type": "integer", "minimum": 1, "maximum": 100, "default": 60},
    "mode": {"enum": ["bulge", "pinch", "swirl"], "default": "bulge"}
  }
}

Example bot settings:

{"operation":"distort","intensity":75,"mode":"swirl"}

Java/JDA: reuse the attachment download and response handling from the example below. Replace its request/form construction with:

RequestBody form = new MultipartBody.Builder().setType(MultipartBody.FORM)
    .addFormDataPart("intensity", "75")
    .addFormDataPart("mode", "swirl")
    .addFormDataPart("image", "upload", RequestBody.create(
        MediaType.parse("application/octet-stream"), source)).build();
Request request = new Request.Builder()
    .url("https://trippn.lol/api/v1/memes/distort")
    .header("Authorization", "Bearer " + key).post(form).build();

Success: HTTP 200 with binary image/png or image/gif. Errors use the JSON error schema below; invalid parameters return 400. Read Content-Type to choose the attachment extension. Existing caption requests remain unchanged.

Upload an image and caption. Receive black Anton text on a white header above the full image. GIF animation timing and loop settings are preserved.

Test uploads and manage keys (panel login required).

POST https://trippn.lol/api/v1/memes/top-text
Authorization: Bearer YOUR_API_KEY
Content-Type: multipart/form-data

image: PNG, JPEG, still WebP, or GIF
text: 1–500 characters

Upload limit: 10 MiB, 16 megapixels per frame. GIF limits: 200 frames, 40 million cumulative input pixels and 24 million cumulative output pixels. Long or large animations may hit the pixel limit first. Still images fit 1600 × 2400 and GIFs fit 800 × 1200 before adding the caption. Output limit: 10 MiB. Tiny images are enlarged for readability. Metadata is removed. Animated PNG/WebP are not supported. Latin text works best; emoji and other scripts are not guaranteed.

Output: image/png / meme.png for still images; image/gif / meme.gif for GIFs.

Meme styles

style=header (default): the original black caption in a white header. Existing requests work unchanged.

style=overlay: uppercase white text with a black outline, placed directly on the image at the top and bottom. No border or header is added, and the image is not cropped. Large images are scaled within the existing limits. Text wraps and shrinks to fit; captions that cannot fit are rejected. Both styles support GIFs.

# Multipart fields for a classic meme
style: overlay
text: THIS WILL BE YOU
bottom_text: WHEN I GET YOU
image: [binary image or GIF file]

text remains the required top caption. bottom_text is optional, up to 500 characters, and only accepted with overlay style.

Request and response schema

The request is multipart/form-data, not JSON. Both fields are required. Send the image as binary file data, not a URL or base64 string. Let your HTTP library generate the multipart boundary. Use Authorization: Bearer YOUR_API_KEY; the key must be active.

# OpenAPI 3.0-style request schema
requestBody:
  required: true
  content:
    multipart/form-data:
      schema:
        type: object
        required: [image, text]
        properties:
          style:
            type: string
            enum: [header, overlay]
            default: header
          bottom_text:
            type: string
            maxLength: 500
            description: Optional bottom caption; nonblank values require overlay style.
          image:
            type: string
            format: binary
            description: PNG, JPEG, still WebP, or GIF; at most 10485760 bytes.
          text:
            type: string
            minLength: 1
            maxLength: 500
            description: Nonblank caption after trimming; newlines supported.

Captions are trimmed and Windows newlines are normalized. Control characters other than newlines are rejected. Image content is decoded and validated, not trusted based on its filename. The total multipart request limit is 11 MiB; the file itself is limited to 10 MiB. Animation and pixel limits above also apply.

JSON Schema for bot-side request data

Use this schema to validate the bot’s caption/style settings and attachment reference before uploading. This JSON is a bot-side descriptor, not the HTTP request body. The bot must download its Discord attachment, then send those bytes as the multipart image field. The API does not fetch URLs or Discord attachment IDs.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "MemeBotRequest",
  "type": "object",
  "additionalProperties": false,
  "required": ["image", "text"],
  "properties": {
    "image": {
      "type": "object",
      "additionalProperties": false,
      "required": ["attachment_id"],
      "properties": {
        "attachment_id": {
          "type": "string",
          "pattern": "^[0-9]+$",
          "description": "Discord attachment ID resolved by the bot, not the meme API."
        }
      }
    },
    "style": {
      "type": "string",
      "enum": ["header", "overlay"],
      "default": "header"
    },
    "text": {
      "type": "string",
      "minLength": 1,
      "maxLength": 500,
      "description": "Required nonblank top caption; server trims and validates control characters."
    },
    "bottom_text": {
      "type": "string",
      "maxLength": 500,
      "default": ""
    }
  },
  "allOf": [{
    "if": {
      "required": ["bottom_text"],
      "properties": { "bottom_text": { "minLength": 1 } }
    },
    "then": {
      "required": ["style"],
      "properties": { "style": { "const": "overlay" } }
    }
  }]
}

The bot should additionally reject blank captions and enforce the documented file/pixel limits. Schema defaults are annotations; apply the defaults in bot code when fields are omitted.

Example JSON descriptor — classic overlay

{
  "image": { "attachment_id": "123456789012345678" },
  "style": "overlay",
  "text": "THIS WILL BE YOU",
  "bottom_text": "WHEN I GET YOU"
}

Resolve the example attachment ID from the command’s actual attachment. In the Java/JDA example, pass the descriptor’s strings into these multipart fields; source contains the downloaded attachment bytes:

RequestBody form = new MultipartBody.Builder()
    .setType(MultipartBody.FORM)
    .addFormDataPart("style", "overlay")
    .addFormDataPart("text", "THIS WILL BE YOU")
    .addFormDataPart("bottom_text", "WHEN I GET YOU")
    .addFormDataPart("image", "upload", RequestBody.create(
        MediaType.parse("application/octet-stream"), source))
    .build();

Example JSON descriptor — original header

{
  "image": { "attachment_id": "123456789012345678" },
  "style": "header",
  "text": "When the server finally works"
}

Successful response — HTTP 200 (binary, not JSON)

The body contains the raw image bytes, not JSON, a download URL, or base64. Choose the output filename using the response Content-Type. PNG, JPEG and still WebP inputs return PNG; GIF inputs return GIF.

HTTP/1.1 200 OK
Content-Type: image/png
Content-Disposition: attachment; filename=meme.png
Cache-Control: no-store

[binary PNG bytes]

# For GIF input:
Content-Type: image/gif
Content-Disposition: attachment; filename=meme.gif

[binary GIF bytes]
# OpenAPI 3.0-style success response schema
responses:
  '200':
    description: Captioned image file
    content:
      image/png:
        schema:
          type: string
          format: binary
      image/gif:
        schema:
          type: string
          format: binary

Error response schema

Application errors have Content-Type: application/json and the following body. The message is human-readable; use the HTTP status for program logic rather than matching message text.

{
  "type": "object",
  "required": ["error"],
  "additionalProperties": false,
  "properties": {
    "error": { "type": "string" }
  }
}

Example invalid-key response:

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"error":"Invalid or revoked API key."}

Proxy failures (such as 502/504), transport failures and unexpected server errors may not return this JSON shape. Check the status and Content-Type before decoding; handle non-JSON errors gracefully.

Java / JDA

The example below retains the default header style. For overlay memes, add these parts to its MultipartBody.Builder before build():

.addFormDataPart("style", "overlay")
.addFormDataPart("bottom_text", "WHEN I GET YOU")

Java 17+, JDA and OkHttp (used by JDA). Add this listener with jda.addEventListener(new MemeCommand());. Call MemeCommand.register(jda); once to register the command. Store the key in the bot environment as MEME_API_KEY, never in chat. The example defers replies using JDA’s interaction flow.

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.utils.FileUpload;
import okhttp3.*;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class MemeCommand extends ListenerAdapter {
    private static final int LIMIT = 10 * 1024 * 1024;
    private static final OkHttpClient HTTP = new OkHttpClient.Builder()
        .callTimeout(45, TimeUnit.SECONDS).build();

    public static void register(JDA jda) {
        jda.upsertCommand("meme", "Caption an image or GIF")
            .addOption(OptionType.ATTACHMENT, "image", "Image or GIF", true)
            .addOption(OptionType.STRING, "text", "Caption", true).queue();
    }

    @Override
    public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
        if (!event.getName().equals("meme")) return;
        Message.Attachment image = event.getOption("image").getAsAttachment();
        String text = event.getOption("text").getAsString();
        if (image.getSize() > LIMIT || text.isBlank() || text.length() > 500) {
            event.reply("Use an image under 10 MiB and a caption of 1–500 characters.")
                .setEphemeral(true).queue();
            return;
        }
        event.deferReply().queue(hook -> CompletableFuture.runAsync(() -> {
            try {
                Generated result = generate(image, text);
                hook.sendFiles(FileUpload.fromData(result.bytes(), result.filename()))
                    .queue(null, failure -> hook.editOriginal(
                        "Discord could not attach the result. Check the server upload limit.").queue());
            } catch (Exception failure) {
                hook.editOriginal("Could not create the meme. Check the key and image limits, then try again.").queue();
            }
        }));
    }

    private record Generated(byte[] bytes, String filename) {}

    private static byte[] bounded(Response response) throws IOException {
        if (response.body() == null) throw new IOException("Empty response");
        byte[] bytes = response.body().byteStream().readNBytes(LIMIT + 1);
        if (bytes.length > LIMIT) throw new IOException("File too large");
        return bytes;
    }

    private static Generated generate(Message.Attachment image, String text) throws IOException {
        String key = System.getenv("MEME_API_KEY");
        if (key == null || key.isBlank()) throw new IOException("Missing API key");
        byte[] source;
        // Use the Discord attachment URL, never a user-entered URL.
        try (Response download = HTTP.newCall(new Request.Builder().url(image.getUrl()).build()).execute()) {
            if (!download.isSuccessful()) throw new IOException("Download failed");
            source = bounded(download);
        }
        RequestBody form = new MultipartBody.Builder().setType(MultipartBody.FORM)
            .addFormDataPart("text", text)
            .addFormDataPart("image", "upload", RequestBody.create(
                MediaType.parse("application/octet-stream"), source)).build();
        Request request = new Request.Builder()
            .url("https://trippn.lol/api/v1/memes/top-text")
            .header("Authorization", "Bearer " + key).post(form).build();
        try (Response response = HTTP.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("API HTTP " + response.code());
            String type = response.header("Content-Type", "");
            if (!type.startsWith("image/png") && !type.startsWith("image/gif"))
                throw new IOException("Unexpected response type");
            return new Generated(bounded(response),
                type.startsWith("image/gif") ? "meme.gif" : "meme.png");
        }
    }
}

Access and errors

Each key permits 30 requests per minute. Images are processed temporarily and not retained; key names, creation dates, successful render counts and last-use times are stored.

Errors return JSON: {"error":"Explanation"}. HTTP 400: invalid image/text; 401: missing, invalid or revoked key; 413: upload too large; 429: rate limit; 503: busy. Respect Retry-After on 429/503. OkHttp supplies the multipart boundary automatically. The example does not automatically retry or log keys.