Obsidian plugin: call Sume with requestUrl and embed the image
Obsidian's requestUrl skips CORS limits, so a plugin can POST to Sume's /v1/images. Save the file with createBinary and insert an embed. Handle the 202 case.

Use requestUrl instead of fetch. Obsidian's requestUrl reference describes it as making requests without CORS restrictions, and its response has status, json, arrayBuffer and text. That matters here because Sume's API allows browser calls only from Sume's own origins, so a plugin that used fetch from Obsidian's origin would be blocked by the browser's CORS check.
The call itself is the Sume image API: POST https://api.sume.com/v1/images with a bearer key, then data[0].url on a 200.
A command that inserts the image
This is the body of a plugin's onload. It reads the key from the plugin's saved data, calls Sume, downloads the image as bytes, writes it into the vault, and inserts an embed at the cursor.
import { Plugin, requestUrl } from "obsidian";
export default class SumeImage extends Plugin {
async onload() {
this.addCommand({
id: "sume-image",
name: "Insert Sume image",
editorCallback: async (editor) => {
const key = (await this.loadData())?.key;
if (!key) return;
const res = await requestUrl({
url: "https://api.sume.com/v1/images",
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ model: "openai/gpt-image-2.5", quality: "low",
output_format: "png", prompt: editor.getSelection() }),
});
if (res.status !== 200) return;
const file = await requestUrl({ url: res.json.data[0].url });
const path = `sume-${Date.now()}.png`;
await this.app.vault.createBinary(path, file.arrayBuffer);
editor.replaceSelection(`![[${path}]]`);
},
});
}
}What can go wrong?
A 202 has no data array. The command above returns quietly on any status but 200; a real plugin should show a notice and poll the status_url in the job envelope. The jobs guide lists the terminal statuses as completed, failed and canceled.
Pin output_format to png. A plain PNG opens in any tool that reads your vault, and Sume accepts the value on every catalog model except Recraft V4, which returns WebP only.
Watch the key. loadData stores the key as plain JSON in the vault's plugin folder, so keep that folder out of any public sync or repository. Each call bills the key's workspace, and usage.cost on the 200 response tells you the amount in USD.
For longer runs, add a command that takes the whole note as the prompt and one that asks for n variants. The request accepts n from 1 to 10, with lower ceilings on some models, so read the catalog before you promise ten choices in a menu.
Test the command on a short selection first. The prompt is whatever text you have highlighted, so an empty selection sends an empty prompt, and the API rejects an empty prompt as invalid. A one-line guard that returns when the selection is blank saves a confusing error notice. For a rate-limited vault, add a lock so a double-click on the command cannot start two paid generations.
Sources
Related posts
More in Integrations
- Odysee 16 GB upload limit vs a 1800-second Sume source
Odysee caps uploads at 16 GB. A 1800-second Sume source would need about 71 Mbps to reach it. Worked sizes at 8, 25 and 50 Mbps, with a Python check.
- Odysee 8 Mbps bitrate warning and Sume's missing bitrate field
Odysee warns above 8 Mbps and suggests 720p if 1080p at 30 fps runs high. Sume trim has no bitrate field, so use width, height and fps, then probe the average.
- Odysee needs H264 and AAC MP4: what Sume trim exact outputs
Odysee wants MP4 with H264 video and AAC audio, or the video has no sound. Sume trim in exact mode re-encodes to libx264 with AAC audio. Check it with a probe.
- One 1080x1920 master for Meta, TikTok, Pinterest and Shopify limits
One vertical MP4 can pass four platforms: Instagram Feed 1080x1920, TikTok 540x960 minimum, Pinterest 2 GB and 6-15 s, Shopify 1 GB. Limits read 2026-10-05.
Written by Sume