Unofficial, zero-dependency, type-safe pCloud SDK for modern JavaScript runtimes.
A modern alternative to pcloud-sdk-js with full TypeScript support, and no runtime dependencies.
pcloud-sdk-js |
pcloud-kit |
|
|---|---|---|
| Runtime deps | isomorphic-fetch, form-data, invariant |
none |
| TypeScript | untyped JS | strict, exactOptionalPropertyTypes |
| Module format | CJS | ESM only, sideEffects: false |
| Progress | XHR events | stream-based byte counts |
| Errors | raw rejected JSON | typed PcloudApiError / PcloudNetworkError |
| API style | callback or promise | promise only |
| Environments | Node + browser | Node + browser (platform-neutral build) |
| EU/US failover | manual | automatic on 5xx |
- Node
>=22.18.0— relies on globalfetch,Writable.toWeb,openAsBlob,Array.prototype.toSorted - Browser — any modern browser with
fetch,FormData, andReadableStream
npm install pcloud-kit
# pnpm add pcloud-kit / yarn add pcloud-kit / bun add pcloud-kitimport { createClient } from 'pcloud-kit'
const client = createClient({ token: process.env.PCLOUD_TOKEN! })
const root = await client.listfolder(0)
for (const entry of root.contents ?? []) {
// TypeScript narrows entry to FolderMetadata | FileMetadata via isfolder
console.log(entry.isfolder ? '[dir]' : '[file]', entry.name)
}import { buildAuthorizeUrl, getTokenFromCode } from 'pcloud-kit/oauth'
// Step 1: redirect the user to pCloud's consent screen
const url = buildAuthorizeUrl({
clientId: process.env.PCLOUD_CLIENT_ID!,
redirectUri: 'https://example.com/callback',
responseType: 'code',
})
// Step 2: in your callback handler, exchange the code for an access token
const { access_token, locationid } = await getTokenFromCode(
code,
process.env.PCLOUD_CLIENT_ID!,
process.env.PCLOUD_APP_SECRET!,
)
const client = createClient({ token: access_token })import { initOauthToken, popup } from 'pcloud-kit/oauth-browser'
// On the main page: opens a popup and wires the callback
initOauthToken({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://example.com/oauth-callback',
receiveToken: (token, locationid) => {
const client = createClient({ token })
},
})
// On the redirectUri page: reads the token from the URL and passes it back
popup()For environments where a redirect URI is impractical, initOauthPollToken uses the poll_token response type and simultaneously polls both EU and US servers:
import { initOauthPollToken } from 'pcloud-kit/oauth-browser'
initOauthPollToken({
clientId: 'YOUR_CLIENT_ID',
receiveToken: (token, locationid) => {
/* … */
},
onError: (err) => {
/* … */
},
})If you have a session token from userinfo (the auth field), use type: 'pcloud' — this switches the auth header from access_token= to auth=:
const client = createClient({ token: sessionToken, type: 'pcloud' })type defaults to 'oauth'.
These are fully typed and bound directly on the client:
| Method | Signature | Returns |
|---|---|---|
userinfo |
() |
Promise<UserInfo> |
listfolder |
(folderid?: number, options?: ListFolderOptions) |
Promise<FolderMetadata> |
createfolder |
(name: string, parentfolderid?: number) |
Promise<FolderMetadata> |
deletefile |
(fileid: number) |
Promise<FileMetadata> |
deletefolder |
(folderid: number, recursive?: boolean) |
Promise<FolderMetadata> |
movefile |
(fileid: number, tofolderid: number) |
Promise<FileMetadata> |
movefolder |
(folderid: number, tofolderid: number) |
Promise<FolderMetadata> |
renamefile |
(fileid: number, toname: string) |
Promise<FileMetadata> |
renamefolder |
(folderid: number, toname: string) |
Promise<FolderMetadata> |
getfilelink |
(fileid: number) |
Promise<string> |
getthumblink |
(fileid: number, options?: ThumbOptions) |
Promise<string> |
sharefolder |
(folderid: number, mail: string, permissions: 'view' | 'edit', message?: string) |
Promise<ShareInfo> |
appshare |
(folderid: number, userid: number, clientid: string) |
Promise<AppShareInfo> |
login |
(params?: Record<string, string>) |
Promise<UserInfo> |
register |
(email: string, password: string, options?: RegisterOptions) |
Promise<number> |
upload |
(source: string | Blob | File, folderid?: number, options?: UploadOptions) |
Promise<{ metadata: FileMetadata; checksums: Checksums }> |
download |
(url: string, options?: DownloadOptions) |
Promise<ReadableStream<Uint8Array>> |
downloadfile |
(fileid: number, destination: string | WritableStream<Uint8Array>, options?: DownloadOptions) |
Promise<FileLocal> |
remoteupload |
(url: string, folderid?: number, options?: RemoteUploadOptions) |
Promise<{ metadata: FileMetadata }> |
getthumbsfileids |
(fileids: number[], receiveThumb: (thumb: ThumbResult) => void, options?: ThumbOptions) |
Promise<ThumbResult[]> |
getthumbslinks |
(fileids: number[], options?: ThumbOptions) |
Promise<ThumbResult[]> |
// From a Blob or File (browser or Node)
const { metadata, checksums } = await client.upload(new Blob(['hello']), folderId, {
onBegin: () => console.log('starting'),
onProgress: ({ loaded, total }) => console.log(loaded, '/', total),
onFinish: () => console.log('done'),
})
// From a filesystem path (Node only)
await client.upload('/path/to/video.mp4', folderId)
// Remote URL — pCloud fetches it server-side, progress is polled automatically
await client.remoteupload('https://example.com/archive.zip', folderId, {
onProgress: ({ all }) => console.log(all.downloaded, '/', all.size),
})Chunked/resumable upload primitives (upload_create, upload_write, upload_save) are available via client.call() — there is no high-level wrapper yet.
// Download a file to disk (Node)
const { path, bytes } = await client.downloadfile(fileId, '/tmp/out.bin')
// Download to any WritableStream (browser or Node)
await client.downloadfile(fileId, writableStream, {
onProgress: ({ loaded, total }) => {
/* … */
},
})
// Get a raw ReadableStream to pipe or consume yourself
const url = await client.getfilelink(fileId)
const stream = await client.download(url)import { PcloudApiError, PcloudNetworkError } from 'pcloud-kit'
try {
await client.listfolder(0)
} catch (err) {
if (err instanceof PcloudApiError) {
// pCloud returned a non-zero result code.
// `err.params` echoes the method's input params with
// known secret/sensitive keys stripped, so it's safe to log.
console.error(err.result, err.method, err.params)
} else if (err instanceof PcloudNetworkError) {
// fetch itself failed (timeout, DNS, etc.). The underlying fetch URL
// may include the auth token as a query param, so `err.message` and
// `err.cause.message` are scrubbed: values of known secret keys are
// replaced with `***`. `err.cause` is a plain `{ name, message }`
// object (not the raw fetch error) to prevent incidental leaks.
console.error(err.status, err.cause)
}
}The error classes can also be imported from pcloud-kit/errors to avoid pulling in the full client.
pCloud's HTTP/JSON API requires authentication parameters to travel as query-string values: ?access_token=, ?auth=, and — for register / login — ?password=. The SDK keeps those values out of its own thrown errors (see Error handling), but anything sitting between your app and *.pcloud.com will see the full URL.
If you operate or pass through any of the following, disable query-string capture (or scrub these keys) so tokens and passwords don't end up in stored logs:
- reverse proxies / load balancers (nginx
$request, Envoy access logs, ALB/CloudFront request logs) - APM and tracing (OpenTelemetry HTTP spans, Datadog/New Relic/Sentry breadcrumbs that record outbound URLs)
- CI logs that print fetch URLs on failure
The server-side OAuth code exchange (getTokenFromCode) is exempt: client_id, code, and client_secret travel in the POST body (per RFC 6749 §3.2), so they don't appear in URLs.
The convenience methods cover the most common operations. For everything else, call() is constrained to the PcloudMethodName literal union:
// Typed against ~160 known pCloud method names
const stat = await client.call<{ metadata: FileMetadata }>('stat', { fileid: 12345 })
// Escape hatch for unlisted or future methods
const res = await client.callRaw('some_new_method', { param: 'value' })call() also accepts an AbortSignal:
const controller = new AbortController()
await client.call('stat', { fileid: 12345 }, { signal: controller.signal })See the pCloud HTTP API docs for the full method list and parameters.
const client = createClient({
token,
type: 'oauth', // 'oauth' (default) | 'pcloud'
apiServer: 'eapi.pcloud.com', // default EU server; 'api.pcloud.com' for US
useProxy: true, // auto-detect nearest server via getapiserver on init
coalesceReads: false, // disable in-flight deduplication of identical GET reads
})
// Swap the token on an existing client (e.g. after token refresh, or
// when reusing a long-lived client across multi-tenant requests).
// Resets the in-flight read coalesce cache so reads under the old
// token can't be served to a caller that now holds a different token.
client.setToken(newToken)
// Re-detect the nearest server
const server = await client.setupProxy()The client automatically falls back from the EU to the US server (api.pcloud.com) on a 5xx or connection failure — no configuration needed.
// Before
import pCloudSdk from 'pcloud-sdk-js'
const client = pCloudSdk.createClient(token)
// After
import { createClient } from 'pcloud-kit'
const client = createClient({ token })The method name and promise shape are identical. You now get discriminated union types for free:
const folder = await client.listfolder(0, { recursive: true })
for (const entry of folder.contents ?? []) {
if (entry.isfolder) {
entry.folderid // FolderMetadata — TypeScript knows this
} else {
entry.fileid // FileMetadata — TypeScript knows this
}
}// Before — XHR ProgressEvent
client.upload(file, folderId, { onProgress: (event) => event.loaded / event.total })
// After — stream-based byte counts
client.upload(file, folderId, {
onProgress: ({ loaded, total }) => loaded / (total ?? Infinity),
})// Before — raw rejected JSON
.catch((err) => {
if (err.result === 2000) { /* invalid token */ }
})
// After — typed error classes
.catch((err) => {
if (err instanceof PcloudApiError && err.result === 2000) { /* invalid token */ }
})- Chunked/resumable uploads — no high-level wrapper; use
client.call('upload_create', …)etc. directly. - Upload progress on file paths (Node) — progress reflects bytes passing through the stream tee, not HTTP body-upload confirmation events.
pnpm install
pnpm exec playwright install chromium # one-time, for browser tests
pnpm test # vitest — runs all three projects (see below)
pnpm build # tsdown (ESM + .d.ts)The project uses tsdown for bundling, vitest for testing, oxlint for linting, and oxfmt for formatting.
pnpm test runs three Vitest projects:
| Project | Location | Environment | Mocking |
|---|---|---|---|
unit |
test/unit/**/*.test.ts |
Node | fetch stub |
scenario-node |
examples/node/**/*.scenario.test.ts |
Node | MSW (msw/node) |
scenario-browser |
examples/browser/**/*.scenario.test.ts |
Chromium (Playwright) | MSW service worker |
Filter to one project with the matching script:
pnpm test:unit
pnpm test:scenario-node
pnpm test:browserThe unit tests stub fetch directly to assert URL shapes and error handling; the scenario tests drive the SDK end-to-end against MSW handlers shared between Node and the browser.
examples/ holds runnable scenarios that double as integration tests. They share fixtures and MSW handlers so the same flows are exercised from both Node and the browser.
examples/
shared/ fixtures, MSW handler factory, scenario runner
node/ Vitest tests + a tsx-runnable manual script
browser/ Vitest browser-mode tests + a Vite + React demo page
Run the same flows the scenario-node tests cover from the command line:
pnpm scenario:node # MSW-mocked, no network
pnpm scenario:node:real # hits the real pCloud API (see env vars below)For --real mode, set:
| Var | Required | Purpose |
|---|---|---|
PCLOUD_TOKEN |
yes | OAuth access token (sent as ?access_token=) |
PCLOUD_AUTH_TOKEN |
optional | Session token (sent as ?auth=); skipped if unset |
PCLOUD_CLIENT_ID |
optional | OAuth app client id; required for the code-exchange step |
PCLOUD_APP_SECRET |
optional | OAuth app secret; required for the code-exchange step |
PCLOUD_CODE |
optional | Authorization code; required for the code-exchange step |
Steps that lack their required env vars are skipped — you don't have to provide an app secret just to validate listfolder.
pnpm scenario:browser # MSW-mocked, no network
pnpm scenario:browser:real # hits the real pCloud APIBoth commands open http://localhost:5173. A small React + Vite page with one button per flow (oauth listfolder, pcloud-mode listfolder, OAuth poll-token, error path).
In MSW mode the inputs are pre-filled with fixture tokens and every flow works against the mocked handlers.
In :real mode the worker is not started and requests go to the live API. Paste a real token into the input(s) — ?access_token= for the oauth-mode button, ?auth= for the pcloud-mode button. The OAuth poll-token and error-path buttons are disabled because they depend on MSW (the OAuth poll button uses a fake client_id and intercepts window.open; the error button overrides the worker handlers).
MIT