@dropthis/node
Official Node.js SDK for dropthis -- the publish layer between AI and the internet. One API call in, one URL out.
Install
npm install @dropthis/node
Quick start
import { Dropthis } from "@dropthis/node";
const dropthis = new Dropthis({ apiKey: "sk_..." });
const { data, error } = await dropthis.drops.publish("<h1>Hello</h1>");
console.log(data.url);
console.log(data.id);
Sharing with an agent? Use data.rawUrl. The canonical url always renders a
branded human view; rawUrl is the drop's exact bytes (single-file drops only — null
otherwise). See Canonical URL vs raw bytes.
Keep the drop_… id. publish() never takes an id — every call creates a NEW drop.
To change something already published, pass the id from the publish response to
drops.updateContent() (the files at the URL) or drops.updateSettings() (title,
visibility, expiry, …). Lost the id? Recover it from the URL with drops.resolve().
Usage
Publish an HTML string
const { data } = await dropthis.drops.publish("<h1>Launch page</h1>");
Publish a file
const { data } = await dropthis.drops.publish("./report.html");
Publish a directory
const { data } = await dropthis.drops.publish("./dist");
Publish with options
const { data } = await dropthis.drops.publish("./dist", {
title: "Q4 Report",
visibility: "unlisted",
noindex: true,
expiresAt: "2026-12-31T00:00:00Z",
});
Update the content of an existing drop
const created = await dropthis.drops.publish("./dist", { title: "v1" });
const updated = await dropthis.drops.updateContent(created.data.id, "./dist-v2", {
ifRevision: created.data.revision,
});
Update settings only
await dropthis.drops.updateSettings("drop_abc123", { title: "New title" });
Resolve a URL back to its drop
Lost the drop_… id? Resolve any public locator — a drop URL, a custom-domain URL, or a bare
vanity/shared slug — back to the drop. The target is sent to the server (POST /drops/resolve),
which owner-scopes and decomposes it; you get the full drop back, or null when nothing of yours
matches. Passing a drop_… id round-trips to an owner-scoped lookup.
Persist the drop_… id. URLs, raw_url, and slugs are locators, not identifiers — a vanity slug is
renameable and the pool host rotates, so a stored URL can drift; the id never moves. Treat
drop_… as an opaque case-sensitive string.
const { data } = await dropthis.drops.resolve("https://my-report.dropthis.app/");
if (data) {
console.log(data.id);
} else {
}
Read back what a drop is serving
drops.getContent() is the owner-side read-back (it works regardless of any viewer
password). By default it returns a JSON manifest of the current deployment's files;
pass path to download one file's exact stored bytes.
const manifest = await dropthis.drops.getContent("drop_abc123");
console.log(manifest.data.files);
const file = await dropthis.drops.getContent("drop_abc123", { path: "index.html" });
console.log(file.data.contentType);
console.log(file.data.text());
const old = await dropthis.drops.getContent("drop_abc123", {
deploymentId: "dep_xyz789",
path: "index.html",
});
Canonical URL vs raw bytes (rawUrl)
Every drop has two faces. The canonical data.url always serves a branded human
view — that is how the dropthis badge is guaranteed without sniffing who's asking.
For a single non-HTML file (renderMode: "file_viewer") that view is a branded
preview (image inline, text/markdown/JSON/CSV/code as escaped source, opaque binary as
a download); for a multi-file bundle with no HTML entry it's a branded index. An HTML
drop (renderMode: "user_html") renders the page itself.
When you need the underlying file's exact bytes (e.g. one agent handing an artifact
to another), use data.rawUrl — the drop's bytes at their natural path under the mount.
It's populated only for single-file (file_viewer) drops and is null for
user_html and collections (the page is itself the artifact / per-file natural paths
come from the manifest). Hand url to humans and rawUrl to agents.
const { data } = await dropthis.drops.publish("./notes.md");
console.log(data.url);
console.log(data.rawUrl);
console.log(data.renderMode);
To stream bytes through the SDK regardless of drop kind (and owner-only, so it works even
on password-protected drops), use drops.getContent() —
that's the programmatic byte-fetch path; rawUrl is the public, shareable one.
Safe concurrent edits with ifRevision
Every drop response carries a revision. Pass it back as ifRevision on
updateContent() / updateSettings() to make the update conditional: if someone else
changed the drop in between, the API answers 409 instead of clobbering, and the error
exposes the server's currentRevision so you can re-read and retry.
const drop = await dropthis.drops.get("drop_abc123");
const result = await dropthis.drops.updateContent(drop.data.id, "./dist-v2", {
ifRevision: drop.data.revision,
});
if (result.error?.statusCode === 409) {
console.log("Drop changed underneath us; server is at revision", result.error.currentRevision);
}
Supported inputs
The drops.publish() and drops.updateContent() methods accept:
- HTML/text string --
"<h1>Hello</h1>" (auto-detected as inline content)
- File path --
"./report.html" (local file)
- Directory --
"./dist" (local directory, bundled)
- Array of paths --
["./dist", "./extra.css"] (multi-path bundle)
- URL object --
new URL("https://example.com/page") (source fetch)
- Bytes --
new Uint8Array(...) (raw bytes)
- Explicit content --
{ kind: "content", content: "...", contentType?: "text/html", path?: "page.html" }
- Source URL --
{ kind: "source_url", sourceUrl: "https://example.com/page" } (the server fetches it; an HTML page becomes a site, any other file becomes a single-file drop with a rawUrl)
- File bundle --
{ kind: "files", files: [{ path, content? | contentBase64? | bytes? | sourceUrl?, contentType? }], entry? }. Give each file its bytes inline (content/contentBase64/bytes) or a sourceUrl for the server to fetch — never both on one file. Mix them freely in one bundle.
Drop settings (title, visibility, password, noindex, expiresAt, metadata) go in the second options argument, not in the input object.
A source_url to a non-HTML file now becomes a single-file drop. Point a top-level source URL (new URL(...) or { kind: "source_url" }) at an image, PDF, or text file and you get a single-file file_viewer drop with rawUrl set — the same as publishing those bytes directly. Previously a source URL only worked for HTML pages.
All inputs are uploaded through staged presigned URLs — one signed PUT per file, up to 5 files in parallel. The SDK handles this transparently.
Explicit input examples
await dropthis.drops.publish({
kind: "content",
content: "<h1>Hello</h1>",
contentType: "text/html",
});
await dropthis.drops.publish({
kind: "source_url",
sourceUrl: "https://example.com/report",
});
await dropthis.drops.publish(
{
kind: "files",
files: [
{ path: "index.html", content: "<h1>Hello</h1>" },
{ path: "style.css", content: "body { margin: 0; }" },
],
entry: "index.html",
},
{ title: "My Site" },
);
await dropthis.drops.publish({
kind: "files",
files: [
{
path: "index.html",
content: '<h1>Gallery</h1><img src="hero.png"><img src="logo.svg">',
},
{ path: "hero.png", sourceUrl: "https://cdn.example.com/hero.png" },
{ path: "logo.svg", sourceUrl: "https://cdn.example.com/logo.svg" },
],
entry: "index.html",
});
Prepare (validate without sending)
prepare() resolves and validates the input locally, returning the prepared request object without making any API calls. It throws PublishInputError on invalid input (e.g. missing file).
import { Dropthis, PublishInputError } from "@dropthis/node";
try {
const prepared = await dropthis.prepare("./dist");
console.log("Ready to publish:", prepared.kind);
} catch (e) {
if (e instanceof PublishInputError) {
console.error("Bad input:", e.message);
}
}
Error handling
All methods return DropthisResult<T> -- either { data: T, error: null, headers } or { data: null, error, headers }. API errors never throw; check error before using data.
const result = await dropthis.drops.get("drop_abc123");
if (result.error) {
console.error(result.error.code, result.error.message);
} else {
console.log(result.data);
}
Local input validation errors (e.g. file_not_found) are also returned as { error: { code: "file_not_found", ... } } rather than thrown -- except for prepare(), which throws PublishInputError.
Configuration
const dropthis = new Dropthis({
apiKey: "sk_...",
baseUrl: "https://...",
timeoutMs: 30_000,
uploadTimeoutMs: 120_000,
fetch: customFetch,
});
You can also pass just the API key as a string:
const dropthis = new Dropthis("sk_...");
Resources
drops
await dropthis.drops.list({ limit: 20 });
await dropthis.drops.list({ domain: "reports.example.com" });
await dropthis.drops.get("drop_abc123");
await dropthis.drops.resolve("https://my-report.dropthis.app/");
await dropthis.drops.resolve("drop_abc123");
await dropthis.drops.getContent("drop_abc123");
await dropthis.drops.getContent("drop_abc123", { path: "index.html" });
await dropthis.drops.updateSettings("drop_abc123", { title: "Updated" });
await dropthis.drops.delete("drop_abc123");
List results support auto-pagination:
const page = await dropthis.drops.list();
const allDrops = await page.data.autoPagingToArray({ limit: 100 });
for await (const drop of page.data) {
console.log(drop.url);
}
To change a drop's content, use client.drops.updateContent(dropId, newInput). drops.updateSettings() is for settings only (title, visibility, password, noindex, expiresAt, metadata).
deployments
await dropthis.deployments.list("drop_abc123");
await dropthis.deployments.get("drop_abc123", "dep_xyz789");
uploads
Low-level upload session management. Most users should use publish() instead.
await dropthis.uploads.create({
schemaVersion: 1,
files: [{ path: "index.html", contentType: "text/html", sizeBytes: 1024 }],
});
await dropthis.uploads.get("upl_abc123");
await dropthis.uploads.complete("upl_abc123");
await dropthis.uploads.cancel("upl_abc123");
auth
await dropthis.auth.requestEmailOtp({ email: "you@example.com" });
await dropthis.auth.verifyEmailOtp({ email: "you@example.com", code: "123456" });
await dropthis.auth.logout();
apiKeys
await dropthis.apiKeys.create({ label: "CI" });
await dropthis.apiKeys.list();
await dropthis.apiKeys.delete("key_abc123");
account
const { data } = await dropthis.account.get();
await dropthis.account.update({ displayName: "Jane Doe" });
await dropthis.account.delete();
domains
Custom domains let you serve drops on your own hostname instead of the shared pool. There are two
modes: path (many drops at hostname/{slug}/) and dedicated (one drop at the hostname root).
const { data: domain } = await dropthis.domains.connect({
hostname: "drops.example.com",
mode: "path",
});
const dnsRecord = domain.dns[0];
const { data: verified } = await dropthis.domains.verify("drops.example.com");
const { data: drop } = await dropthis.drops.publish("<h1>Hello</h1>", {
domain: "drops.example.com",
slug: "summer-sale",
});
Other domain operations:
await dropthis.domains.list();
await dropthis.domains.get("drops.example.com");
await dropthis.domains.update("bio.example.com", { dropId: "drop_abc123" });
await dropthis.domains.update("drops.example.com", { default: true });
await dropthis.domains.delete("drops.example.com");
Pricing tiers
- Free — drops expire after 7 days, 5 MB per drop, 500 MB active storage, dropthis badge.
- Pro — drops never expire, 100 MB per drop, 10 GB storage, no badge, 1 custom domain, password-protected drops. Pro is currently invite-only. Learn more at https://dropthis.app/pricing.
account.get().data.limits reflects your active tier.
Cloudflare Workers (edge)
Use the fs-free entry point for Cloudflare Workers and other edge runtimes. It does not import node:fs, node:path, or node:crypto.
import { DropthisEdge } from "@dropthis/node/edge";
const dropthis = new DropthisEdge({ apiKey: env.DROPTHIS_API_KEY });
const { data, error } = await dropthis.drops.publish("<h1>Hello from the edge</h1>");
DropthisEdge accepts the in-memory subset of PublishInput: inline strings, Uint8Array, URL, and the explicit { kind: "content" }, { kind: "source_url" }, and { kind: "files" } forms. Local file paths and string[] path arrays are not supported (no filesystem on the edge).
DropthisEdge exposes the drop lifecycle through drops.publish(input, options?), drops.updateContent(dropId, input, options?), drops.updateSettings, drops.get, drops.list, drops.resolve, drops.getContent, and drops.delete, plus the deployments, account, apiKeys, and domains resource accessors — the same surface as the Node client.
Types
Key types exported from the package:
import type {
AccountLimits,
AccountResponse,
DropthisClientOptions,
DropthisResult,
DropthisErrorResponse,
DropResponse,
DropDeploymentResponse,
DropOptions,
DeploymentContentManifest,
DeploymentContentFile,
DropContentFile,
GetContentOptions,
PrepareOptions,
RequestControls,
PublishOptions,
PublishInput,
PublishFileInput,
ListPage,
CreateUploadSessionRequest,
CreateUploadSessionResponse,
} from "@dropthis/node";
Agent skills
For AI coding agents (Cursor, Claude Code, Windsurf, etc.), install the dropthis-skills package:
npx skills add dropthis-dev/dropthis-skills
Links