-
Notifications
You must be signed in to change notification settings - Fork 32
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.
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.
An integration cannot authorize itself. Before its first DOWNLOAD, instruct the user to paste the calling extension's exact ID under Save In → Advanced → External integrations → Approved extensions and select Allow. 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 IDs. 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 under Pending approval with an Approve action, so a legacy integration can be approved without manually copying its ID.
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.
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.
Since Save In 4.1, a user rule ending in after: close-tab may close the tab an activeTab request selected, after the browser accepts the save and only when the tab still shows the saved URL. An explicit-URL request never closes any tab.
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's request — the user's routing rules may still exclude it or skip it as unmatched, and download failures surface asynchronously in Save In's History and notifications. Since Save In 4.1, the url echoed in the response is the WHATWG-canonical form of the requested URL, which can differ from the caller's spelling; correlate requests yourself rather than string-comparing the echo. Keep item-level failures visible to the user and do not retry protocol errors indefinitely.
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.
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.
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.