Skip to content

Extension integration guide

Ng Guoyou edited this page Jul 13, 2026 · 5 revisions

Extension integration guide

This guide is for extension maintainers integrating with Save In. End users looking for copy-and-paste configurations should use the integration recipes.

Save In exposes a versioned external API through runtime.onMessageExternal. A cooperating extension can hand a direct URL to Save In's routing and renaming pipeline instead of starting a duplicate browser download.

IDs and capability negotiation

Save In's ID is platform-specific:

  • Chrome Web Store: jpblofcpgfjikaapfedldfeilmpgkedf
  • Firefox: {72d92df5-2aa0-4b06-b807-aa21767545cd}

Use the ID for the browser in which the calling extension is running; do not ship the Firefox ID in a Chrome build or vice versa.

Probe the installed extension before showing or enabling an integration:

const SAVE_IN_ID = "jpblofcpgfjikaapfedldfeilmpgkedf"; // Chrome
// const SAVE_IN_ID = "{72d92df5-2aa0-4b06-b807-aa21767545cd}"; // Firefox

const pong = await browser.runtime.sendMessage(SAVE_IN_ID, { type: "PING" });
if (pong?.type !== "PONG" || !pong.body?.capabilities?.includes("download")) {
  throw new Error("This Save In installation does not expose the Download API");
}

The current protocol version is 1. Additive behavior is advertised through capabilities. Do not assume a feature merely from the version number. Current builds advertise sender_allowlist, meaning that discovery succeeds for installed extensions but DOWNLOAD remains default-deny until the user authorizes the caller.

User authorization

An integration cannot authorize itself. Before its first DOWNLOAD, instruct the user to add the calling extension's exact ID under Save In → Advanced → External integrations → Allowed extension IDs. Display browser.runtime.id in your own integration UI or documentation so the user can copy it; do not confuse this caller ID with Save In's destination ID.

The list is matched by exact, trimmed lines. If Save In returns UNAUTHORIZED, show a setup action with the caller ID and stop automatic retries. Authorization is checked before active-tab lookup and before the download pipeline runs. Save In also notifies the user and lists non-private rejected callers in Options with an Add action, so a legacy integration can be approved without manually copying its ID.

Hand off a direct URL

const response = await browser.runtime.sendMessage(SAVE_IN_ID, {
  type: "DOWNLOAD",
  body: {
    version: 1,
    url: item.url,
    comment: "your-extension-id",
    info: {
      pageUrl: tab.url,
      srcUrl: item.url,
      linkText: item.label,
      selectionText: selection,
      suggestedFilename: item.filename,
    },
  },
});

if (response?.body?.status !== "OK") {
  throw new Error(response?.body?.message || "Save In rejected the hand-off");
}

Use a stable, namespaced comment; users can target it with a comment: routing rule. Metadata is optional, but pageUrl, srcUrl, link text, and a trustworthy suggested filename improve routing and naming.

status: "OK" means the browser accepted the Save In download request. Completion, interruption, retry, history, and notifications happen asynchronously inside Save In.

Static commands and the active tab

If your extension can send configured JSON but cannot interpolate the current URL, check for the active_tab capability and send:

await browser.runtime.sendMessage(SAVE_IN_ID, {
  type: "DOWNLOAD",
  body: { version: 1, target: "activeTab", comment: "your-extension-id" },
});

Save In uses the sender's tab when available; background callers resolve the active tab in the last-focused window. An explicit url always takes precedence.

Batches

Send one DOWNLOAD request per URL. Use a small concurrency limit or sequential requests rather than flooding the service worker. Each successful response acknowledges only that item. Keep item-level failures visible to the user and do not retry protocol errors indefinitely.

Download ownership

Choose exactly one owner:

  • To use Save In's routing, retries, history, and notifications, hand the URL to Save In and do not start your own download.
  • To retain your extension's queue, conversion, or native-companion workflow, keep the download and do not expect Save In to adopt it.

Save In deliberately excludes downloads initiated by other extensions from its owned-download state. Starting both workflows creates duplicates.

URL and generated-content limits

Accepted schemes are http, https, ftp, data, and blob. Reject or handle file, javascript, browser-internal, and extension URLs yourself.

A direct HLS/DASH manifest URL is not equivalent to a merged or converted media file. Keep segmented streaming, request-context reconstruction, conversion, and native-companion jobs in the extracting extension. Only hand off a direct resource URL that Save In can fetch as a normal browser download.

Large screenshots, complete-page archives, and other generated artifacts should not be forced through giant data: messages. Keep those in the generating extension until a future API explicitly supports generated-content transfer. A blob: URL may also be scoped to its creating extension and inaccessible elsewhere.

Trust and errors

The browser can deliver external messages from installed extensions, but Save In accepts DOWNLOAD only when the browser-authenticated sender.id exactly matches the user's allowlist. The allowlist is empty by default. Message shape and URL scheme are still validated after authorization. Web pages cannot call this API because Save In does not declare externally_connectable page origins.

Download errors use UNAUTHORIZED, BAD_REQUEST, or INVALID_URL; unknown external message types return UNKNOWN_TYPE. Treat them as terminal for the submitted request and show enough context for the user to correct the integration. In particular, do not retry UNAUTHORIZED until the user changes Save In's allowlist. Save In records only the rejected caller ID, request kind, count, and time for review; it does not retain the submitted URL.

Clone this wiki locally