# Pinner.xyz
> Privacy-focused storage without the infrastructure.
import ApiTokenSetup from "../partials/_api-token-setup.mdx";
## Authentication
Pinner uses API tokens for authentication.
### Config File
Store your token in the config file:
```bash
pinner config set auth_token "your-api-token"
```
### CLI Authentication
```bash
# Login with existing account
pinner auth login
# Register a new account
pinner auth register
# Check current auth status
pinner auth status
```
import { HomePage } from "vocs/components";
import SdkInstallation from "../partials/_sdk-installation.mdx";
import CliInstallation from "../partials/_cli-installation.mdx";
## Installation
### Verify Installation
```bash
# SDK
pnpm exec pinner --help
# CLI
pinner --help
```
import { HomePage, Callout } from "vocs/components";
import PinnerDefinition from "../partials/_pinner-definition.mdx";
import Prerequisites from "../partials/_prerequisites.mdx";
import ApiTokenSetup from "../partials/_api-token-setup.mdx";
import SdkInstallation from "../partials/_sdk-installation.mdx";
import GatewayQuickInfo from "../partials/_gateway-quick-info.mdx";
import GatewayAlternatives from "../partials/_gateway-alternatives.mdx";
## Quickstart
Get started with Pinner in 5 minutes. But first, a quick overview\...
**Pinner ≠ Gateway**
Pinner is a pinning service, not a gateway. We ensure your content stays available on decentralized networks, but we don't provide HTTP gateways for content retrieval. [Learn more](/about) or [contact us](/) about custom gateway solutions.
### Step 2: Install the SDK
### Step 3: Upload Your First File
```typescript
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Upload and pin a simple message
const operation = await pinner.upload.json({ message: "Hello, Pinner!" }).pin();
console.log("CID:", operation.result.cid);
```
**What just happened?**
1. You created content (a JSON object)
2. Pinner generated a **CID** (Content Identifier) - a unique address for your content
3. Pinner **pinned** your content, ensuring it stays available on the decentralized network
### Understanding the Result
The `CID` (Content Identifier) is a unique fingerprint for your content. You can use it to:
* Retrieve your content from any IPFS gateway
* Share it with others
* Pin it again in the future
### What Next?
* [Learn about CIDs](/concepts/cids) - Understand content identifiers
* [Upload more file types](/sdk/upload) - Directories, CAR files, and more
* [Explore the CLI](/cli/getting-started) - Command-line interface
* [What is Pinner?](/about) - Product overview and scope
import { CodeGroup } from '@/components';
import { HomePage } from "vocs/components";
import SdkInstallation from "../../partials/_sdk-installation.mdx";
## Pinata Adapter Reference
The Pinata adapter provides drop-in compatibility with the Pinata SDK, allowing you to migrate with minimal code changes.
### Overview
Pinner provides two Pinata adapters:
| Adapter | SDK Version | Status |
| --------------------- | ------------------ | ----------------- |
| `pinataAdapter` | Pinata SDK **2.x** | ✅ Recommended |
| `pinataLegacyAdapter` | Pinata SDK **1.x** | ✅ For legacy code |
:::tip
New projects should use the **v2 adapter** (`pinataAdapter`) which matches the current Pinata SDK. Use the legacy adapter only when maintaining existing code.
:::
### Installation
### Quick Setup
```typescript title="Pinata SDK 2.x"
import { Pinner } from "@lumeweb/pinner";
import { pinataAdapter } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const adapter = pinataAdapter(pinner, {
pinataGateway: "example.mypinata.cloud"
});
```
```typescript title="Pinata SDK 1.x"
import { Pinner } from "@lumeweb/pinner";
import { pinataLegacyAdapter } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const adapter = pinataLegacyAdapter(pinner);
```
For detailed migration steps, see the [Migration Guide](/migration/pinata).
### API Reference
#### Pinata SDK 2.x Adapter
The v2 adapter provides a complete interface matching Pinata SDK 2.x:
```typescript
interface PinataAdapter {
config: PinataConfig;
updateConfig(newConfig: PinataConfig): void;
upload: {
public: PublicUpload;
private: PrivateUpload;
};
files: {
public: PublicFiles;
private: PrivateFiles;
};
gateways: {
public: PublicGateways;
private: PrivateGateways;
};
groups: {
public: PublicGroups;
private: PrivateGroups;
};
analytics: Analytics;
}
```
#### Pinata SDK 1.x Legacy Adapter
The legacy adapter provides a complete interface matching Pinata SDK 1.x:
```typescript
interface PinataLegacyAdapter {
pinFileToIPFS(file: File, options?: UploadOptions): Promise;
pinJSONToIPFS(data: any, options?: UploadOptions): Promise;
pinByHash(cid: string, options?: UploadOptions): Promise;
pinList(query?: FileListQuery): Promise;
unpin(cid: string): Promise<{ message: string }>;
hashMetadata(cid: string, metadata: Record): Promise<{ message: string }>;
}
```
### API Comparison Tables
#### Pinata SDK 2.x → Pinner Adapter
| Pinata SDK 2.x | Pinner with Adapter | Notes |
| ------------------------------------------ | ------------------------------------------- | -------------------- |
| `pinata.upload.public.file(file)` | `adapter.upload.public.file(file)` | ✅ Fully supported |
| `pinata.upload.public.fileArray(files)` | `adapter.upload.public.fileArray(files)` | ✅ Fully supported |
| `pinata.upload.public.json(data)` | `adapter.upload.public.json(data)` | ✅ Fully supported |
| `pinata.upload.public.base64(data)` | `adapter.upload.public.base64(data)` | ✅ Fully supported |
| `pinata.upload.public.cid(cid)` | `adapter.upload.public.cid(cid)` | ✅ Fully supported |
| `pinata.upload.public.url(url)` | `adapter.upload.public.url(url)` | ⚠️ Not supported |
| `pinata.files.public.list()` | `adapter.files.public.list()` | ✅ Fully supported |
| `pinata.files.public.get(id)` | `adapter.files.public.get(id)` | ✅ Fully supported |
| `pinata.files.public.delete([cid])` | `adapter.files.public.delete([cid])` | ✅ Fully supported |
| `pinata.files.public.deletePinRequest(id)` | `adapter.files.public.deletePinRequest(id)` | ✅ Pinner-only method |
#### Pinner Extensions
The following methods extend the standard Pinata SDK interface with Pinner-specific functionality:
| Method | Description |
| ---------------------------------- | ------------------------------------------ |
| `adapter.gateways.public.get(cid)` | Get gateway URL for CID (Pinner extension) |
#### Pinata SDK 1.x → Pinner Legacy Adapter
| Pinata SDK 1.x | Pinner with Legacy Adapter |
| ----------------------------- | ------------------------------------- |
| `pinFileToIPFS(file)` | `adapter.pinFileToIPFS(file)` |
| `pinJSONToIPFS(data)` | `adapter.pinJSONToIPFS(data)` |
| `pinByHash(cid)` | `adapter.pinByHash(cid)` |
| `unpin(cid)` | `adapter.unpin(cid)` |
| `pinList()` | `adapter.pinList()` |
| `hashMetadata(cid, metadata)` | `adapter.hashMetadata(cid, metadata)` |
### Configuration
#### PinataConfig
```typescript
interface PinataConfig {
pinataJwt?: string;
pinataGateway?: string;
pinataGatewayKey?: string;
customHeaders?: Record;
endpointUrl?: string;
uploadUrl?: string;
legacyUploadUrl?: string;
}
```
### Builder Patterns
#### Upload Builder
The v2 adapter uses a builder pattern for uploads with chaining:
```typescript
const upload = await adapter.upload.public.file(file)
.name("my-file.txt")
.keyvalues({ project: "demo", version: "1.0" })
.execute();
console.log("CID:", upload.cid);
console.log("Size:", upload.size);
```
#### Filter Builder
Use the filter builder to list files with specific criteria:
```typescript
const files = await adapter.files.public.list()
.name("my-file")
.limit(10)
.order("DESC")
.all();
for (const file of files) {
console.log(file.name, file.cid);
}
```
See [Code Examples](/migration/examples) for more detailed usage patterns.
### Unsupported Features
The following Pinata SDK features are not currently supported by the adapter:
| Feature | Status | Alternative |
| --------------- | --------------- | ------------------------------ |
| Private uploads | ❌ Not supported | Use Pinner's native API |
| Groups | ❌ Not supported | Use keyvalues metadata instead |
| Analytics | ❌ Not supported | Not available in Pinner |
| Swap CID | ❌ Not supported | Not available in Pinner |
| Signed URLs | ❌ Not supported | Not available in Pinner |
For features not supported by the adapter, consider using [Pinner's native API](/sdk/upload) which provides more control and capabilities.
### Related Documentation
* [Migration Guide](/migration/pinata) - Step-by-step migration instructions
* [Code Examples](/migration/examples) - Comprehensive code examples
* [Pinner API Reference](/sdk/upload) - Native Pinner API documentation
import { CodeGroup } from '@/components';
## SDK Configuration
Configure the SDK with custom options.
### PinnerConfig Interface
```typescript title="Browser"
interface PinnerConfig {
/** API authentication token. Required for all API operations. */
jwt: string;
/** API endpoint URL. Defaults to "https://ipfs.pinner.xyz" */
endpoint?: string;
/** IPFS gateway URL for content retrieval. Defaults to "https://dweb.link" */
gateway?: string;
/** Allowed MIME types for upload. If undefined, all types allowed. */
allowedFileTypes?: string[];
/** Custom fetch implementation. */
fetch?: typeof fetch;
/** Custom datastore instance for Helia. Highest priority - takes precedence over storage and datastoreName. */
datastore?: Datastore;
/** Custom storage instance for both Helia blockstore and datastore. Used when datastore is not provided. */
storage?: Storage;
/** Custom base name for Helia storage. Passed as the `base` option to both blockstore and datastore. Only used when neither datastore nor storage are provided. Defaults to "pinner-helia-data". */
datastoreName?: string;
}
```
```typescript title="Node.js"
interface PinnerConfig {
/** API authentication token. Required for all API operations. */
jwt: string;
/** API endpoint URL. Defaults to "https://ipfs.pinner.xyz" */
endpoint?: string;
/** IPFS gateway URL for content retrieval. Defaults to "https://dweb.link" */
gateway?: string;
/** Allowed MIME types for upload. If undefined, all types allowed. */
allowedFileTypes?: string[];
/** Custom fetch implementation. */
fetch?: typeof fetch;
/** Custom datastore instance for Helia. Highest priority - takes precedence over storage and datastoreName. */
datastore?: Datastore;
/** Custom storage instance for both Helia blockstore and datastore. Used when datastore is not provided. */
storage?: Storage;
/** Custom base name for Helia storage. Passed as the `base` option to both blockstore and datastore. Only used when neither datastore nor storage are provided. Defaults to "pinner-helia-data". */
datastoreName?: string;
}
```
### Endpoint Configuration
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
// Default endpoint (https://ipfs.pinner.xyz)
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Custom endpoint
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN",
endpoint: "https://staging.ipfs.pinner.xyz"
});
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
// Default endpoint (https://ipfs.pinner.xyz)
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
// Custom endpoint
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY,
endpoint: "https://staging.ipfs.pinner.xyz"
});
```
### Custom Gateway
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN",
gateway: "https://ipfs.io"
});
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY,
gateway: "https://ipfs.io"
});
```
### Custom Fetch
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN",
fetch: window.fetch
});
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
import { fetch } from "undici";
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY,
fetch
});
```
### Storage Configuration
The SDK uses Helia for IPFS operations and provides three levels of storage configuration:
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
// Level 1: Default storage (IndexedDB with base name "pinner-helia-data")
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Level 2: Custom base name
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN",
datastoreName: "my-custom-storage"
});
// Level 3: Custom storage instance
import { createStorage } from "unstorage";
import indexeddbDriver from "unstorage/drivers/indexedb";
const storage = createStorage({
driver: indexedbDriver({ base: "my-app" })
});
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN",
storage
});
// Level 4: Custom datastore instance (highest priority)
import { MemoryDatastore } from "interface-datastore";
const datastore = new MemoryDatastore();
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN",
datastore
});
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
// Level 1: Default storage (filesystem in ./.pinner-*)
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
// Level 2: Custom base name
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY,
datastoreName: "my-custom-storage"
});
// Level 3: Custom storage instance
import { createStorage } from "unstorage";
import fsDriver from "unstorage/drivers/fs";
const storage = createStorage({
driver: fsDriver({ base: "./my-storage" })
});
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY,
storage
});
// Level 4: Custom datastore instance (highest priority)
import { MemoryDatastore } from "interface-datastore";
const datastore = new MemoryDatastore();
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY,
datastore
});
```
#### Storage Priority
1. **datastore** (highest) - Custom datastore instance used directly
2. **storage** - Custom unstorage instance for both blockstore and datastore
3. **datastoreName** - Custom base name for default storage
4. **default** - Uses "pinner-helia-data" as base name
### Environment-Specific Setup
```typescript title="development.ts"
export const config = {
jwt: process.env.DEV_PINNER_API_KEY,
endpoint: "https://staging.ipfs.pinner.xyz"
};
```
```typescript title="production.ts"
export const config = {
jwt: process.env.PROD_PINNER_API_KEY,
endpoint: "https://ipfs.pinner.xyz"
};
```
import { CodeGroup } from '@/components';
## Error Handling
Handle errors that may occur during SDK operations.
### Error Types
```typescript title="Browser"
import { Pinner, PinnerError } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
try {
const result = await pinner.upload(file);
} catch (error) {
if (error instanceof PinnerError) {
console.error("Code:", error.code);
console.error("Message:", error.message);
console.error("Retryable:", error.retryable);
console.error("Cause:", error.cause);
}
}
```
```typescript title="Node.js"
import { Pinner, PinnerError } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
try {
const result = await pinner.upload(file);
} catch (error) {
if (error instanceof PinnerError) {
console.error("Code:", error.code);
console.error("Message:", error.message);
console.error("Retryable:", error.retryable);
console.error("Cause:", error.cause);
}
}
```
#### Error Properties
All `PinnerError` instances include:
| Property | Type | Description | |
| ----------- | --------- | ------------------------------------------- | --------------------------------------- |
| `code` | `string` | Unique error code for programmatic handling | |
| `message` | `string` | Human-readable error description | |
| `retryable` | `boolean` | Whether the operation can be retried | |
| `cause` | \`Error | undefined\` | Underlying error that caused this error |
| `name` | `string` | Error class name | |
### Error Classes
| Error Class | Code | Retryable | Description |
| --------------------- | ---------------------- | --------- | ----------------------------------------------------- |
| `PinnerError` | Various | Varies | Base error class for all Pinner errors |
| `ConfigurationError` | `CONFIGURATION_ERROR` | No | Invalid SDK configuration |
| `AuthenticationError` | `AUTHENTICATION_ERROR` | No | Invalid or expired API token |
| `UploadError` | `UPLOAD_ERROR` | Varies | Base class for upload errors |
| `NetworkError` | `NETWORK_ERROR` | Yes | Network connectivity issue |
| `ValidationError` | `VALIDATION_ERROR` | No | Invalid input or validation failure |
| `EmptyFileError` | `EMPTY_FILE_ERROR` | No | Attempted to upload empty file |
| `TimeoutError` | `TIMEOUT_ERROR` | Yes | Request timed out |
| `PinError` | `PIN_ERROR` | Varies | Pin operation failed |
| `NotFoundError` | `NOT_FOUND` | No | Content not found |
| `RateLimitError` | `RATE_LIMIT_EXCEEDED` | Yes | Too many requests |
| `EnvironmentError` | `ENVIRONMENT_ERROR` | No | Unsupported environment (e.g., missing required APIs) |
#### RateLimitError Properties
```typescript
try {
await pinner.upload(file);
} catch (error) {
if (error instanceof RateLimitError) {
console.error("Rate limited, retry after:", error.retryAfter, "seconds");
}
}
```
### Common Error Codes
| Code | Description |
| ---------------------- | ----------------------------------- |
| `CONFIGURATION_ERROR` | Invalid SDK configuration |
| `AUTHENTICATION_ERROR` | Invalid or expired API token |
| `UPLOAD_ERROR` | Upload failed |
| `NETWORK_ERROR` | Network connectivity issue |
| `VALIDATION_ERROR` | Invalid input or validation failure |
| `EMPTY_FILE_ERROR` | Cannot upload empty file |
| `TIMEOUT_ERROR` | Request timed out |
| `PIN_ERROR` | Pin operation failed |
| `NOT_FOUND` | Content not found |
| `RATE_LIMIT_EXCEEDED` | Too many requests |
| `ENVIRONMENT_ERROR` | Unsupported environment |
### Retry Strategies
```typescript title="Browser"
import { Pinner, isRetryable } from "@lumeweb/pinner";
async function uploadWithRetry(file, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await pinner.upload(file);
} catch (error) {
lastError = error;
if (!isRetryable(error)) {
throw error;
}
console.log(`Retry attempt ${attempt}/${maxRetries}`);
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
throw lastError;
}
```
```typescript title="Node.js"
import { Pinner, isRetryable } from "@lumeweb/pinner";
async function uploadWithRetry(file, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await pinner.upload(file);
} catch (error) {
lastError = error;
if (!isRetryable(error)) {
throw error;
}
console.log(`Retry attempt ${attempt}/${maxRetries}`);
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
throw lastError;
}
```
### Handling Network Errors
```typescript title="Browser"
import { Pinner, NetworkError } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
try {
const result = await pinner.upload(file);
} catch (error) {
if (error instanceof NetworkError) {
console.error("Network error. Check your connection.");
return;
}
throw error;
}
```
```typescript title="Node.js"
import { Pinner, NetworkError } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
try {
const result = await pinner.upload(file);
} catch (error) {
if (error instanceof NetworkError) {
console.error("Network error. Check your connection.");
return;
}
throw error;
}
```
import { CodeGroup } from '@/components';
import { HomePage } from "vocs/components";
import Prerequisites from "../../partials/_prerequisites.mdx";
import SdkInstallation from "../../partials/_sdk-installation.mdx";
import UploadCodeExamples from "../../partials/_upload-code-examples.mdx";
## SDK Getting Started
Install and initialize the Pinner SDK.
### Installation
### Basic Initialization
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: "YOUR_API_TOKEN"
});
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY
});
```
### Your First Upload
### TypeScript Configuration
```typescript title="Browser"
import { Pinner, PinnerConfig } from "@lumeweb/pinner";
const config: PinnerConfig = {
jwt: "YOUR_API_TOKEN",
endpoint: "https://api.pinner.xyz"
};
const pinner = new Pinner(config);
```
```typescript title="Node.js"
import { Pinner, PinnerConfig } from "@lumeweb/pinner";
const config: PinnerConfig = {
jwt: process.env.PINNER_API_KEY,
endpoint: "https://api.pinner.xyz"
};
const pinner = new Pinner(config);
```
import { CodeGroup } from '@/components';
import UploadCodeExamples from "../../partials/_upload-code-examples.mdx";
import PinStatusCheck from "../../partials/_pin-status-check.mdx";
## Pin Management
Manage content pins using the Pinner SDK.
### Pin by CID
### List Pins
```typescript title="Browser"
const pins = await pinner.listPins();
for (const pin of pins) {
console.log(pin.cid, pin.status);
}
```
```typescript title="Node.js"
const pins = await pinner.listPins();
for (const pin of pins) {
console.log(pin.cid, pin.status);
}
```
### Get Pin Status
### Check if Content is Pinned
```typescript title="Browser"
const isPinned = await pinner.isPinned("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq");
console.log("Is pinned:", isPinned);
```
```typescript title="Node.js"
const isPinned = await pinner.isPinned("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq");
console.log("Is pinned:", isPinned);
```
### Update Metadata
```typescript title="Browser"
await pinner.setPinMetadata("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq", {
name: "updated-name",
customKey: "custom-value"
});
```
```typescript title="Node.js"
await pinner.setPinMetadata("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq", {
name: "updated-name",
customKey: "custom-value"
});
```
### Unpin
```typescript title="Browser"
await pinner.unpin("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq");
```
```typescript title="Node.js"
await pinner.unpin("bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq");
```
### Unpin by Request ID
Remove a pin using a known request ID. The request ID (`requestid`) is the pinning service's identifier for a specific pin operation:
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Unpin using a known request ID (obtained from your pinning service)
await pinner.unpinByRequestId("your-request-id-here");
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
// Unpin using a known request ID (obtained from your pinning service)
await pinner.unpinByRequestId("your-request-id-here");
```
#### Using with Pinata Adapter
If you're using the Pinata adapter, you can delete pins by CID directly:
```typescript
import { pinataAdapter } from "@lumeweb/pinner";
const adapter = pinataAdapter(pinner, {
pinataGateway: "example.mypinata.cloud"
});
// Delete by CID (adapter handles request ID internally)
await adapter.files.public.delete(["bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq"]);
```
:::info
The request ID (`requestid`) is typically returned by the pinning service when content is pinned. For Pinata, you can use the adapter's `delete()` method which accepts CIDs and handles the request ID lookup internally.
:::
import { CodeGroup } from '@/components';
## Progress Tracking
Monitor the progress of uploads and pin operations.
### UploadOperation Interface
The `upload()` method returns an `UploadOperation` with control methods:
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
const operation = await pinner.upload(file);
// Access the result promise
const result = await operation.result;
console.log("CID:", result.cid);
console.log("Upload ID:", result.id);
// Control the upload
operation.pause(); // Pause (TUS only)
operation.resume(); // Resume (TUS only)
operation.cancel(); // Cancel the upload
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const operation = await pinner.upload(file);
// Access the result promise
const result = await operation.result;
console.log("CID:", result.cid);
console.log("Upload ID:", result.id);
// Control the upload
operation.pause(); // Pause (TUS only)
operation.resume(); // Resume (TUS only)
operation.cancel(); // Cancel the upload
```
#### UploadOperation Properties
| Property | Type | Description |
| ---------- | -------------------------- | --------------------------------------- |
| `result` | `Promise` | Promise resolving when upload completes |
| `progress` | `Readonly` | Current upload progress |
| `cancel()` | `() => void` | Cancel the ongoing upload |
| `pause()` | `() => void` | Pause the upload (TUS protocol) |
| `resume()` | `() => void` | Resume a paused upload (TUS protocol) |
### Progress Tracking
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
const operation = await pinner.upload(file, {
onProgress: (progress) => {
console.log(`Progress: ${progress.percentage.toFixed(2)}%`);
console.log(`Uploaded: ${progress.bytesUploaded} / ${progress.bytesTotal} bytes`);
console.log(`Speed: ${progress.speed} bytes/sec`);
console.log(`ETA: ${progress.eta} seconds`);
}
});
const result = await operation.result;
console.log("Complete:", result.cid);
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const operation = await pinner.upload(file, {
onProgress: (progress) => {
console.log(`Progress: ${progress.percentage.toFixed(2)}%`);
console.log(`Uploaded: ${progress.bytesUploaded} / ${progress.bytesTotal} bytes`);
console.log(`Speed: ${progress.speed} bytes/sec`);
console.log(`ETA: ${progress.eta} seconds`);
}
});
const result = await operation.result;
console.log("Complete:", result.cid);
```
#### UploadProgress Properties
| Property | Type | Description |
| --------------- | -------- | ----------------------------------- |
| `percentage` | `number` | Percentage complete (0-100) |
| `bytesUploaded` | `number` | Number of bytes uploaded |
| `bytesTotal` | `number` | Total bytes to upload |
| `speed` | `number` | Upload speed in bytes per second |
| `eta` | `number` | Estimated time remaining in seconds |
### WaitForOperation
Wait for an operation to complete:
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
const operation = await pinner.upload(file);
// Get the result first
const uploadResult = await operation.result;
// Wait for the pinning operation to complete
const result = await pinner.waitForOperation(uploadResult, {
interval: 1000, // Poll every 1 second
timeout: 60000 // Timeout after 60 seconds
});
console.log("Status:", result);
console.log("CID:", result.cid);
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const operation = await pinner.upload(file);
// Get the result first
const uploadResult = await operation.result;
// Wait for the pinning operation to complete
const result = await pinner.waitForOperation(uploadResult, {
interval: 1000, // Poll every 1 second
timeout: 60000 // Timeout after 60 seconds
});
console.log("Status:", result);
console.log("CID:", result.cid);
```
#### waitForOperation Scenarios
The `waitForOperation` method accepts either an operation ID (number) or an `UploadResult`:
**Scenario 1: Wait with UploadResult (recommended)**
```typescript
const operation = await pinner.upload(file);
const uploadResult = await operation.result;
const result = await pinner.waitForOperation(uploadResult);
```
**Scenario 2: Wait with operation ID from UploadResult**
```typescript
const operation = await pinner.upload(file);
const uploadResult = await operation.result;
const result = await pinner.waitForOperation(uploadResult.operationId);
```
#### Operation Polling Options
| Option | Type | Default | Description |
| --------------- | ---------- | ---------------------------------- | --------------------------------- |
| `interval` | `number` | 2000 | Polling interval in milliseconds |
| `timeout` | `number` | 300000 | Maximum wait time in milliseconds |
| `settledStates` | `string[]` | `["completed", "failed", "error"]` | States considered settled |
import { CodeGroup } from '@/components';
import UploadCodeExamples from "../../partials/_upload-code-examples.mdx";
## Upload Files
Upload various content types to Pinner.
### Basic File Upload
### Upload with Options
```typescript title="Browser"
const result = await pinner.upload(file, {
name: "my-file",
keyvalues: {
customKey: "customValue"
}
});
```
```typescript title="Node.js"
const result = await pinner.upload(file, {
name: "my-file",
keyvalues: {
customKey: "customValue"
}
});
```
### Directory Upload
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
const files = [
new File(["Hello, Pinner!"], "hello.txt"),
new File([JSON.stringify({ name: "config" })], "config.json"),
];
const operation = await pinner.uploadDirectory(files);
const result = await operation.result;
console.log("CID:", result.cid);
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
import fs from "fs";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const files = [
new File([fs.readFileSync("data/hello.txt")], "hello.txt"),
new File([fs.readFileSync("data/config.json")], "config.json"),
];
const operation = await pinner.uploadDirectory(files);
const result = await operation.result;
console.log("CID:", result.cid);
```
### CAR Upload
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
const response = await fetch("/path/to/data.car");
const carData = response.body;
const operation = await pinner.uploadCar(carData);
const result = await operation.result;
console.log("CID:", result.cid);
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
import fs from "fs";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const carData = fs.readFileSync("/path/to/data.car");
const carFile = new File([carData], "data.car", { type: "application/vnd.ipld.car" });
const operation = await pinner.uploadCar(carFile);
const result = await operation.result;
console.log("CID:", result.cid);
```
### Encoders
Choose how content is encoded before upload.
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// JSON encoder
const jsonResult = await pinner.upload.json({ name: "test" });
const result = await jsonResult.result;
// Base64 encoder
const base64Result = await pinner.upload.base64("SGVsbG8sIFBpbm5lciE=");
const base64Upload = await base64Result.result;
// Text encoder
const textResult = await pinner.upload.text("Hello, Pinner!");
const textUpload = await textResult.result;
// CSV encoder
const csvResult = await pinner.upload.csv("name,age\nJohn,30\nJane,25");
const csvUpload = await csvResult.result;
// URL encoder (fetches and uploads)
const urlResult = await pinner.upload.url("https://example.com/data.json");
const urlUpload = await urlResult.result;
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
// JSON encoder
const jsonResult = await pinner.upload.json({ name: "test" });
const result = await jsonResult.result;
// Base64 encoder
const base64Result = await pinner.upload.base64("SGVsbG8sIFBpbm5lciE=");
const base64Upload = await base64Result.result;
// Text encoder
const textResult = await pinner.upload.text("Hello, Pinner!");
const textUpload = await textResult.result;
// CSV encoder
const csvResult = await pinner.upload.csv("name,age\nJohn,30\nJane,25");
const csvUpload = await csvResult.result;
// URL encoder (fetches and uploads)
const urlResult = await pinner.upload.url("https://example.com/data.json");
const urlUpload = await urlResult.result;
```
### Upload Options
Control upload behavior with options:
```typescript title="Browser"
const operation = await pinner.upload(file, {
name: "my-file",
keyvalues: {
project: "demo",
version: "1.0"
},
onProgress: (progress) => {
console.log(`${progress.percentage.toFixed(2)}% complete`);
},
onComplete: (result) => {
console.log("Upload complete:", result.cid);
},
onError: (error) => {
console.error("Upload failed:", error);
},
signal: abortController.signal,
waitForOperation: true,
operationPollingOptions: {
interval: 1000,
timeout: 60000
}
});
```
```typescript title="Node.js"
const operation = await pinner.upload(file, {
name: "my-file",
keyvalues: {
project: "demo",
version: "1.0"
},
onProgress: (progress) => {
console.log(`${progress.percentage.toFixed(2)}% complete`);
},
onComplete: (result) => {
console.log("Upload complete:", result.cid);
},
onError: (error) => {
console.error("Upload failed:", error);
},
signal: abortController.signal,
waitForOperation: true,
operationPollingOptions: {
interval: 1000,
timeout: 60000
}
});
```
#### Option Details
| Option | Type | Description |
| ------------------------- | ------------------------- | ----------------------------------------- |
| `name` | `string` | Custom name for the content |
| `keyvalues` | `Record` | Custom metadata |
| `onProgress` | `(progress) => void` | Progress callback |
| `onComplete` | `(result) => void` | Success callback |
| `onError` | `(error) => void` | Error callback |
| `signal` | `AbortSignal` | Cancel upload with AbortController |
| `size` | `number` | Size override for ReadableStream inputs |
| `isDirectory` | `boolean` | Whether upload is a directory |
| `isCarFile` | `boolean` | Whether input is already a valid CAR file |
| `waitForOperation` | `boolean` | Wait for pinning to complete |
| `operationPollingOptions` | `OperationPollingOptions` | Polling configuration |
#### Type Guard
Check if a value is an `UploadResult`:
```typescript
import { isUploadResult } from "@lumeweb/pinner";
const result = await operation.result;
if (isUploadResult(result)) {
console.log("Upload ID:", result.id);
console.log("CID:", result.cid);
}
```
### Builder Pattern
Use the builder pattern for fluent, chainable uploads:
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// File upload with builder
const operation = await pinner.upload.file(file)
.name("my-file.txt")
.keyvalues({ project: "demo", version: "1.0" })
.waitForOperation(true)
.operationPollingOptions({ interval: 1000, timeout: 60000 })
.pin();
const result = await operation.result;
// JSON upload with builder
const jsonOp = await pinner.upload.json({ data: "test" })
.name("config.json")
.keyvalues({ type: "config" })
.pin();
// Text upload with builder
const textOp = await pinner.upload.text("Hello, world!")
.name("greeting.txt")
.pin();
// CSV upload with builder
const csvOp = await pinner.upload.csv("name,age\nJohn,30")
.name("users.csv")
.pin();
// Raw CAR upload with builder
const carOp = await pinner.upload.raw(carFile)
.name("data.car")
.pin();
// Text upload using content alias
const contentOp = await pinner.upload.content("Hello, world!")
.name("greeting.txt")
.pin();
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
// File upload with builder
const operation = await pinner.upload.file(file)
.name("my-file.txt")
.keyvalues({ project: "demo", version: "1.0" })
.waitForOperation(true)
.operationPollingOptions({ interval: 1000, timeout: 60000 })
.pin();
const result = await operation.result;
// JSON upload with builder
const jsonOp = await pinner.upload.json({ data: "test" })
.name("config.json")
.keyvalues({ type: "config" })
.pin();
// Text upload with builder
const textOp = await pinner.upload.text("Hello, world!")
.name("greeting.txt")
.pin();
// CSV upload with builder
const csvOp = await pinner.upload.csv("name,age\nJohn,30")
.name("users.csv")
.pin();
// Raw CAR upload with builder
const carOp = await pinner.upload.raw(carFile)
.name("data.car")
.pin();
// Text upload using content alias
const contentOp = await pinner.upload.content("Hello, world!")
.name("greeting.txt")
.pin();
```
#### Builder Methods
All builder methods support chaining:
| Method | Description |
| ---------------------------------- | ------------------------------------- |
| `.name(string)` | Set the content name |
| `.keyvalues(object)` | Set custom metadata |
| `.waitForOperation(boolean)` | Wait for pinning completion |
| `.operationPollingOptions(object)` | Configure polling behavior |
| `.pin()` | Start the upload and return operation |
#### Builder Namespace Methods
The `upload` interface also works as a namespace with these methods:
| Method | Input Type | Description |
| ---------------- | ------------------------------- | ----------------------------- |
| `.file(file)` | `File` | Upload a File object |
| `.json(data)` | `object` | Upload JSON as a file |
| `.text(text)` | `string` | Upload text content |
| `.content(text)` | `string` | Alias for `.text()` |
| `.base64(data)` | `string` | Upload Base64 decoded content |
| `.csv(data)` | `string \| object[] \| any[][]` | Upload CSV data |
| `.url(url)` | `string` | Upload from URL |
| `.raw(car)` | `File \| ReadableStream` | Upload raw CAR file |
import { CodeGroup } from '@/components';
## SDK Utilities
Utility functions for stream manipulation, type guards, and CAR preprocessing.
### Type Guards
#### isUploadResult
Type guard to check if a value is an `UploadResult`. Useful when receiving unknown values from callbacks or event handlers:
```typescript
import { isUploadResult } from "@lumeweb/pinner";
// Check a value from a callback or event handler
function handleUploadResult(value: unknown) {
if (isUploadResult(value)) {
// TypeScript now knows value is UploadResult
console.log("Upload ID:", value.id);
console.log("CID:", value.cid);
console.log("Created:", value.createdAt);
} else {
console.log("Not an UploadResult");
}
}
```
#### isRetryable
Check if an error is retryable:
```typescript
import { isRetryable } from "@lumeweb/pinner";
async function uploadWithRetry(file, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await pinner.upload(file);
} catch (error) {
if (!isRetryable(error)) {
throw error; // Non-retryable, give up
}
console.log(`Retry attempt ${attempt}/${maxRetries}`);
}
}
}
```
#### isAuthenticationError
Check if an error is an authentication error:
```typescript
import { isAuthenticationError } from "@lumeweb/pinner";
try {
await pinner.upload(file);
} catch (error) {
if (isAuthenticationError(error)) {
console.log("Token expired, please re-authenticate");
// Trigger re-authentication flow
}
}
```
### MIME Type Constants
Export constants for common MIME types used with CAR files:
```typescript
import { MIME_TYPE_CAR, MIME_TYPE_OCTET_STREAM, FILE_EXTENSION_CAR } from "@lumeweb/pinner";
console.log(MIME_TYPE_CAR); // "application/vnd.ipld.car"
console.log(MIME_TYPE_OCTET_STREAM); // "application/octet-stream"
console.log(FILE_EXTENSION_CAR); // ".car"
```
### Pinata Adapters
#### pinataAdapter
Create a Pinata SDK v2.x compatible adapter:
```typescript
import { pinataAdapter } from "@lumeweb/pinner";
const adapter = pinataAdapter(pinner, {
pinataGateway: "example.mypinata.cloud"
});
// Use like Pinata SDK
const result = await adapter.upload.public.file(file);
```
#### pinataLegacyAdapter
Create a Pinata SDK v1.x compatible adapter:
```typescript
import { pinataLegacyAdapter } from "@lumeweb/pinner";
const adapter = pinataLegacyAdapter(pinner);
// Use like legacy Pinata SDK
const result = await adapter.pinFileToIPFS(file);
```
See the [Pinata Adapter Reference](/sdk/adapters) for detailed documentation.
## Pinata Migration Examples
Comprehensive code examples for migrating from Pinata SDK to Pinner. See the [Migration Guide](/migration/pinata) for step-by-step instructions.
### Pinata SDK 2.x Examples
#### File Upload
##### Pinata SDK 2.x
```typescript
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT
});
const file = new File(["Hello!"], "hello.txt", { type: "text/plain" });
const upload = await pinata.upload.public.file(file);
console.log("CID:", upload.cid);
console.log("Name:", upload.name);
```
##### Pinner with Adapter
```typescript
import { Pinner } from "@lumeweb/pinner";
import { pinataAdapter } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY
});
const adapter = pinataAdapter(pinner);
const file = new File(["Hello!"], "hello.txt", { type: "text/plain" });
const upload = await adapter.upload.public.file(file)
.name("hello.txt")
.execute();
console.log("CID:", upload.cid);
console.log("Name:", upload.name);
```
#### JSON Upload
##### Pinata SDK 2.x
```typescript
const data = { name: "test", value: 123 };
const upload = await pinata.upload.public.json(data);
```
##### Pinner with Adapter
```typescript
const data = { name: "test", value: 123 };
const upload = await adapter.upload.public.json(data)
.name("data.json")
.execute();
```
#### Base64 Upload
##### Pinata SDK 2.x
```typescript
const base64String = "SGVsbG8gV29ybGQh";
const upload = await pinata.upload.public.base64(base64String);
```
##### Pinner with Adapter
```typescript
const base64String = "SGVsbG8gV29ybGQh";
const upload = await adapter.upload.public.base64(base64String)
.name("file.txt")
.execute();
```
#### Pin by CID
##### Pinata SDK 2.x
```typescript
const pin = await pinata.upload.public.cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco");
```
##### Pinner with Adapter
```typescript
const pin = await adapter.upload.public.cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco")
.execute();
```
#### List Files
##### Pinata SDK 2.x
```typescript
const files = await pinata.files.public.list()
.limit(10)
.all();
for (const file of files) {
console.log(file.name, file.cid);
}
```
##### Pinner with Adapter
```typescript
const files = await adapter.files.public.list()
.limit(10)
.all();
for (const file of files) {
console.log(file.name, file.cid);
}
```
#### Directory Upload
##### Pinata SDK 2.x
```typescript
const files = [
new File(["content"], "file1.txt"),
new File(["content"], "file2.txt"),
];
const upload = await pinata.upload.public.fileArray(files);
```
##### Pinner with Adapter
```typescript
const files = [
new File(["content"], "file1.txt"),
new File(["content"], "file2.txt"),
];
const upload = await adapter.upload.public.fileArray(files)
.name("my-directory")
.execute();
```
#### Delete Files
##### Pinata SDK 2.x
```typescript
await pinata.files.public.delete(["QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"]);
```
##### Pinner with Adapter
```typescript
await adapter.files.public.delete(["QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"]);
```
### Pinata SDK 1.x Legacy Examples
#### File Upload
##### Pinata SDK 1.x
```typescript
import { pinata } from "pinata";
const file = new File(["Hello!"], "hello.txt", { type: "text/plain" });
const upload = await pinata.pinFileToIPFS(file);
console.log("IpfsHash:", upload.IpfsHash);
```
##### Pinner with Legacy Adapter
```typescript
import { Pinner } from "@lumeweb/pinner";
import { pinataLegacyAdapter } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY
});
const adapter = pinataLegacyAdapter(pinner);
const file = new File(["Hello!"], "hello.txt", { type: "text/plain" });
const upload = await adapter.pinFileToIPFS(file);
console.log("IpfsHash:", upload.IpfsHash);
```
#### JSON Upload
##### Pinata SDK 1.x
```typescript
const data = { name: "test", value: 123 };
const upload = await pinata.pinJSONToIPFS(data);
```
##### Pinner with Legacy Adapter
```typescript
const data = { name: "test", value: 123 };
const upload = await adapter.pinJSONToIPFS(data);
```
#### Pin by CID
##### Pinata SDK 1.x
```typescript
const pin = await pinata.pinByHash("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco");
```
##### Pinner with Legacy Adapter
```typescript
const pin = await adapter.pinByHash("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco");
```
#### List Pins
##### Pinata SDK 1.x
```typescript
const pins = await pinata.pinList({
limit: 10
});
for (const pin of pins.files) {
console.log(pin.ipfs_pin_hash);
}
```
##### Pinner with Legacy Adapter
```typescript
const pins = await adapter.pinList({
limit: 10
});
for (const pin of pins.files) {
console.log(pin.ipfs_pin_hash);
}
```
#### Unpin
##### Pinata SDK 1.x
```typescript
await pinata.unpin("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco");
```
##### Pinner with Legacy Adapter
```typescript
await adapter.unpin("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco");
```
#### Update Metadata
##### Pinata SDK 1.x
```typescript
await pinata.hashMetadata("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco", {
project: "my-app",
version: "2.0"
});
```
##### Pinner with Legacy Adapter
```typescript
await adapter.hashMetadata("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco", {
project: "my-app",
version: "2.0"
});
```
### Native Pinner API
For the best experience, you can use Pinner's native API directly:
#### File Upload
```typescript
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({
jwt: process.env.PINNER_API_KEY
});
const file = new File(["Hello!"], "hello.txt", { type: "text/plain" });
const result = await pinner.uploadAndWait(file);
console.log("CID:", result.cid);
```
#### Upload with Operation Control
```typescript
// Pass progress callback in options
const operation = await pinner.upload(file, {
name: "hello.txt",
keyvalues: { project: "demo" },
onProgress: (progress) => {
console.log(`Progress: ${progress.percentage.toFixed(1)}%`);
console.log(`Uploaded: ${progress.bytesUploaded} / ${progress.bytesTotal} bytes`);
}
});
// Wait for completion
const result = await operation.result;
// Or access progress directly from the operation
console.log("Final progress:", operation.progress.percentage);
```
#### Pin Management
```typescript
// Pin by CID
for await (const cid of pinner.pinByHash("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco")) {
console.log("Pinned:", cid.toString());
}
// List pins
const pins = await pinner.listPins();
for (const pin of pins) {
console.log(pin.cid.toString(), pin.name);
}
// Unpin
await pinner.unpin("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco");
```
### Error Handling
#### Pinata SDK
```typescript
try {
const upload = await pinata.upload.public.file(file);
} catch (error) {
console.error("Pinata error:", error.message);
}
```
#### Pinner SDK
```typescript
import { Pinner, PinnerError } from "@lumeweb/pinner";
try {
const result = await pinner.upload(file);
} catch (error) {
if (error instanceof PinnerError) {
console.error("Code:", error.code);
console.error("Message:", error.message);
}
}
```
import { HomePage } from "vocs/components";
import SdkInstallation from "../../partials/_sdk-installation.mdx";
## Migrate from Pinata
A step-by-step guide to migrate your application from Pinata SDK to Pinner.
### Why Migrate?
* ✅ **Privacy-focused infrastructure** - Your data, your control
* ✅ **Competitive pricing** - Better value for your storage needs
* ✅ **Multi-Network Access** - Support for multiple storage networks through one service
* ✅ **Drop-in compatibility** - Minimal code changes required
### Migration Strategy
Pinner offers two approaches:
| Approach | Best For | Effort |
| --------------------- | ----------------------------------- | ------ |
| **Pinata Adapter** | Quick migration, minimal changes | Low |
| **Native Pinner API** | Long-term projects, better features | Medium |
:::tip
Start with the adapter for quick migration, then gradually transition to the native API for better performance and features.
:::
### Quick Migration (Using Adapter)
#### Step 1: Install Pinner
#### Step 2: Configure Adapter
Choose the appropriate adapter and configure it. See [Pinata Adapter Reference](/sdk/adapters#quick-setup) for detailed setup instructions.
**For Pinata SDK 2.x:**
```typescript
import { pinataAdapter } from "@lumeweb/pinner";
```
**For Pinata SDK 1.x:**
```typescript
import { pinataLegacyAdapter } from "@lumeweb/pinner";
```
#### Step 3: Replace Initialization
Replace your Pinata SDK initialization with the Pinner adapter. See [Adapter Quick Setup](/sdk/adapters#quick-setup) for detailed examples.
The adapter provides the same API as Pinata SDK. See the [API Comparison](/sdk/adapters#api-comparison-tables) for a complete mapping.
See [Code Examples](/migration/examples) for comprehensive side-by-side code comparisons.
### Migration Checklist
* [ ] Install `@lumeweb/pinner` package
* [ ] Update imports and initialization
* [ ] Replace API calls with adapter calls
* [ ] Add `.execute()` to builder methods (v2 adapter)
* [ ] Test file uploads
* [ ] Test file listing and deletion
* [ ] Update error handling (see [Code Examples](/migration/examples))
* [ ] Remove Pinata SDK dependency
* [ ] Deploy and monitor
### Common Patterns
See [Code Examples](/migration/examples) for comprehensive side-by-side comparisons of:
* File uploads
* Directory uploads
* JSON uploads
* Base64 uploads
* Pinning by CID
* Listing files
* Deleting files
* And more
### Handling Differences
#### Builder Pattern (v2 Adapter)
The v2 adapter uses a builder pattern. Chain methods and call `.execute()`. See [Builder Patterns](/sdk/adapters#builder-patterns) for detailed examples.
#### Error Handling
Pinner errors follow a different structure. See [Error Handling Examples](/migration/examples#error-handling).
#### Unsupported Features
Some Pinata features are not supported. See [Unsupported Features](/sdk/adapters#unsupported-features) for details and alternatives.
### Testing Your Migration
1. **Test in development** - Verify all upload operations work
2. **Check file listings** - Ensure metadata is preserved
3. **Verify deletions** - Test unpin operations
4. **Monitor performance** - Compare upload speeds
5. **Check costs** - Review your billing
### Next Steps
* 📖 [API Reference](/sdk/adapters) - Complete adapter documentation
* 💻 [Code Examples](/migration/examples) - Detailed code comparisons
* 🚀 [Native API](/sdk/upload) - Using Pinner without adapter
* 📚 [Concepts](/concepts/cids) - Understanding CIDs and IPFS
import { CodeGroup } from '@/components';
import UploadCodeExamples from "../../partials/_upload-code-examples.mdx";
## Upload Your First File (3 minutes)
**Expected output:**
```
Uploaded CID: bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
Gateway URL: https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link
Size: 154 bytes
Time: 0.123s
```
### Verify it worked
```bash
pinner status bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
:::success[✅ Success looks like:]
**CID:** `bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq`
**Size:** 154 bytes
**Gateway:** [https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link](https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link)
:::
***
[Pinata Migration →](/guide/pinata-migration) [Reference Docs →](/api/swagger)
import { HomePage, Callout } from "vocs/components";
import PinnerDefinition from "../../partials/_pinner-definition.mdx";
import Prerequisites from "../../partials/_prerequisites.mdx";
import ApiTokenSetup from "../../partials/_api-token-setup.mdx";
## Pinner Guides
Step-by-step tutorials. Copy, paste, adapt.
**New to Pinner?** Start with [What is Pinner?](/about) to understand what we do (and don't do).
Upload Your First File
Migrate from Pinata
### Which guide?
| Your situation | Start with |
| ------------------ | ----------------------------- |
| New to Pinner | What is Pinner → First Upload |
| Using Pinata SDK | Pinata Migration |
| CLI-only workflows | CLI Getting Started |
| Building a web app | SDK Getting Started |
### Before you start
### What's next?
| After completing | Ready for |
| ---------------- | ----------------------------- |
| First Upload | SDK deep dives, CLI reference |
| Pinata Migration | Production switchover |
***
[What is Pinner? →](/about) [Upload your first file →](/guide/first-upload) [Migrate from Pinata →](/guide/pinata-migration)
import { CodeGroup } from '@/components';
import PinataApiMapping from "../../partials/_pinata-api-mapping.mdx";
## Migrate from Pinata
Same API calls, better privacy.
### API Mapping
### Upload Public File
```typescript title="Pinata SDK 1.x"
const result = await pinata.upload.public.file(file);
```
```typescript title="Pinata SDK 2.x"
const result = await pinata.upload.publicFile(file);
```
```typescript title="Pinner Adapter"
const result = await adapter.upload.public.file(file).execute();
```
### List Pins
```typescript title="Pinata SDK 1.x"
const result = await pinata.pinList({ status: "pinned" });
```
```typescript title="Pinata SDK 2.x"
const result = await pinata.getPins({ status: "pinned" });
```
```typescript title="Pinner Adapter"
const result = await adapter.pin.list({ status: "pinned" }).execute();
```
### Delete Pin
```typescript title="Pinata SDK 1.x"
const result = await pinata.unpin(cid);
```
```typescript title="Pinata SDK 2.x"
const result = await pinata.unpin(cid);
```
```typescript title="Pinner Adapter"
const result = await adapter.pin.remove(cid).execute();
```
### Test migration now
```bash
export PINNER_API_KEY="your_api_key"
npm install @lumeweb/pinner
```
```typescript
import { pinataAdapter } from "@lumeweb/pinner";
const adapter = pinataAdapter({ jwt: process.env.PINNER_API_KEY });
// Quick test
const test = await adapter.upload.public.json({ test: true }).execute();
console.log("CID:", test.cid);
```
### Unsupported features?
See the [Adapter Limitations](/sdk/adapters#limitations) table for feature compatibility.
***
[Get Pinner API key → signup](https://account.pinner.xyz) [Full adapter reference →](/sdk/adapters)
import { CodeGroup } from '@/components';
import { HomePage, Callout } from "vocs/components";
## CAR Files (Content Addressable Archive)
A CAR (Content Addressable Archive) file is a container format for storing and transferring IPLD (InterPlanetary Linked Data) blocks. It's the standard format for packaging content-addressed data in IPFS ecosystems.
### What is a CAR File?
A CAR file contains:
* **Header** - Encoded in DAG-CBOR, specifies version and root CIDs
* **Blocks** - Raw IPLD data blocks, each prefixed with length and CID
* **Optional metadata** - Additional information about the archive
#### Structure
```
┌─────────────────────────────────────┐
│ Header (DAG-CBOR encoded) │
│ - version: 1 or 2 │
│ - roots: Array of root CIDs │
├─────────────────────────────────────┤
│ Block 1 │
│ - length (varint) │
│ - CID (content identifier) │
│ - block data (raw bytes) │
├─────────────────────────────────────┤
│ Block 2 │
│ - ... │
├─────────────────────────────────────┤
│ ... more blocks ... │
└─────────────────────────────────────┘
```
### Why CAR Files Matter
CAR files are essential for:
* **Bulk uploads** - Efficiently upload many blocks at once
* **Data portability** - Bundle IPLD data for transfer or storage
* **Streaming support** - Read/write incrementally without loading entire files
* **Partial reads** - CARv2 includes offset/size fields for random access
* **Directory preservation** - Maintain IPLD graph structure (UnixFS, DAGs)
### How Pinner Uses CARs
When you upload content through Pinner, the SDK packages your data into CAR format for efficient storage on IPFS. This ensures:
* Content is addressed by its cryptographic hash (CID)
* Blocks are deduplicated across the network
* Data can be retrieved from any IPFS node
#### Pre-Calculable Root CID
A critical advantage of CAR format is **IPFS-nativity**. Since CAR contains raw IPLD blocks with their CIDs pre-computed, you can know the root CID of your data *before* uploading:
```js title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Upload a directory - root CID is calculated locally from IPFS block structure
const files = [
new File(["Hello, Pinner!"], "hello.txt"),
new File([JSON.stringify({ name: "config" })], "config.json"),
];
const operation = await pinner.uploadDirectory(files);
// Get the root CID - calculated client-side, no server round-trip needed
const rootCid = operation.result.cid;
console.log("Root CID:", rootCid); // e.g., bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
```js title="Node.js"
import { Pinner } from "@lumeweb/pinner";
import fs from "fs";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Upload a directory - root CID is calculated locally from IPFS block structure
const files = [
new File([fs.readFileSync("data/hello.txt")], "hello.txt"),
new File([fs.readFileSync("data/config.json")], "config.json"),
];
const operation = await pinner.uploadDirectory(files);
// Get the root CID - calculated client-side, no server round-trip needed
const rootCid = operation.result.cid;
console.log("Root CID:", rootCid); // e.g., bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
This is **impossible with non-IPFS-native formats** like ZIP. A ZIP file's checksum is based on raw bytes, not IPFS block hashes. To get the IPFS CID from a ZIP, you'd need to upload it and wait for the server to re-chunk and re-encode the data - introducing trust dependencies and latency.
With CARs, the CID is deterministic and locally verifiable. You're guaranteed the content will resolve to the same CID once pinned.
**Need to upload archives?** While CAR files offer pre-calculable CIDs, you can also upload ZIP, tar, and other archive formats directly. See [Archive Upload API](/api/archives) for curl examples with the `archive_mode` parameter.
### Uploading CAR Files
{/* */}
```js title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Upload a CAR file
const response = await fetch("/path/to/data.car");
const carData = response.body;
const operation = await pinner.uploadCar(carData);
const result = await operation.result;
console.log("Root CID:", result.cid);
```
```js title="Node.js"
import { Pinner } from "@lumeweb/pinner";
import fs from "fs";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Upload a CAR file
const carData = fs.readFileSync("/path/to/data.car");
const carFile = new File([carData], "data.car", { type: "application/vnd.ipld.car" });
const operation = await pinner.uploadCar(carFile);
const result = await operation.result;
console.log("Root CID:", result.cid);
```
import { CodeGroup } from '@/components';
import UploadCodeExamples from "../../partials/_upload-code-examples.mdx";
## CIDs (Content Identifiers)
A CID is a self-describing content-addressed identifier used to address content in distributed systems like IPFS.
### CID Versions
#### CIDv0
Legacy format, starts with `Qm`. Limited to DAG-PB codec:
```
QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco
```
#### CIDv1
Modern format with multicodec identifier. Supports multiple content types:
```
bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
### Pinner CID Normalization
Pinner automatically normalizes all CIDs to v1 format. This ensures:
* Consistent identifier format across your content
* Better interoperability with modern IPFS tools and gateways
* Support for multiple content codecs (not just DAG-PB)
When you upload content, Pinner always returns the v1 CID format regardless of the input format.
### How Pinner Uses CIDs
Pinner uses CIDs to uniquely identify your content. When you upload a file, Pinner returns the CID which can be used to:
* Retrieve your content from any IPFS gateway
* Share content with others
* Pin existing content by CID
### Finding Your Content's CID
import { CodeGroup } from '@/components';
import UploadCodeExamples from "../../partials/_upload-code-examples.mdx";
import PinStatusCheck from "../../partials/_pin-status-check.mdx";
import PinStates from "../../partials/_pin-states.mdx";
## Pin Lifecycle
Content goes through several states when pinned to Pinner.
### Pin States
### Checking Pin Status
### Retry Strategies
For failed pins, you can:
1. Retry the upload
2. Check network connectivity
3. Verify the content is available on IPFS
import { CodeGroup } from '@/components';
## Quotas & Limits
Pinner enforces usage limits to ensure fair access for all users.
### Storage Quotas
| Plan | Storage | Retention |
| ---------- | ------- | ---------- |
| Free | 1 GB | 30 days |
| Pro | 100 GB | Indefinite |
| Enterprise | Custom | Custom |
### Rate Limits
| Endpoint | Free | Pro |
| ----------- | ---- | --- |
| Uploads/min | 10 | 100 |
| Pins/min | 30 | 300 |
| Queries/min | 60 | 600 |
### Fair Use Policy
* No bandwidth abuse
* No automated scraping
* Respect rate limits
### Checking Your Pins
```typescript title="Browser"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
const pins = await pinner.listPins();
console.log(`Total pins: ${pins.length}`);
for (const pin of pins) {
console.log(`${pin.cid} - ${pin.status}`);
}
```
```typescript title="Node.js"
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY });
const pins = await pinner.listPins();
console.log(`Total pins: ${pins.length}`);
for (const pin of pins) {
console.log(`${pin.cid} - ${pin.status}`);
}
```
import CliAuthCommands from "../../partials/_cli-auth-commands.mdx";
## CLI Authentication
Manage authentication from the command line.
### account
Manage your Pinner.xyz account settings:
```bash
# Enable 2FA
pinner account otp enable
# Enable 2FA with OTP code (non-interactive)
pinner account otp enable --otp 123456
# Disable 2FA
pinner account otp disable
# Disable 2FA with password (non-interactive)
pinner account otp disable --password mypassword
```
## Command Overview
The Pinner CLI provides commands for all operations.
### Command Structure
```
pinner [global options] [command options] [arguments...]
```
### Global Flags
| Flag | Description |
| --------------- | ------------------------------------------------------------------ |
| `--json` | Output in JSON format |
| `--verbose, -v` | Enable verbose logging |
| `--quiet, -q` | Suppress non-error output |
| `--unmask` | Show sensitive data unmasked |
| `--auth-token` | Auth token (env: PINNER\_AUTH\_TOKEN) |
| `--secure` | Use HTTPS for API connections (default: true, env: PINNER\_SECURE) |
### Available Commands
| Command | Description |
| --------------- | ----------------------------------- |
| `setup` | Interactive first-time setup wizard |
| `auth` | Authenticate with Pinner.xyz |
| `register` | Create a new account |
| `confirm-email` | Confirm your email address |
| `account` | Manage account settings (2FA) |
| `upload` | Upload files/directories to IPFS |
| `pin` | Pin existing content by CID |
| `list` | List pinned content |
| `status` | Get pin status for CID |
| `unpin` | Remove a pin |
| `metadata` | Update pin metadata |
| `config` | View/set configuration |
| `doctor` | Display diagnostic information |
### Getting Help
```bash
# General help
pinner --help
# Command-specific help
pinner upload --help
# Subcommand help
pinner account otp --help
```
### Tutorial Commands
The CLI includes a tutorial mode that shows common workflows:
```bash
# Show tutorial
pinner --help
# Tutorial shows commands in priority order:
# 1. pinner upload myfile.txt
# 2. pinner pin
# 3. pinner list
# 4. pinner status
# 5. pinner unpin
# 6. pinner doctor
```
import { CodeGroup } from '@/components';
## Shell Completions
Enable tab completion for the Pinner CLI in your shell.
### Overview
The Pinner CLI supports programmable completion for:
* **bash**
* **zsh**
* **fish**
* **PowerShell (pwsh)**
### Generate Completion Script
Generate the completion script for your shell:
```bash title="Bash"
pinner completion bash > ~/.bash_completion.d/pinner
```
```bash title="ZSH"
pinner completion zsh > ~/.zsh/completion/_pinner
```
```bash title="Fish"
pinner completion fish > ~/.config/fish/completions/pinner.fish
```
```powershell title="PowerShell"
pinner completion pwsh > pinner.ps1
```
### Setup per Shell
#### Bash
Add to your shell profile:
```bash
# Option 1: Source directly
source <(pinner completion bash)
# Option 2: Save to bash_completion.d (requires setup)
pinner completion bash > ~/.bash_completion.d/pinner
```
The completion script will be automatically sourced in new bash shells if saved to `~/.bash_completion.d/pinner`.
#### ZSH
Add to your `.zshrc`:
```bash
# Generate completion function
PROG=pinner
source <(pinner completion zsh)
```
Or save to your completions directory:
```bash
pinner completion zsh > ~/.zsh/completion/_pinner
```
Make sure your `.zshrc` includes the completions directory:
```bash
fpath+=~/.zsh/completion
autoload -Uz compinit
compinit
```
#### Fish
Save to fish completions directory:
```bash
pinner completion fish > ~/.config/fish/completions/pinner.fish
```
Completions are automatically loaded in new fish shells.
#### PowerShell
Generate and source the completion script:
```powershell
# Generate completion script
pinner completion pwsh > pinner.ps1
# Source for current session
. ./pinner.ps1
# Persist across sessions - add to profile
code $profile
# Add: . ./pinner.ps1
```
### Verify Installation
```bash
# Test completion is working
pinner
# Should show: auth config doctor help list pin setup status unpin upload
# Test command completion
pinner auth
# Should show available auth options
```
### Completion Features
The completion script provides:
* Command completion (`pinner `)
* Subcommand completion (`pinner auth `)
* Flag completion (`pinner upload --`)
* File/path completion for file arguments
### Troubleshooting
#### Completion not working in new shell
Ensure the completion script is sourced in your shell profile:
* **Bash**: `~/.bashrc` or `~/.bash_profile`
* **ZSH**: `~/.zshrc`
* **Fish**: `~/.config/fish/config.fish`
* **PowerShell**: `$PROFILE`
#### Regenerate completions
If you update the CLI, regenerate completions:
```bash
pinner completion bash > ~/.bash_completion.d/pinner
```
### Related Commands
* [`pinner --help`](/cli/commands) - General help
* [`pinner doctor`](/cli/doctor) - Diagnostic checks
## CLI Config
Manage CLI configuration.
### config command
```bash
pinner config [get | set ]
```
### View All Configuration
```bash
pinner config
```
Displays all configuration values with descriptions.
### Get Configuration Value
```bash
# Get specific value
pinner config get base_endpoint
# Get secure setting
pinner config get secure
```
### Set Configuration Value
```bash
# Set base endpoint
pinner config set base_endpoint "api.pinner.xyz"
# Disable HTTPS
pinner config set secure false
# Set max retries
pinner config set max_retries 5
# Set memory limit for CAR generation in MB
pinner config set memory_limit 256
# Preview without saving
pinner config set memory_limit 256 --dry-run
```
### Common Configuration Keys
| Key | Type | Description |
| --------------- | ------ | ---------------------------------------------------- |
| `base_endpoint` | string | API endpoint domain (empty for default) |
| `secure` | bool | Use HTTPS (default: true) |
| `max_retries` | int | Maximum retry attempts (default: 3) |
| `memory_limit` | int | Memory limit for CAR generation in MB (default: 100) |
### Configuration File
The CLI stores configuration in `~/.config/pinner/config.json`:
```json
{
"base_endpoint": "",
"secure": true,
"max_retries": 3,
"memory_limit": 100
}
```
Note: The authentication token is managed separately by the `pinner auth` command.
### Options
| Option | Description |
| ----------- | ------------------------------ |
| `--dry-run` | Preview changes without saving |
### Subcommands
| Subcommand | Description |
| ------------------- | -------------------------------- |
| (none) | Show all configuration values |
| `get ` | Get specific configuration value |
| `set ` | Set configuration value |
## CLI Doctor
Run diagnostic checks to troubleshoot issues.
### doctor command
```bash
pinner doctor
```
### Example Output
```
Version: 0.1.0
OS: linux/amd64
Config: /home/user/.config/pinner/config.json
Config Status: Found
Endpoint: https://api.pinner.xyz
Memory limit: 100 MB
Max retries: 3
Authentication: Authenticated
Shell completion: Enabled (bash, zsh)
```
### JSON Output
```bash
pinner doctor --json
```
Returns structured JSON with all diagnostic information.
### What Gets Checked
| Check | Description |
| ---------------- | -------------------------------------- |
| Version | CLI version and build info |
| OS | Operating system and architecture |
| Config | Configuration file location and status |
| Endpoint | API endpoint configuration |
| Memory limit | CAR generation memory limit |
| Max retries | Maximum retry attempts |
| Authentication | Whether a token is configured |
| Shell completion | Which shells have completion enabled |
### Use Cases
Use `pinner doctor` when:
* Reporting bugs or issues
* Troubleshooting connection problems
* Verifying your configuration
* Checking authentication status
* Ensuring CLI is properly installed
## CLI Global Flags
Flags available on all commands.
### Available Flags
| Flag | Description |
| --------------- | ------------------------------------------------------------------ |
| `--json` | Output in JSON format |
| `--verbose, -v` | Enable verbose logging |
| `--quiet, -q` | Suppress non-error output |
| `--unmask` | Show sensitive data (tokens, passwords) unmasked |
| `--auth-token` | Auth token to override config (env: PINNER\_AUTH\_TOKEN) |
| `--secure` | Use HTTPS for API connections (default: true, env: PINNER\_SECURE) |
### Examples
```bash
# JSON output
pinner list --json
# Verbose logging
pinner upload file.txt --verbose
# Suppress output
pinner upload file.txt --quiet
# Show unmasked tokens
pinner config --unmask
# Override auth token
pinner upload file.txt --auth-token "YOUR_TOKEN"
# Use HTTP instead of HTTPS
pinner status bafy... --secure false
```
### Environment Variables
| Variable | Description |
| --------------------- | ------------------------------------- |
| `PINNER_AUTH_TOKEN` | API authentication token |
| `PINNER_SECURE` | Use HTTPS (true/false, default: true) |
| `PINNER_EMAIL` | Email address for authentication |
| `PINNER_PASSWORD` | Password for authentication |
| `PINNER_OTP` | 6-digit OTP code for 2FA |
| `PINNER_MEMORY_LIMIT` | Memory limit for CAR generation in MB |
### Output Modes
The CLI supports different output modes:
* **Default**: Human-readable colored output
* `--json`: Machine-readable JSON output
* `--quiet`: Minimal output (errors only)
* `--verbose`: Detailed output with debug information
* `--unmask`: Show sensitive values (tokens, passwords)
import Prerequisites from "../../partials/_prerequisites.mdx";
import CliInstallation from "../../partials/_cli-installation-content.mdx";
import CliAuthCommands from "../../partials/_cli-auth-commands.mdx";
## CLI Getting Started
Install and configure the Pinner CLI.
### Installation
### Initial Setup
The CLI includes an interactive setup wizard that guides you through authentication and configuration:
```bash
# Run the setup wizard
pinner setup
# Skip specific steps if needed
pinner setup --skip-auth
pinner setup --skip-config
pinner setup --reset
```
### Authentication
### Verification
```bash
# Check version
pinner --version
# View help
pinner --help
# Run diagnostics
pinner doctor
```
## CLI List
List your pinned content.
### list command
```bash
pinner list
```
### Filter by Name
```bash
# Filter pins by name
pinner list --name "backup"
# From stdin
echo "backup" | pinner list
```
### Filter by Status
```bash
# List only pinned content
pinner list --status pinned
# List queued content
pinner list --status queued
# List pinning content
pinner list --status pinning
# List failed content
pinner list --status failed
```
### Limit Results
```bash
# Maximum number of results (default: 10)
pinner list --limit 20
# Combined filters
pinner list --name backup --status failed --limit 50
```
### Watch Mode
Continuously monitor and update pin status:
```bash
pinner list --watch
```
### Options
| Option | Description |
| ---------- | -------------------------------------------------- |
| `--name` | Filter by name (supports stdin) |
| `--status` | Filter by status (queued, pinning, pinned, failed) |
| `--limit` | Maximum number of results (default: 10) |
| `--watch` | Continuously monitor and update |
### Output Fields
| Field | Description |
| ------- | -------------------------------------------- |
| CID | Content identifier |
| NAME | User-provided name |
| STATUS | Pin status (queued, pinning, pinned, failed) |
| CREATED | Creation timestamp |
## CLI Metadata
Manage metadata for pinned content.
### metadata command
```bash
pinner metadata
```
### Set Metadata
```bash
# Set key-value pairs
pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --set category=backup --set environment=prod
# From stdin (key=value per line)
echo -e "category=backup\nenvironment=prod" | pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
### Clear Metadata
```bash
# Clear all metadata
pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --clear
```
### Preview Mode
```bash
# Preview without making changes
pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --set category=backup --dry-run
```
### Options
| Option | Description |
| ----------- | ---------------------------------------- |
| `--set` | Set metadata as key=value (repeatable) |
| `--clear` | Clear all metadata |
| `--dry-run` | Preview operation without making changes |
### Examples
```bash
# Set multiple metadata fields
pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --set author=alice --set version=1.0 --set date=2024-01-15
# Clear and set new metadata
pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --clear --set newkey=newvalue
# Preview changes
pinner metadata bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --set category=backup --dry-run
```
## CLI Pin
Pin existing content by CID.
### pin command
```bash
pinner pin
```
### Pin a CID
```bash
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
### Pin with Name
```bash
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --name "My Content"
```
### Pin Multiple CIDs
```bash
# As arguments
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco
# In parallel (default: 1)
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco --parallel 5
# From a file (one CID per line)
pinner pin --file cids.txt
# From stdin
echo "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq" | pinner pin
```
### Pin and Wait
```bash
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --wait
```
### Continue on Error
```bash
# Continue processing even if some pins fail
pinner pin --file cids.txt --continue --parallel 10 --wait
```
### Preview Mode
```bash
# Preview without making changes
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --dry-run
```
### Options
| Option | Description |
| ------------ | ------------------------------------------------ |
| `--name` | Custom name for the pin |
| `--wait` | Wait for pinning to complete |
| `--file` | Read CIDs from a file (one per line) |
| `--parallel` | Number of parallel operations (default: 1) |
| `--continue` | Continue processing even if some operations fail |
| `--dry-run` | Preview operation without making changes |
### Output
For single CID:
```
Pinned CID: bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
Request ID: req-123456
Status: pinned
```
For batch operations:
```
Batch operation completed in 2.345s
Total: 10 | Succeeded: 9 | Failed: 1 | Skipped: 0
Failed operations:
- QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco: error message here
```
## CLI Setup
Use the interactive setup wizard to configure the CLI.
### setup command
```bash
pinner setup
```
The setup wizard guides you through:
* Authentication (login or register)
* Configuration settings (API endpoint, memory limits, etc.)
### Options
| Option | Description |
| ------------------- | --------------------------------------------------------------------------------------------- |
| `--skip-auth` | Skip authentication step |
| `--skip-config` | Skip configuration step |
| `--reset` | Reset configuration and start fresh |
| `--non-interactive` | Run in non-interactive mode (not recommended - use `pinner auth` and `pinner config` instead) |
### Interactive Mode
```bash
pinner setup
# Welcome to Pinner CLI Setup
# ? Would you like to authenticate? (Y/n):
# ? Would you like to configure settings? (Y/n):
```
### Non-Interactive Mode
For scripted environments, use the individual commands instead:
```bash
# Authenticate first
pinner auth --email user@example.com
# Then configure settings
pinner config set memory_limit 256
pinner config set secure true
```
### Reset Configuration
To start fresh with configuration:
```bash
pinner setup --reset
```
### What Gets Configured
The setup wizard helps configure:
* API endpoint (base domain)
* Secure connection (HTTPS)
* Memory limit for CAR generation
* Authentication token
import PinStatusCheck from "../../partials/_pin-status-check.mdx";
## CLI Status
Check the status of pinned content.
### status command
```bash
pinner status
```
### Check Status
```bash
pinner status bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
### Watch Mode
Watch for status changes:
```bash
pinner status bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --watch
```
### Global JSON Output
Use the global `--json` flag for JSON output:
```bash
pinner status bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --json
```
### Multiple CIDs
```bash
# From arguments
pinner status bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco
# From stdin
cat cids.txt | pinner status
```
### Status Values
| Status | Description |
| --------- | ---------------------------- |
| `queued` | Pin is queued for processing |
| `pinning` | Pin is being processed |
| `pinned` | Pin is successfully pinned |
| `failed` | Pin failed to pin |
### Output Fields
| Field | Description |
| --------- | ---------------------------------------- |
| CID | Content identifier |
| Status | Pin status |
| Created | Creation timestamp |
| Delegates | Pinning service delegates (if available) |
## Troubleshooting
Common issues and solutions for the Pinner CLI. If you can't find your issue here, run `pinner doctor` to gather diagnostic information.
### Quick Fixes
#### Authentication Issues
**"Not authenticated" error when running commands**
```bash
# Check authentication status
pinner doctor
```
If you're not authenticated:
```bash
# Interactive login
pinner auth --email your@email.com
# Or provide token directly
pinner auth YOUR_API_TOKEN
```
**"Authentication expired or invalid"**
Your session token has expired. Re-authenticate:
```bash
pinner auth --email your@email.com
```
#### Connection Issues
**"Connection failed" or "Network timeout"**
1. Check your internet connection
2. Verify the API endpoint is accessible:
```bash
ping pinner.xyz
```
3. Check if you're behind a firewall or proxy
4. Try with `--secure=false` to use HTTP (not recommended for production):
```bash
pinner --secure=false upload myfile.txt
```
#### File Issues
**"File not found"**
1. Verify the file path is correct
2. Check file permissions:
```bash
ls -la /path/to/your/file
```
3. Use absolute paths:
```bash
pinner upload /absolute/path/to/file.txt
```
**"Permission denied"**
1. Check file ownership:
```bash
ls -la /path/to/file
```
2. Change permissions if needed:
```bash
chmod 644 /path/to/file
```
### Authentication Errors
#### Not Authenticated
**Cause:** No authentication token is configured.
**Solution:**
```bash
# Method 1: Interactive login
pinner auth --email your@email.com
# Method 2: Provide token directly
pinner auth YOUR_API_TOKEN
# Method 3: Use environment variables
export PINNER_EMAIL="your@email.com"
export PINNER_PASSWORD="yourpassword"
pinner auth
```
**Environment variables for authentication:**
* `PINNER_EMAIL` - Email address
* `PINNER_PASSWORD` - Password
* `PINNER_OTP` - 2FA code (if enabled)
* `PINNER_AUTH_TOKEN` - API token (alternative to login)
#### Registration Issues
**"Registration failed"**
1. Verify your email format is correct
2. Check password meets requirements (usually 8+ characters)
3. Ensure you're not using an already registered email:
```bash
pinner register --email new@email.com
```
**Email confirmation issues**
After registration, you must confirm your email:
```bash
pinner confirm-email --email your@email.com --token VERIFICATION_TOKEN
```
Check your email for the verification token. If you didn't receive it:
1. Check spam folder
2. Verify the email address is correct
3. Try registering again
### Upload Errors
#### Upload Failed
**Cause:** The upload operation could not complete.
**Solutions:**
1. Check your internet connection
2. Verify file is readable:
```bash
cat /path/to/file > /dev/null && echo "File is readable"
```
3. Try with a smaller file first
4. Check available disk space
5. Increase memory limit for large files:
```bash
pinner upload largefile.dat --memory-limit 1024
```
#### Upload Interrupted
**Cause:** Upload was interrupted (Ctrl+C or network issue).
**Solution:**
The CLI supports resume for TUS uploads. Try uploading again:
```bash
pinner upload file.dat --wait
```
#### Invalid CID
**Cause:** The content identifier (CID) format is invalid.
**Solution:**
1. Verify the CID is correct
2. CIDs should start with `Qm`, `bafy`, or similar multiformat prefix
3. Example valid CID: `bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq`
### Pinning Errors
#### Pin Not Found
**Cause:** The specified CID is not pinned in your account.
**Solution:**
1. List your pins to verify:
```bash
pinner list
```
2. Check if the CID is correct
3. Pin the content if not already pinned:
```bash
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
#### Pinning Failed
**Cause:** The pinning operation could not complete.
**Solutions:**
1. Verify the CID exists on IPFS
2. Check your network connection
3. Try again later
4. Check pin status:
```bash
pinner status bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
### Configuration Issues
#### Configuration Not Found
**Cause:** No configuration file exists.
**Solution:**
Run setup to create configuration:
```bash
pinner setup
```
Configuration is stored at `~/.config/pinner/config.toml` (Linux/macOS) or `%APPDATA%\pinner\config.toml` (Windows).
#### Configuration Invalid
**Cause:** Configuration file contains invalid values.
**Solution:**
1. Check configuration:
```bash
pinner config get
```
2. Reset configuration:
```bash
pinner setup --reset
```
3. Edit config manually or delete and re-run setup
### Network Errors
#### Connection Failed
**Cause:** Cannot connect to the Pinner API.
**Solutions:**
1. Check internet connection:
```bash
curl -I https://pinner.xyz
```
2. Check firewall/proxy settings
3. Try with verbose output:
```bash
pinner --verbose upload file.txt
```
#### Network Timeout
**Cause:** Request timed out.
**Solutions:**
1. Try again (may be temporary)
2. Use a more stable network connection
3. Increase timeout by retrying the operation
### Input Validation Errors
#### Path Required
**Cause:** No file path provided for upload.
**Solution:**
```bash
pinner upload /path/to/file.txt
```
#### CID Required
**Cause:** No CID provided for pin/status operations.
**Solution:**
```bash
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
#### Invalid CID Format
**Cause:** CID format is not recognized.
**Solution:**
1. Verify the CID is correct
2. CIDs are case-sensitive
3. Example valid CID: `bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq`
#### No CIDs Provided
**Cause:** No CIDs specified for batch operation.
**Solution:**
Provide CIDs directly or use a file:
```bash
# Direct CIDs
pinner pin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco
# From file (one CID per line)
pinner pin --file cids.txt
```
### Getting Help
#### Run Diagnostics
Use the `doctor` command to gather diagnostic information:
```bash
pinner doctor
```
For JSON output (useful for bug reports):
```bash
pinner doctor --json
```
#### Verbose Output
Enable verbose mode to see detailed logs:
```bash
pinner --verbose upload file.txt
```
#### Check Configuration
View your current configuration:
```bash
pinner config get
```
#### Still Need Help?
1. Run `pinner doctor` to gather diagnostic info
2. Search [existing issues](https://github.com/lumeweb/pinner-cli/issues)
3. Open a new issue with:
* Error message
* Steps to reproduce
* Configuration output (`pinner doctor`)
* Operating system and version
### Environment Variables Reference
| Variable | Description |
| --------------------- | ------------------------------------- |
| `PINNER_EMAIL` | Email for authentication |
| `PINNER_PASSWORD` | Password for authentication |
| `PINNER_OTP` | 2FA code for authentication |
| `PINNER_AUTH_TOKEN` | API token (alternative to login) |
| `PINNER_SECURE` | Use HTTPS (true/false) |
| `PINNER_MEMORY_LIMIT` | Memory limit for CAR generation in MB |
### Shell Completion
If shell completion isn't working:
```bash
# Setup bash completion
echo 'source <(pinner completion bash)' >> ~/.bashrc
# Setup zsh completion
echo 'source <(pinner completion zsh)' >> ~/.zshrc
# Setup fish completion
pinner completion fish > ~/.config/fish/completions/pinner.fish
```
## CLI Unpin
Remove a pin from content.
### unpin command
```bash
pinner unpin
```
### Unpin Content
```bash
pinner unpin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
Prompts for confirmation by default.
### Skip Confirmation
```bash
# Skip confirmation prompt
pinner unpin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --confirm
```
### Batch Unpin
Remove multiple CIDs:
```bash
# As arguments
pinner unpin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco --confirm
# In parallel
pinner unpin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco --parallel 5 --confirm
# From a file
pinner unpin --file cids.txt --confirm
# From stdin
echo "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq" | pinner unpin --confirm
# Continue on error
pinner unpin --file cids.txt --confirm --continue --parallel 10
```
### Preview Mode
```bash
# Preview without making changes
pinner unpin bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq --dry-run
```
### Options
| Option | Description |
| ------------ | ------------------------------------------------ |
| `--confirm` | Skip confirmation prompts |
| `--file` | Read CIDs from a file (one per line) |
| `--parallel` | Number of parallel operations (default: 1) |
| `--continue` | Continue processing even if some operations fail |
| `--dry-run` | Preview operation without making changes |
### Output
For single CID:
```
Unpinned CID: bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
```
For batch operations:
```
Batch operation completed in 1.234s
Total: 10 | Succeeded: 9 | Failed: 1 | Skipped: 0
Failed operations:
- QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco: error message here
```
## CLI Upload
Upload files and directories to IPFS via the Pinner.xyz service.
### upload command
```bash
pinner upload
```
### Upload a File
```bash
pinner upload document.txt
```
### Upload with Name
```bash
pinner upload document.txt --name "My Document"
```
### Upload and Wait
```bash
pinner upload document.txt --wait
```
Blocks until the pinning operation completes.
### Upload a Directory
```bash
pinner upload ./my-directory --name "My Project"
```
### Upload from Stdin
```bash
# Pipe content
cat myfile.txt | pinner upload --name "my file"
# Echo content
echo "hello world" | pinner upload --name "greeting"
# Download and upload
curl -s https://example.com/data | pinner upload --name "downloaded"
```
### Large File Options
```bash
# Set memory limit for CAR generation (in MB)
pinner upload largefile.zip --memory-limit 500 --wait
# Preview without uploading
pinner upload file.txt --dry-run
```
### Options
| Option | Description |
| ---------------- | ------------------------------------------------------------------ |
| `--name` | Custom name for the pin |
| `--wait` | Wait for pinning to complete |
| `--memory-limit` | Memory limit for CAR generation in MB (env: PINNER\_MEMORY\_LIMIT) |
| `--dry-run` | Preview operation without uploading |
### Output
The upload command returns:
* **CID**: Content identifier for your uploaded content
* **Gateway URL**: Public URL to access your content
* **Size**: File size in human-readable format
* **Time**: Upload duration
```bash
Uploaded CID: bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq
Gateway URL: https://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq.ipfs.dweb.link
Size: 1.5 MB
Time: 2.345s
```
## Archive Uploads
Upload archive files (ZIP, tar, tar.gz, etc.) with the `archive_mode` parameter to control how archives are processed.
### archive\_mode Parameter
Use `archive_mode` to control how archives are handled:
| Mode | Value | Behavior |
| ------------ | ------------------- | ---------------------------------------------------------- |
| **Extract** | `extract` (default) | Server extracts contents, creates IPFS directory structure |
| **Preserve** | `preserve` | Archive uploaded as a single file |
#### When to Use Each Mode
* **extract**: Publishing websites, datasets, multi-file content where individual files need to be accessible
* **preserve**: Backups, sharing archives directly, when you need the exact same file back
### Supported Formats
* ZIP (`.zip`)
* TAR (`.tar`)
* Compressed TAR (`.tar.gz`, `.tar.bz2`, `.tar.xz`)
* Other archive formats supported by the server
### CID Limitation
Unlike CAR files, archive uploads cannot return the CID before processing completes.
#### Why?
1. Archives must be downloaded and verified
2. Contents are extracted and re-chunked into IPFS blocks
3. Root CID is computed from the resulting IPLD graph
#### What This Means
| | CAR Files | Archives |
| ------------------- | ----------- | ---------------- |
| CID calculation | Client-side | Server-side |
| Known before upload | Yes | No |
| Processing time | Instant | Requires polling |
| Pre-computation | Possible | Not possible |
***
### POST Upload (≤100MB)
Use POST for simple archive uploads up to 100MB.
#### Endpoint
```
POST https://ipfs.pinner.xyz/api/upload
```
#### Query Parameters
| Parameter | Type | Required | Description |
| -------------- | -------- | -------- | --------------------------------- |
| `archive_mode` | `string` | No | `extract` (default) or `preserve` |
#### Form Fields
| Field | Type | Required | Description |
| ------ | -------- | -------- | ------------------- |
| `file` | `File` | Yes | The archive file |
| `name` | `string` | No | Custom display name |
#### Examples
**Extract contents (default):**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload?archive_mode=extract" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@archive.zip"
```
**Preserve archive as single file:**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload?archive_mode=preserve" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@archive.zip"
```
**With custom name:**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload?archive_mode=extract" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@website.zip" \
-F "name=my-website"
```
#### Response
```json
{
"id": "12345",
"cid": "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq",
"name": "archive.zip",
"size": 1048576,
"mimeType": "application/zip",
"operationId": 12345
}
```
***
### TUS Upload (>100MB)
Use TUS for large archives over 100MB.
#### Workflow
1. Create upload with `archive_mode` in metadata
2. Upload chunks
3. Finish upload
4. Poll for CID
#### Create Upload
```bash
# Create upload with extract mode
curl -X POST "https://ipfs.pinner.xyz/api/upload/tus" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: 524288000" \
-H "Upload-Metadata: archive_mode ZXh0cmFjdA==" \
-H "Content-Length: 0"
```
#### Upload Chunks
```bash
# Upload first chunk
curl -X PATCH "https://ipfs.pinner.xyz/api/upload/tus/abc123xyz" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Tus-Resumable: 1.0.0" \
-H "Content-Type: application/offset+octet-stream" \
-H "Upload-Offset: 0" \
-H "Content-Length: 5242880" \
--data-binary @chunk1.bin
```
#### Finish and Poll
```bash
# Finish upload
curl -X DELETE "https://ipfs.pinner.xyz/api/upload/tus/abc123xyz" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Tus-Resumable: 1.0.0"
# Poll for CID
curl "https://ipfs.pinner.xyz/api/operations/abc123xyz" \
-H "Authorization: Bearer YOUR_API_TOKEN"
```
***
### Error Handling
#### Common Errors
**400 Bad Request:**
```json
{
"error": "bad_request",
"message": "Invalid archive format or corrupted file"
}
```
**413 Content Too Large:**
```json
{
"error": "payload_too_large",
"message": "POST upload exceeds 100MB limit. Use TUS for larger files."
}
```
**422 Unprocessable Entity:**
```json
{
"error": "invalid_archive",
"message": "Archive is corrupted or password-protected"
}
```
***
### See Also
* [Small File Uploads](/api/small-uploads) - POST upload method
* [Large File Uploads](/api/large-uploads) - TUS resumable uploads
* [Upload Limits](/api/limits) - Check account limits
import ApiTokenSetup from "../../partials/_api-token-setup.mdx";
## Authentication
All API requests require authentication using Bearer tokens.
### Authorization Header
Include your API token in the `Authorization` header:
| Header | Value | Description |
| --------------- | -------------- | -------------- |
| `Authorization` | `Bearer TOKEN` | Your API token |
### Example
```bash
curl "https://api.pinner.xyz/api/upload-limit" \
-H "Authorization: Bearer YOUR_API_TOKEN"
```
### Token Security
* Keep your token secure and never expose it in client-side code
* Use environment variables to store tokens
* Rotate tokens if they are compromised
### Error Responses
**401 Unauthorized:**
```json
{
"error": "unauthorized",
"message": "Invalid or missing API token"
}
```
**403 Forbidden:**
```json
{
"error": "forbidden",
"message": "Insufficient permissions"
}
```
### See Also
* [Small File Uploads](/api/small-uploads)
* [Large File Uploads](/api/large-uploads)
* [Archive Uploads](/api/archives)
## Large File Uploads (TUS)
Upload files of any size using the [TUS](https://tus.io/) resumable upload protocol. TUS allows you to resume interrupted uploads and track progress.
### Why TUS?
* **Resume interrupted uploads** - Pick up from where you left off
* **No size limit** - Upload files of any size
* **Progress tracking** - Built-in offset tracking
* **Better for unreliable connections** - Mobile, slow networks
### Learn More
Visit [tus.io](https://tus.io/) for the official TUS protocol specification, implementation guides, and best practices.
### Official TUS Clients
Use the official TUS client libraries for reliable uploads:
| Language | Library | Repository |
| ---------- | --------------- | ------------------------------------------------------------------------ |
| JavaScript | tus-js-client | [github.com/tus/tus-js-client](https://github.com/tus/tus-js-client) |
| Go | tus-go-client | [github.com/tus/tus-go-client](https://github.com/tus/tus-go-client) |
| Python | tuspy | [github.com/tus/tuspy](https://github.com/tus/tuspy) |
| Java | tus-java-client | [github.com/tus/tus-java-client](https://github.com/tus/tus-java-client) |
### Using Our SDK
The Pinner SDK includes built-in TUS support:
```bash
pnpm add @lumeweb/pinner
```
```typescript
import { Pinner } from "@lumeweb/pinner";
const pinner = new Pinner({ jwt: "YOUR_API_TOKEN" });
// Upload large file with automatic TUS
const operation = await pinner.upload.largeFile("large-file.zip");
const result = await operation.result;
console.log("CID:", result.cid);
```
### Using Our CLI
The CLI handles TUS automatically for large files:
```bash
pinner upload large-file.zip --wait
```
### TUS Server Capabilities
Query the TUS server for supported versions, extensions, and limits:
```
OPTIONS https://ipfs.pinner.xyz/api/upload/tus
```
#### Response Headers
| Header | Description |
| ------------------------ | ----------------------------- |
| `Tus-Version` | Supported protocol versions |
| `Tus-Extension` | Supported extensions |
| `Tus-Max-Size` | Maximum upload size in bytes |
| `Tus-Checksum-Algorithm` | Supported checksum algorithms |
#### Example
```bash
curl -X OPTIONS "https://ipfs.pinner.xyz/api/upload/tus" \
-H "Authorization: Bearer YOUR_API_TOKEN"
```
### TUS Protocol Overview
If implementing TUS manually, the protocol follows these steps:
1. **Create upload** - POST to `/api/upload/tus` with metadata
2. **Upload chunks** - PATCH requests with file data and offset
3. **Finish upload** - DELETE request
4. **Get CID** - Poll `/api/operations/{id}`
#### Create Upload
```
POST https://ipfs.pinner.xyz/api/upload/tus
```
**Headers:**
* `Authorization: Bearer TOKEN`
* `Tus-Resumable: 1.0.0`
* `Upload-Length: `
* `Upload-Metadata: archive_mode ,name `
**Status Codes:**
* `201 Created` - Upload created (includes `Location` header)
* `400 Bad Request` - Checksum algorithm not supported
* `412 Precondition Failed` - TUS version mismatch
* `413 Request Entity Too Large` - Exceeds max size
* `415 Unsupported Media Type` - Wrong Content-Type
#### Upload Chunks
```
PATCH https://ipfs.pinner.xyz/api/upload/tus/{id}
```
**Headers:**
* `Authorization: Bearer TOKEN`
* `Tus-Resumable: 1.0.0`
* `Upload-Offset: `
* `Content-Type: application/offset+octet-stream`
**Status Codes:**
* `204 No Content` - Chunk accepted (includes new `Upload-Offset`)
* `400 Bad Request` - Checksum algorithm not supported
* `403 Forbidden` - Cannot modify final upload
* `404 Not Found` - Upload resource not found
* `409 Conflict` - Offset mismatch
* `410 Gone` - Upload expired
* `412 Precondition Failed` - TUS version mismatch
* `415 Unsupported Media Type` - Wrong Content-Type
* `460 Checksum mismatch`
#### Finish Upload
```
DELETE https://ipfs.pinner.xyz/api/upload/tus/{id}
```
**Status Codes:**
* `204 No Content` - Upload terminated
* `412 Precondition Failed` - TUS version mismatch
#### Get Offset (HEAD)
```
HEAD https://ipfs.pinner.xyz/api/upload/tus/{id}
```
**Status Codes:**
* `200 OK` - Returns current offset (includes `Upload-Offset` header)
* `403 Forbidden` - Resource not accessible
* `404 Not Found` - Resource not found
* `410 Gone` - Upload expired
* `412 Precondition Failed` - TUS version mismatch
#### Get CID
```
GET https://ipfs.pinner.xyz/api/operations/{id}
```
**Status Codes:**
* `200 OK` - Returns operation status and CID when complete
### See Also
* [Small File Uploads](/api/small-uploads) - POST upload method
* [Archive Uploads](/api/archives) - Archive-specific options
* [Upload Limits](/api/limits) - Check account limits
* [SDK Getting Started](/sdk/getting-started) - Pinner SDK documentation
* [CLI Upload](/cli/upload) - Command-line upload guide
## Upload Limits
Retrieve the maximum allowed upload size for your account.
### Endpoint
```
GET https://account.pinner.xyz/api/upload-limit
```
### Request Headers
| Header | Value | Description |
| --------------- | -------------- | -------------- |
| `Authorization` | `Bearer TOKEN` | Your API token |
### Example
```bash
curl "https://account.pinner.xyz/api/upload-limit" \
-H "Authorization: Bearer YOUR_API_TOKEN"
```
### Response
```json
{
"limit": 104857600
}
```
The `limit` value is in bytes. In this example, `104857600` bytes = 100MB.
### Account Limits
| Limit Type | Description |
| ----------- | ---------------------------------------------------- |
| POST upload | Maximum file size for simple uploads (≤100MB) |
| TUS upload | No hard limit, use resumable uploads for large files |
| Rate limits | Apply per API endpoint |
### See Also
* [Small File Uploads](/api/small-uploads) - POST upload method
* [Large File Uploads](/api/large-uploads) - TUS resumable uploads
## Small File Uploads (POST)
Upload files up to 100MB using simple HTTP POST with multipart/form-data.
### Endpoint
```
POST https://ipfs.pinner.xyz/api/upload
```
### Content-Type
Use `multipart/form-data` with the file in the `file` field.
### Form Fields
| Field | Type | Required | Description |
| ------ | -------- | -------- | ------------------- |
| `file` | `File` | Yes | The file to upload |
| `name` | `string` | No | Custom display name |
### Query Parameters
| Parameter | Type | Required | Description |
| -------------- | -------- | -------- | --------------------------------------- |
| `archive_mode` | `string` | No | `extract` or `preserve` (archives only) |
### Request Headers
| Header | Value | Description |
| --------------- | -------------- | -------------- |
| `Authorization` | `Bearer TOKEN` | Your API token |
### Examples
**Basic upload:**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@document.pdf"
```
**With custom name:**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@document.pdf" \
-F "name=my-document"
```
**Archive with extract mode:**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload?archive_mode=extract" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@archive.zip"
```
**Archive with preserve mode:**
```bash
curl -X POST "https://ipfs.pinner.xyz/api/upload?archive_mode=preserve" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@archive.zip"
```
### Response
```json
{
"id": "12345",
"cid": "bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq",
"name": "document.pdf",
"size": 1048576,
"mimeType": "application/pdf",
"operationId": 12345
}
```
### Limits
| Limit | Value |
| ----------------- | ------------- |
| Max file size | 100MB |
| Supported formats | Any file type |
### See Also
* [Large File Uploads](/api/large-uploads) - TUS resumable uploads
* [Archive Uploads](/api/archives) - Archive-specific options
* [Upload Limits](/api/limits) - Server capabilities
import { CodeGroup } from '@/components';
## API Documentation
Each service in the Pinner ecosystem provides self-hosted OpenAPI (Swagger) documentation.
### Service Endpoints
| Service | Domain | Description |
| ------- | ------------------------------------------------ | ---------------------------------- |
| IPFS | [ipfs.pinner.xyz](https://ipfs.pinner.xyz) | File upload, pinning, TUS protocol |
| Account | [account.pinner.xyz](https://account.pinner.xyz) | User account, API keys, limits |
### Accessing Swagger Docs
Every service exposes three documentation endpoints:
| Endpoint | Format | Description |
| --------------- | ------ | -------------------------- |
| `/swagger` | HTML | Interactive Swagger UI |
| `/swagger.yaml` | YAML | OpenAPI specification file |
| `/swagger.json` | JSON | OpenAPI specification file |
#### Examples
**IPFS Service:**
* [https://ipfs.pinner.xyz/swagger](https://ipfs.pinner.xyz/swagger) - Interactive UI
* [https://ipfs.pinner.xyz/swagger.yaml](https://ipfs.pinner.xyz/swagger.yaml) - YAML spec
* [https://ipfs.pinner.xyz/swagger.json](https://ipfs.pinner.xyz/swagger.json) - JSON spec
**Account Service:**
* [https://account.pinner.xyz/swagger](https://account.pinner.xyz/swagger) - Interactive UI
* [https://account.pinner.xyz/swagger.yaml](https://account.pinner.xyz/swagger.yaml) - YAML spec
* [https://account.pinner.xyz/swagger.json](https://account.pinner.xyz/swagger.json) - JSON spec
### Using Swagger UI
The interactive Swagger UI allows you to:
* Browse all available endpoints
* View request/response schemas
* Execute API calls directly from the browser
* Generate client code in multiple languages
### Programmatic Access
Download the OpenAPI spec for use in code generation tools:
```bash
# Download IPFS service spec
curl -o ipfs-swagger.json https://ipfs.pinner.xyz/swagger.json
# Download Account service spec
curl -o account-swagger.json https://account.pinner.xyz/swagger.json
```
### Generate TypeScript Clients with Orval
[Orval](https://orval.dev) generates type-safe TypeScript clients from OpenAPI specifications with support for multiple frameworks including React Query, SWR, Angular, and plain fetch.
#### Installation
```bash title="npm"
npm install orval --save-dev
```
```bash title="pnpm"
pnpm add -D orval
```
#### Configuration
Create an `orval.config.ts` file in your project:
```typescript
import { defineConfig } from "orval";
export default defineConfig({
pinner: {
input: "./pinner-api.yaml",
output: {
mode: "tags",
client: "react-query",
target: "src/api",
schemas: "src/api/model",
},
},
});
```
#### Generate API Client
```bash
# Download the OpenAPI spec
curl -o pinner-api.yaml https://ipfs.pinner.xyz/swagger.yaml
# Generate TypeScript client
npx orval
```
#### Framework-Specific Examples
**React Query (default):**
```typescript
import { useQuery, useMutation } from "@tanstack/react-query";
import { getPins, uploadFile } from "./src/api/pinner";
function PinsComponent() {
const { data, isLoading } = useQuery({
queryKey: ["pins"],
queryFn: getPins,
});
// ...
}
```
**SWR:**
```typescript
import useSWR from "swr";
import { getPins } from "./src/api/pinner";
function PinsComponent() {
const { data } = useSWR("/pins", getPins);
// ...
}
```
**Plain Fetch:**
```typescript
import { getPins } from "./src/api/pinner";
async function fetchPins() {
const response = await getPins();
return response.data;
}
```
#### Using with Portal SDK
The [portal-sdk](https://github.com/LumeWeb/web/tree/main/libs/portal-sdk) demonstrates Orval integration with the Account API:
```bash
# From the portal-sdk directory
cd libs/portal-sdk
pnpm orval
```
This generates type-safe clients at `src/account/generated/` with fetch-based HTTP calls.
### See Also
* [Authentication](/api/authentication) - API authentication
* [Small File Uploads](/api/small-uploads) - POST upload method
* [Large File Uploads](/api/large-uploads) - TUS resumable uploads
* [Archive Uploads](/api/archives) - Archive-specific options
* [Upload Limits](/api/limits) - Check account limits
import PinnerDefinition from "../../partials/_pinner-definition-content.mdx";
import WhyNoGateway from "../../partials/_why-no-gateway.mdx";
import GatewayAlternatives from "../../partials/_gateway-alternatives.mdx";
## Frequently Asked Questions
### Product Scope
#### What is Pinner?
#### What does "pinning" mean?
Pinning tells IPFS to keep a copy of content on a specific node. Without pinning, IPFS may garbage collect content that's not frequently accessed. Pinner ensures your important content is always available.
#### What is the difference between pinning and gateways?
| Aspect | Pinning | Gateways |
| --------------- | ------------------------------ | ------------------------ |
| Purpose | Keep content available on IPFS | Serve content via HTTP |
| User experience | Developer-focused (CIDs) | End-user friendly (URLs) |
| Pinner offering | ✅ Core service | ❌ Not by default |
#### Does Pinner provide a gateway?
### Pricing & Value
#### How is Pinner's pricing different?
Many IPFS services bundle gateways with pinning and hide the true cost through cross-subsidization. With Pinner, you pay for pinning—nothing more, nothing less.
#### What is a CID?
A CID (Content Identifier) is a unique address for content on IPFS. It's generated from the content itself, making it tamper-proof. [Learn more](/concepts/cids).
#### What happens if Pinner goes away?
Your content remains on the decentralized network as long as at least one node has it pinned. You can export your pin list and migrate to another service if needed.
### Account & Billing
#### How do I create an account?
Visit [account.pinner.xyz](https://account.pinner.xyz) to sign up.
#### What are the usage limits?
See our [Quotas & Limits](/concepts/quotas) page for details.
#### How do I contact support?
Email [support@pinner.xyz](mailto\:support@pinner.xyz)
import PinnerDefinition from "../../partials/_pinner-definition-content.mdx";
import WhyNoGateway from "../../partials/_why-no-gateway.mdx";
import GatewayAlternatives from "../../partials/_gateway-alternatives.mdx";
## What is Pinner?
### When to Use Pinner
| Use Case | Pinner Right for You? |
| -------------------------------- | --------------------------------------- |
| Archiving important data on IPFS | ✅ Yes |
| Ensuring content availability | ✅ Yes |
| Building dApps with IPFS backend | ✅ Yes |
| Fast content delivery to users | ❌ Consider a CDN |
| Public file sharing via URL | ❌ Consider a gateway service |
| Static website hosting | 🔄 On our roadmap (semi-public gateway) |
### Quick Summary
* **Pinner = Pinning** (keeping content available on decentralized networks)
* **Pinner ≠ Gateway** (serving content to users via HTTP)
* **Gateway available?** Contact us for custom solutions
* **Why no default gateway?** Better pricing, sustainability, focus