# Pinner.xyz Privacy-focused storage without the infrastructure. Store anything. Decentralized, private, no lock-in. Get Started CLI ## Products | | | | |---|---|---| | **IPFS Pinning** | Pin content to IPFS with reliable, transparent pricing. | [Get started →](/ipfs/quickstart) | | **Websites & Domains** | Deploy static sites, SPAs, and dapps on decentralized storage with your own domain. | [Get started →](/hosting/quickstart) | ## What is Pinner? **Pinner is:** a decentralized storage service that pins your content and respects your privacy. Your data lives on a distributed network; no single point of failure, no vendor lock-in. One subscription covers pinning, website hosting, and DNS. **Pinner is not:** a public anonymous IPFS gateway or a lock-in platform. We don't serve arbitrary IPFS content to the public; see [Why No Public Gateway](/ipfs/concepts/why-no-gateway) for the reasoning. Website hosting serves your pinned content through ipfs.pub. *** **Migrating from Pinata?** See the [Pinata migration guide](/migration/pinata) for a side-by-side comparison. **Fleek refugee?** See the [Fleek migration guide](/migration/fleek) to move your DNS and hosted sites to Pinner. # Sign Up & Get Started Get from zero to pinning in three steps. ## Step 1: Create an account Go to [pinner.xyz](https://pinner.xyz) and sign up, or register directly from the CLI (see below). You'll need an email address, name, and password. Email verification is required before you can log in. After signing up, choose a subscription plan in the dashboard. Plans support card payments (via Stripe) and crypto payments (via ATLOS). No ID is required for crypto payments. ## Step 2: Install the CLI and authenticate :::code-group ```bash [macOS / Linux] curl -fsSL https://get.pinner.xyz | sh ``` ```powershell [Windows] irm https://get.pinner.xyz/install.ps1 | iex ``` ::: Then log in; the CLI will walk you through authentication and automatically create an API key for your machine: ```bash pinner auth --email you@example.com ``` Or use the interactive setup wizard for a guided first-time experience: ```bash pinner setup ``` If you already have an auth token from another machine, you can save it directly: ```bash pinner auth YOUR_AUTH_TOKEN ``` ## Step 3: Start pinning ```bash # Upload a file pinner upload ./my-file.txt # Pin by CID pinner pin QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco ``` That's it. You're pinning. *** ### Register from the CLI You can also create a new account entirely from the CLI: ```bash # Interactive (prompts for all fields) pinner register # Non-interactive pinner register --email you@example.com --first-name Jane --last-name Doe --password yourpassword ``` Then confirm your email address: ```bash pinner confirm-email --email you@example.com --token abc123def456 ``` # API Keys ## API key vs auth token Pinner uses two types of credentials. It's important to understand the difference: | | API key | Auth token | |--|---------|------------| | **What it is** | A long-lived JWT (30-day expiry) with audience `api`, created via `pinner auth` or `pinner account api-keys` | A short-lived session JWT with audience `login`, issued during authentication | | **How you use it** | Store it in the `PINNER_AUTH_TOKEN` env var, or pass it as a Bearer token to the API | Not typically used directly; managed internally by the CLI and browser sessions | | **Lifetime** | 30 days from creation (can also have an optional expiry on the key record) | Expires after a session timeout | | **Where it lives** | `PINNER_AUTH_TOKEN` env var, CLI config (`auth_token`), or `Authorization: Bearer` header | Internal to CLI sessions and browser cookies | As a developer, you interact with **API keys**. The term "auth token" refers to the internal session JWT; you rarely need to handle it directly. ## Primary path: CLI Run `pinner auth` to log in and create an API key. The CLI authenticates you, generates a key automatically, and saves the resulting JWT to your local config. You don't need to visit the dashboard to get a key. ```bash # Interactive: prompts for email and password pinner auth # Semi-interactive: provide email, prompted for password (and OTP if 2FA is enabled) pinner auth --email you@example.com # Name your key (useful for tracking which machine it's on) pinner auth --email you@example.com --key-name "ci-pipeline" # Skip API key creation, save the login token directly pinner auth --email you@example.com --no-create-key ``` The default key name is `cli-generated` when `--key-name` is not specified. If a key with the same name already exists, it is replaced automatically. ### Non-interactive (CI/CD) For automated environments, pass credentials via environment variables: ```bash PINNER_EMAIL=you@example.com PINNER_PASSWORD=yourpassword pinner auth ``` With 2FA enabled: ```bash PINNER_EMAIL=you@example.com PINNER_PASSWORD=yourpassword PINNER_OTP=123456 pinner auth ``` Or provide an existing JWT token directly: ```bash pinner auth ``` This is the CI/CD path; prefer the interactive `pinner auth` for local development. ## Managing API keys via CLI The `pinner account api-keys` command provides full lifecycle management of API keys: ```bash # List all API keys pinner account api-keys list # Search keys by name pinner account api-keys list --search my-key # Create a new API key (token is shown once; save it securely) pinner account api-keys create my-key # Delete an API key by UUID or name pinner account api-keys delete my-key # Force-delete the key currently used for authentication pinner account api-keys delete my-key --force ``` The token returned by `create` can be used with `pinner auth --auth-token ` or the `PINNER_AUTH_TOKEN` environment variable. ## API endpoints For programmatic access, API keys can be managed via REST endpoints: | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | `/api/account/keys` | Create an API key (body: `{"name": "my-key"}`) | | `GET` | `/api/account/keys` | List API keys (paginated, with filters and sort) | | `DELETE` | `/api/account/keys/:keyID` | Delete an API key by UUID | All endpoints require an `Authorization: Bearer` header with a valid login JWT. ## Exchanging an API key for a login token API key JWTs have the `api` audience. Some API endpoints require a `login` audience JWT. To exchange an API key for a login JWT: ``` POST /api/auth/key Authorization: APIKey ``` The response returns a `{"token": ""}`. The CLI handles this exchange automatically when needed. ## Fallback: Dashboard manual generation If you can't use the CLI, you can generate an API key in the web dashboard. Go to your account settings and create a new API key. This is the fallback path; prefer `pinner auth` or `pinner account api-keys` instead. ## Environment variables Set your credentials as environment variables so the CLI picks them up automatically: | Environment variable | Purpose | Used by | |----------------------|---------|---------| | `PINNER_AUTH_TOKEN` | Your API key JWT or login token for authentication | SDK, CLI | | `PINNER_EMAIL` | Email address for login | CLI (`pinner auth`) | | `PINNER_PASSWORD` | Password for login (prefer stdin or env var) | CLI (`pinner auth`) | | `PINNER_OTP` | OTP code for 2FA login | CLI (`pinner auth`) | ```bash # Most common: set your API key token export PINNER_AUTH_TOKEN="your-api-key-jwt" # For login flows export PINNER_EMAIL="you@example.com" export PINNER_PASSWORD="yourpassword" ``` The CLI checks these env vars automatically when you don't pass a token directly. The SDK requires you to pass the JWT explicitly; it does not read env vars. ## Authentication method API keys are JWTs signed with Ed25519. When calling the API: * **Authorization header**: `Authorization: Bearer` is the primary method * **Cookie**: The auth middleware also checks the configured auth cookie * **Query parameter**: As a fallback, the token can be passed as a query parameter The SDK uses the `Authorization: Bearer` header by default. ## Scopes All API keys currently grant full account access. Scoped keys are on the roadmap. ## Rotation To rotate an API key: 1. Create a new key with `pinner account api-keys create rotation-$(date +%Y%m%d)` 2. Update your environment variables and deployments with the new token 3. Delete the old key: `pinner account api-keys delete ` Or use `pinner auth` with `--key-name`; it automatically replaces any existing key with the same name. ## Revocation Revoke a compromised or unused key through any of these methods: * **CLI**: `pinner account api-keys delete ` * **API**: `DELETE /api/account/keys/:keyID` * **Dashboard**: Go to your account settings and delete the key If you delete the key currently used for authentication, you must re-authenticate with `pinner auth`. ## 2FA / OTP management Secure your account with one-time passwords. ```bash # Enable 2FA (generates a secret, prompts for OTP verification) pinner account otp enable # Enable 2FA non-interactively (provide the OTP code from your authenticator app) pinner account otp enable --otp 123456 # Disable 2FA (prompts for password) pinner account otp disable # Disable 2FA non-interactively pinner account otp disable --password mypassword ``` When 2FA is active, include your OTP code when authenticating: ```bash PINNER_EMAIL=you@example.com PINNER_PASSWORD=yourpassword PINNER_OTP=123456 pinner auth ``` # Billing & Pricing ## Plans Pinner offers subscription plans with different storage, upload, and download allowances. Visit [pinner.xyz](https://pinner.xyz) for current pricing and plan details. Each plan renews automatically at the cadence you chose (monthly or yearly). Yearly plans are discounted. ## Payment methods Pinner accepts card and cryptocurrency; no identity verification required for crypto payments.
Visa Mastercard American Express Discover USDC USDT DAI PayPal USD Bitcoin Ethereum Solana Litecoin Monero Dogecoin BNB Polygon
| Currency | Type | Notes | |----------|------|-------| | **BTC** | Bitcoin | Broad support, widely available | | **ETH** | Ethereum | Fast confirmations on mainnet | | **SOL** | Solana | Low fees, fast finality | | **LTC** | Litecoin | Low fees, fast settlement | | **XMR** | Monero | Private by default; transactions are untraceable | | **DOGE** | Dogecoin | Low-cost transactions | | **BNB** | BNB | Binance ecosystem | | **POL** | Polygon | Ethereum L2 scaling | | **USDC** | Stablecoin | USD-pegged, price stability | | **USDT** | Stablecoin | USD-pegged, widely supported | | **DAI** | Stablecoin | USD-pegged, decentralized | | **PYUSD** | Stablecoin | USD-pegged, backed by PayPal | :::tip **Privacy-first payments.** Pinner is one of the few storage providers that accepts Monero (XMR). If privacy matters to you, XMR transactions are untraceable by design; no sender, receiver, or amount is visible on-chain. ::: ## Your balance Your account has a credit balance. Payments add credits; subscription renewals and overage usage deduct them. You can view your current balance and transaction history in the dashboard. ## Changing plans Switch plans at any time. You'll get a credit for the unused portion of your current plan, and the new plan is charged for the remaining period. No double-billing. For card payments, plan changes are handled through the Stripe customer portal (linked from the dashboard). ## Cancelling You can cancel from the dashboard. Cancellation takes effect at the end of your current billing period; you keep access until then. You can also undo a scheduled cancellation before it takes effect. ## What happens to my content if I cancel? When you cancel, your pins stay active until the end of the billing period. After that, Pinner stops pinning your content. What happens next depends on whether anyone else is holding your data: * **If your content is pinned elsewhere** by your own IPFS node, another pinning service, or other network peers, it stays available on IPFS. Your CIDs are portable: pin them on another provider with no re-upload. * **If Pinner is the only node holding your content**, it becomes unreachable once Pinner stops pinning it, until you re-pin it somewhere else. This is the reality of any single pinning provider, not just Pinner. We recommend [exporting your pin list](/ipfs/how-to/list-pins) before cancelling, so you know exactly what to re-pin. If availability matters for your production content, consider pinning critical data with at least two providers. Because IPFS is content-addressed and open, there's no lock-in; migration is a pin list export and re-import, not a data migration. ## Support Email [support@pinner.xyz](mailto:support@pinner.xyz) for billing questions. # Quotas & Limits Every Pinner account has quotas that govern how much you can upload, download, and store. These keep the shared infrastructure fair for everyone. ## Quota types | Type | What it limits | |------|---------------| | **Upload** | Bytes you can upload per month | | **Download** | Bytes you can download per month | | **Storage** | Total bytes of pinned content you can store | Limits are evaluated over a monthly window by default. Check the dashboard or API for your current plan's exact limits and usage. ## When you hit a limit If an upload would push you past your storage or upload quota, the request is rejected with an error like `storage quota exceeded` or `upload quota exceeded`. The response includes your current usage, the requested bytes, and your limit so you can see how much headroom remains. ## Checking your usage Use the dashboard to see your current usage and remaining quota. You can also query the API for per-type byte counts and effective limits. ## Need more? Email [support@pinner.xyz](mailto:support@pinner.xyz) to discuss increased allowances or custom plans. # Operations When you upload a file, pin content, or trigger other server-side processing, Pinner creates an operation to track it. Operations let you monitor progress, check whether a request completed, and inspect errors if something went wrong. ## List operations ```bash # List your account operations pinner operations list # Filter by status pinner operations list --status running # Filter by CID pinner operations list --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e # Continuously watch operations pinner operations list --watch ``` ## Get a specific operation ```bash # Get details for one operation pinner operations get # Watch an operation until it completes pinner operations get --watch ``` ## What you'll see Operations track state across uploads, pins, website deployments, and other server-side processing. Each operation has: * **ID**: unique identifier * **Status**: `pending`, `running`, `completed`, `failed`, or `error` * **CID**: the content identifier, when applicable * **Progress**: percentage complete When you run `pinner operations get`, additional details are shown including the updated timestamp, step progress (if multi-step), and any error message. Use `--watch` to continuously monitor operations instead of polling manually. The command refreshes every 2 seconds and exits automatically when all operations reach a terminal status (`completed`, `failed`, or `error`). # Manage DNS Zones DNS zones are required for custom domains on Website Hosting. Before you can add records, you need a zone for your domain. Each zone starts in `pending_nameserver` status and moves to `active` once its nameservers are verified. ## List zones ```bash pinner dns zones list ``` Returns all zones in your account with their ID, domain name, and status. The possible statuses are: * `pending_nameserver`: the zone's nameservers have not yet been verified * `active`: nameserver delegation has been verified ## Create a zone ```bash # Create a zone for your domain pinner dns zones create --domain example.com # Optionally specify custom nameservers pinner dns zones create --domain example.com --nameservers ns1.example.com,ns2.example.com ``` This creates the zone in `pending_nameserver` status. Update your domain registrar to point to the Pinner name servers, then validate the zone (see below). ## Get zone details ```bash # By domain name pinner dns zones get example.com # By numeric zone ID pinner dns zones get 42 ``` Displays the zone's ID, domain, status, and timestamps. ## Validate zone delegation Check whether a zone's nameservers are properly delegated: ```bash pinner dns zones validate example.com ``` Returns a validation result showing whether delegation is valid, a descriptive message, and (if validation fails) the Pinner name servers you should point to. Re-run after updating your registrar until the result shows valid. ## Delete a zone ```bash # By domain name pinner dns zones delete example.com # By numeric zone ID pinner dns zones delete 42 ``` :::warning Deleting a zone removes all records in it. This breaks any custom domains that depend on those records. ::: ## Typical workflow 1. Create a zone for your domain: `pinner dns zones create --domain example.com` 2. Validate the zone: `pinner dns zones validate example.com` 3. Point your domain's NS records to the Pinner name servers shown in the validation output 4. Re-validate until the zone status becomes `active` 5. Add records to the zone (see [Manage DNS Records](/dns/how-to/manage-records)) 6. Use the domain with Website Hosting # Manage DNS Records DNS records link your domain to Pinner services. Common uses: TXT records for domain validation, dnslink TXT records for IPFS content addressing, and ALIAS records for Website Hosting. ## Supported record types The following DNS record types can be created through the API and CLI: | Type | Use case | |------|----------| | A | Map a name to an IPv4 address | | AAAA | Map a name to an IPv6 address | | ALIAS | Map the zone apex to another domain (used by Website Hosting) | | CNAME | Map a subdomain to another domain | | MX | Specify mail servers | | NS | Delegate a subdomain to another set of nameservers | | SRV | Specify services by port and hostname | | TXT | Store arbitrary text (validation tokens, dnslink records, SPF, etc.) | SOA and NS records for the zone apex are managed automatically by PowerDNS and cannot be modified directly. ## List records ```bash # List all records in a zone pinner dns records list example.com ``` ## Create a record ### TXT record (domain validation) ```bash pinner dns records create example.com \ --name "pinner-verify" \ --type TXT \ --content "pinner-verify=abc123def456" ``` ### TXT record (dnslink for IPFS content) ```bash pinner dns records create example.com \ --name "_dnslink" \ --type TXT \ --content "dnslink=/ipfs/bafybeig...cid" ``` dnslink TXT values must follow the `dnslink=/ipfs/` or `dnslink=/ipns/` format per the [dnslink spec](https://dnslink.info/). ### CNAME record (subdomain alias) ```bash pinner dns records create example.com \ --type CNAME \ --name "www" \ --content "example.org" ``` ### ALIAS record (zone apex) ```bash pinner dns records create example.com \ --type ALIAS \ --name "@" \ --content "gateway.pinner.xyz." ``` ALIAS records are used at the zone apex (`@`) where CNAME is not allowed. Website Hosting automatically creates ALIAS records pointing to the gateway domain. ### A record ```bash pinner dns records create example.com \ --type A \ --name "@" \ --content "203.0.113.42" ``` ### AAAA record ```bash pinner dns records create example.com \ --type AAAA \ --name "@" \ --content "2001:db8::1" ``` ### With optional flags ```bash # Set a custom TTL (default: 3600 seconds) pinner dns records create example.com \ --name "www" \ --type CNAME \ --content "example.org" \ --ttl 7200 # Create a disabled record pinner dns records create example.com \ --name "staging" \ --type CNAME \ --content "staging.example.org" \ --disabled ``` ## Get a record ```bash pinner dns records get example.com --name www --type CNAME ``` ## Update a record ```bash pinner dns records update example.com \ --name www \ --type CNAME \ --content "new-site.example.com" ``` You can also update the TTL or disabled state: ```bash pinner dns records update example.com \ --name www \ --type CNAME \ --content "new-site.example.com" \ --ttl 7200 ``` ## Delete a record ```bash pinner dns records delete example.com \ --name www \ --type CNAME ``` List records with `pinner dns records list example.com` to find the names and types of your records. ## Common patterns | Goal | Record type | Name | Content | |------|------------|------|---------| | Prove domain ownership | TXT | `pinner-verify` | `pinner-verify=` | | dnslink (IPFS content mapping) | TXT | `_dnslink` | `dnslink=/ipfs/` | | dnslink (IPNS content mapping) | TXT | `_dnslink` | `dnslink=/ipns/` | | Website Hosting (auto-created) | ALIAS | `@` | Gateway domain | | Subdomain alias | CNAME | `www` | Target domain | | Root domain pointing | A | `@` | IPv4 address | | IPv6 root domain | AAAA | `@` | IPv6 address | # Migrate from Fleek Fleek Hosting shut down on January 31, 2026. If you had a site on Fleek, this guide walks you through getting it back up on Pinner. ## The short version If you already have your build output and just need to get it live: ```bash # 1. Authenticate pinner auth # 2. Launch the wizard: handles upload, website creation, and DNS in one flow pinner websites wizard # Done: your site is live on IPFS ``` ## What you had on Fleek vs. what Pinner provides Fleek Hosting was an all-in-one platform. It combined CDN, IPFS pinning, DNS, SSL, ENS, IPNS, and CI into a single service. Pinner takes a different approach: it handles IPFS pinning, website hosting, DNS, and SSL, but each concern stays independently manageable. If Pinner ever stops serving your needs, your CIDs and your content move with you. | Fleek feature | Pinner equivalent | Notes | |---------------|-------------------|-------| | HTTP CDN with TLS | Pinner gateway + automatic SSL | Your site serves over HTTPS at your domain | | IPFS pinning | Pinner IPFS pinning | Same CIDs: your content identifiers don't change | | IPFS gateway | `ipfs.pub` | No public anonymous gateway; see [Why no public gateway](/ipfs/concepts/why-no-gateway) | | DNSLink | Pinner DNS hosting | Pinner manages DNSLink TXT records automatically | | IPNS | Pinner IPNS | `pinner ipns publish` or SDK equivalent | | ENS | `pinner point` | Maps an onchain name to a CID via IPNS. See [Onchain Names](/onchain/quickstart) | | GitHub Actions CI | `pinner-deploy-action` | Drop-in replacement: see [Deploy with GitHub Actions](/hosting/how-to/deploy-github-actions) | | Dashboard | Pinner web portal | Manage plans, billing, and websites at pinner.xyz | | Domain registration | Bring your own domain | Pinner doesn't sell domains: you register at your registrar and point DNS at Pinner | ## Quick path The fastest way to get your site back up if you have your build output ready. ### 1. Install and authenticate ```bash npm install -g @lumeweb/pinner pinner auth ``` `pinner auth` prompts for your email and password interactively. For CI/CD, pass your API key directly: `pinner auth YOUR_API_KEY`. ### 2. Deploy with the wizard The interactive wizard handles upload, website creation, and DNS setup in one guided flow: closest to the one-click experience you had on Fleek: ```bash # Build your site if you haven't already npm run build # Launch the wizard pinner websites wizard ``` The wizard walks you through: 1. **Content source**: select the directory to upload (e.g. `./dist`) 2. **Target type**: IPFS or IPNS 3. **Domain**: your custom domain or a `*.pinner.xyz` subdomain 4. **DNS mode**: Pinner-managed (recommended) or self-managed 5. **Validation**: verifies DNS records are in place That's it. Your site is live on IPFS.
Manual alternative: if you prefer running commands individually or are scripting the migration ```bash # Upload the build output to IPFS pinner upload ./dist ``` Note the CID from the output. Then create the website: ```bash # For a subdomain (fastest: no DNS setup needed) pinner websites create mysite.pinner.xyz --cid # For a custom domain pinner websites create mysite.example.com --cid ``` If you used a custom domain, Pinner needs to verify you own it. With Pinner-managed DNS (recommended): delegate your domain's nameservers to Pinner, and it handles DNSLink, validation, and ALIAS records automatically. For full DNS setup instructions, see [Migrate DNS from Fleek](/migration/fleek/dns-migration). Wait for SSL: ```bash pinner websites ssl status mysite.example.com ```
## Full path with CI If you had a Fleek GitHub integration that auto-deployed on push, replace it with Pinner's GitHub Action. The workflow is similar: build, upload, deploy. ### Replace your Fleek workflow Your Fleek workflow probably looked something like this: ```yaml # Old Fleek workflow (no longer works) - name: Deploy to Fleek uses: fleek-platform/action@v1 with: apiKey: ${{ secrets.FLEEK_API_KEY }} siteId: ${{ secrets.FLEEK_SITE_ID }} ``` Replace it with: ```yaml - name: Deploy to Pinner uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_API_KEY }} path: ./dist domain: mysite.example.com ``` Set the `PINNER_API_KEY` secret in your repository settings: 1. Go to **Settings → Secrets and variables → Actions** 2. Click **New repository secret** 3. Name: `PINNER_API_KEY` 4. Value: your API key from [API Keys](/account/api-tokens) The action uploads your build output, pins it on IPFS, and creates or updates the website record all in one step. For IPNS publishing, automatic cleanup, and pin-by-CID mode, see the full [GitHub Actions documentation](/hosting/how-to/deploy-github-actions). ## Feature differences to know about A few things work differently on Pinner. Being upfront about these so you're not surprised. ### No public anonymous gateway Fleek provided a public IPFS gateway. Pinner operates `ipfs.pub`, but it's not an anonymous, general-purpose gateway. For details on why and how to access IPFS content, see [Why no public gateway](/ipfs/concepts/why-no-gateway). You can also use [check.ipfs.pub](https://check.ipfs.pub) (Pinner's deploy of check.ipfs.network) to verify your content is available on the IPFS network. ### DNS is separate from domain registration On Fleek, you could register a domain and it was automatically configured. Pinner doesn't sell domains; you register at your preferred registrar and point DNS at Pinner's nameservers. If you already own your domain, this is a one-time setup. ### ENS: use `pinner point` If you had an ENS name pointing at your Fleek site, use `pinner point` to map the ENS name to your new CID via IPNS: ```bash pinner point myname.eth --cid ``` This creates an IPNS key and publishes the CID under it. See [Onchain Names](/onchain/quickstart) for the full flow. ### Your CIDs stay the same This is important: the content identifier (CID) for your files doesn't change when you move providers. IPFS content addressing is provider-independent. If you have CIDs from Fleek, you can pin them directly on Pinner without re-uploading: ```bash pinner pin add QmYourCIDHere ``` Or via the GitHub Action: ```yaml - name: Pin existing CID uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_API_KEY }} cid: QmYourCIDHere ``` ## Self-hosting vs. Pinner The IPFS Foundation published a [modular migration guide](https://blog.ipfs.tech/2026-fleek-migration/) that walks through self-hosting with Kubo + Caddy + Cloudflare DNS + GitHub Actions. That approach gives you maximum control but requires running your own IPFS node and managing infrastructure. Pinner gives you the same modularity without the operational overhead. Your CIDs are yours, your content is portable, and you can switch providers at any time, but you don't have to maintain an IPFS Cluster or a reverse proxy. If self-hosting appeals to you, the Shipyard guide is excellent. If you'd rather focus on building your site than running infrastructure, Pinner handles the IPFS layer for you. ## Migration checklist * \[ ] Install Pinner CLI (`npm install -g @lumeweb/pinner`) * \[ ] Authenticate (`pinner auth`) * \[ ] Upload your site or pin existing CIDs * \[ ] Create the website record * \[ ] Configure DNS (see [DNS migration](/migration/fleek/dns-migration) if using a custom domain) * \[ ] Verify SSL provisioning * \[ ] Replace Fleek GitHub Action with `pinner-deploy-action` * \[ ] Set up `pinner point` if you had ENS names * \[ ] Remove Fleek secrets from your repository * \[ ] Monitor your site for 24-48 hours during DNS propagation ## Next steps * [DNS Migration from Fleek](/migration/fleek/dns-migration): detailed DNS setup and troubleshooting * [Deploy with GitHub Actions](/hosting/how-to/deploy-github-actions): full CI/CD setup * [Custom Domains](/hosting/how-to/custom-domains): domain setup reference * [Migrate from Pinata](/migration/pinata): migrating SDK calls if you were also using Pinata for pinning # DNS Migration from Fleek DNS is the most important part of migrating off Fleek. The good news: if you were on Fleek, you're already managing your own DNS at your registrar. You don't need to switch nameservers; you just need to change what your records point at. ## How Fleek handled DNS On Fleek, you managed DNS at your own registrar (GoDaddy, Namecheap, Cloudflare, etc.). Fleek gave you the record values, and you added them yourself: | Record | What you set on Fleek | |--------|----------------------| | CNAME | Subdomains pointed at Fleek's infrastructure (later upgraded to BunnyCDN) | | ANAME / ALIAS | Root domains pointed at Fleek's infrastructure | | SSL | Automatic: Fleek provisioned Let's Encrypt certificates once DNS was in place | DNSLink was handled differently depending on your setup: * **For custom domains using Fleek's CDN**: your CNAME/ANAME record pointed at Fleek's infrastructure. DNSLink wasn't on your domain; Fleek managed it on their end. * **For DNSLink domains specifically**: Fleek's approach was to CNAME your domain to a Fleek-assigned subdomain (like `yourdomain.on.fleek.co`). The `_dnslink` TXT record lived on Fleek's subdomain, not your custom domain. You just had a CNAME pointing at it. This means you most likely never had a `_dnslink` TXT record on your own domain if you were using Fleek's standard setup. DNSLink was either managed by Fleek on their infrastructure, or you CNAME'd to a Fleek subdomain that had it. ## How Pinner handles DNS Pinner gives you two options: | Option | How it works | Best for | |--------|--------------|----------| | **Pinner-managed DNS** | Delegate nameservers to Pinner. Pinner creates and manages DNSLink, validation, and ALIAS records automatically. Updates when you deploy. | People who want the "set it and forget it" experience: closest to what you might have wanted from Fleek | | **Self-managed DNS** | Keep your DNS at your registrar (just like Fleek). You add and maintain the DNS records yourself. | People who want to stay at Cloudflare, Route53, or another DNS provider | If you were comfortable managing DNS records on Fleek, the self-managed path will feel familiar: same workflow, different values. If you'd rather not touch DNS records anymore, use Pinner-managed DNS. ## Step-by-step: Switch to Pinner-managed DNS Delegate nameservers to Pinner, and it handles everything automatically. This is the lowest-effort path. ### 1. Create the website on Pinner ```bash pinner websites create mysite.example.com --cid ``` By default, Pinner enables DNS hosting for new websites: it creates a DNS zone and the required records automatically. ### 2. Get Pinner's nameservers ```bash pinner websites config ``` This shows the nameservers Pinner assigned to your domain. ### 3. Switch nameservers at your registrar Go to the same registrar where you managed DNS for Fleek, and update the nameservers to Pinner's. This is the step that causes downtime. DNS propagation takes anywhere from a few minutes to 48 hours. Your site may be unreachable during this window. **To minimize downtime:** Do the nameserver switch during a low-traffic period. Pinner's DNS records are already in place, so the moment the new nameservers propagate, your site starts working on Pinner. ### 4. Verify the switch After changing nameservers, confirm the delegation: ```bash dig NS mysite.example.com ``` You should see Pinner's nameservers in the response. If you still see your registrar's default nameservers, propagation isn't complete yet. ### 5. Validate on Pinner ```bash pinner websites validate mysite.example.com ``` ### 6. Wait for SSL SSL provisions automatically after DNS is confirmed: ```bash pinner websites ssl status mysite.example.com ``` ## Step-by-step: Switch to self-managed DNS Stay at your current DNS provider. Same approach you used with Fleek; just update the record values to point at Pinner instead. ### 1. Create the website with DNS hosting disabled ```bash pinner websites create mysite.example.com --cid --no-dns-hosting ``` ### 2. Update your DNS records At your DNS provider, replace the Fleek values with Pinner values: | Record | Old value (Fleek) | New value (Pinner) | |--------|-------------------|-------------------| | CNAME or ALIAS | Fleek's CDN target | Pinner gateway domain | | TXT `_dnslink` | Probably absent: Fleek managed DNSLink on their end | `dnslink=/ipfs/` or `dnslink=/ipns/` | If you were using Fleek's standard DNS setup, you likely had a CNAME pointing at Fleek's infrastructure, and Fleek handled DNSLink internally. In that case, the `_dnslink` TXT record is new: Pinner needs it on your domain (unless you use Pinner-managed DNS, which handles it automatically). Get the Pinner gateway domain from: ```bash pinner websites config ``` For IPNS targets (recommended if your site updates frequently): * DNSLink value: `dnslink=/ipns/`: the domain always resolves to the latest content For direct IPFS targets: * DNSLink value: `dnslink=/ipfs/`: you'll need to update this record each time you deploy ### 3. Add validation TXT record The website response includes a `validation_record_host` and `validation_token`. Add: | Record | Name | Value | |--------|------|-------| | TXT | (from `validation_record_host`) | (from `validation_token`) | ### 4. Validate and wait for SSL ```bash pinner websites validate mysite.example.com pinner websites ssl status mysite.example.com ``` ## Removing the old Fleek records Once Pinner is serving your site, remove the old Fleek DNS records at your registrar: * Delete the CNAME/ALIAS record pointing at Fleek's infrastructure * You likely don't have a `_dnslink` TXT record to remove; Fleek managed that on their end * Keep any non-Fleek records (email, other subdomains, etc.) If you're using Pinner-managed DNS, this is handled automatically; delegating the nameservers replaces all the records at once. ## What's different from Fleek | | Fleek | Pinner | |---|---|---| | DNS management | You add CNAME/ANAME records at your registrar | Pinner-managed DNS (automatic) or self-managed (same as Fleek) | | DNSLink | Manual opt-in via domain settings | Automatic with Pinner-managed DNS; self-managed if you handle DNS yourself | | SSL | Automatic (Let's Encrypt) | Automatic (Caddy on-demand TLS) | | Domain verification | TXT record at registrar | TXT record at registrar (same pattern) | | Record type for root domain | ANAME | ALIAS | | Record type for subdomain | CNAME | CNAME | ## Troubleshooting ### Validation keeps failing with `dns_missing` DNS propagation can take up to 48 hours (usually faster: 5 to 30 minutes). Verify the DNSLink and verification TXT records exist: ```bash dig TXT _dnslink.mysite.example.com dig TXT ``` ### Validation fails with `dns_mismatch` The DNSLink TXT record was found but doesn't match the expected target. Check that the `_dnslink` TXT record value is correct; it should be `dnslink=/ipfs/` or `dnslink=/ipns/`. ### SSL is stuck in `pending` or `failed` SSL can only provision after DNS is fully resolved. Check that: 1. The DNSLink record resolves correctly (for self-managed DNS) 2. Nameserver delegation is complete (for Pinner-managed DNS) 3. The validation TXT record is in place Then check SSL status: ```bash pinner websites ssl status mysite.example.com ``` ### I had multiple domains on Fleek Each domain is independent. Update the DNS records for each domain separately, the same way you set them up on Fleek. ### I had DNSLink set up on Fleek: do I need a `_dnslink` record now? Probably yes. On Fleek, DNSLink was handled on Fleek's side; your domain either CNAME'd to a Fleek subdomain that had the `_dnslink` TXT record, or Fleek managed it internally. Either way, the `_dnslink` TXT record was not on your custom domain. With Pinner, DNSLink needs to resolve from your domain (unless you use Pinner-managed DNS, which handles it automatically). For self-managed DNS, add a TXT record at `_dnslink.yourdomain.com` with the value `dnslink=/ipfs/` or `dnslink=/ipns/`. ## Next steps * [Deploy with GitHub Actions](/hosting/how-to/deploy-github-actions): automate deploys on push * [IPNS](/hosting/concepts/ipns): use IPNS for updatable content addresses * [Custom Domains](/hosting/how-to/custom-domains): full domain setup reference # Migrate from Pinata ## Why migrate? * **Decentralized infrastructure**: no single point of failure, no lock-in * **Competitive pricing**: pay for pinning, not bundled gateway infrastructure * **Privacy-focused**: your data, your control * **Compatibility adapter**: swap the SDK, keep most of your call patterns ## Migration strategy Approach | Best for | Effort | \----------|----------|--------| **Pinata Adapter** | Quick migration, minimal code changes | Low | **Native Pinner API** | Long-term projects, better features | Medium | :::tip Start with the adapter to ship fast, then migrate to the native API when you want Pinner-specific features. ::: ## Feature compatibility The adapter provides broad Pinata API coverage, but some features are not supported because Pinner's architecture differs from Pinata's: Feature | v2 Adapter | Legacy Adapter | Notes | \---------|:----------:|:--------------:|-------| Public file upload | ✅ | ✅ | `file`, `fileArray`, `base64`, `json` | Pin by CID | ✅ | ✅ | | Private uploads | ❌ | ❌ | Throws "not supported" | URL upload | ❌ | N/A | Throws "not supported" in v2 | File listing | ✅ | ✅ | Pagination filters limited | Get file by ID | ✅ | ✅ | | Delete / unpin | ✅ | ✅ | | Update metadata | ✅ | ✅ | | Pin queue | ✅ | ✅ | Returns pinned items (no true queue) | Groups | ⚠️ | N/A | `list()` returns empty; create/add/remove throw | Gateways | ✅ | N/A | Public gateways only; private throws | Signed URLs | ❌ | ⚠️ | v2 throws; legacy returns gateway URL fallback | Swap CID | ❌ | ❌ | Throws "not supported" | Analytics | ⚠️ | ⚠️ | Returns empty data (not real analytics) | :::warning If your app uses private uploads, groups, swap CID, or signed URLs, those calls will throw runtime errors through the adapter. You must remove or replace them before migrating. ::: For the full adapter method reference, see [Adapter Reference](/migration/pinata/adapter-reference). ## Adapter setup ### Install Pinner ```bash npm install @lumeweb/pinner ``` ### Pick your adapter **Pinata SDK 2.x**: use `pinataAdapter`: ```typescript import { Pinner, pinataAdapter } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const adapter = pinataAdapter(pinner); ``` **Pinata SDK 1.x**: use `pinataLegacyAdapter`: ```typescript import { Pinner, pinataLegacyAdapter } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const adapter = pinataLegacyAdapter(pinner); ``` ## Step-by-step: replace initialization 1. **Replace the import.** Swap `pinata` for `@lumeweb/pinner` and import the adapter. 2. **Replace the constructor.** Instead of `new PinataSDK({ pinataJwt })`, use `new Pinner({ jwt })` + adapter. 3. **Handle unsupported features.** Remove or replace calls to private uploads, groups, swap CID, signed URLs, and URL uploads (see [Feature compatibility](#feature-compatibility)). 4. **Add `.execute()` to v2 builder methods.** The v2 adapter uses a builder pattern: chain `.name()` and `.keyvalues()`, then call `.execute()`. 5. **Test uploads and listings.** Verify your existing calls work with the adapter. ## Error handling Pinner errors use a different structure. Instead of catching generic errors: ```typescript import { PinnerError } from "@lumeweb/pinner"; try { const result = await adapter.upload.public.file(file).execute(); } catch (error) { if (error instanceof PinnerError) { console.error("Code:", error.code); console.error("Message:", error.message); console.error("Retryable:", error.retryable); } } ``` `PinnerError` has the following properties: Property | Type | Description | \----------|------|-------------| `code` | `string` | Machine-readable error code (e.g. `"UPLOAD_ERROR"`, `"PIN_ERROR"`) | `message` | `string` | Human-readable error message | `retryable` | `boolean` | Whether the operation can be retried | See the [Error Reference](/ipfs/reference/errors) for all error codes and retry guidance. ## Migration checklist * \[ ] Install `@lumeweb/pinner` * \[ ] Update imports and initialization * \[ ] Remove or replace unsupported features (private uploads, groups, swap CID, signed URLs, URL uploads) * \[ ] Add `.execute()` to builder methods (v2 adapter only) * \[ ] Test file uploads * \[ ] Test file listing and deletion * \[ ] Update error handling * \[ ] Remove Pinata SDK dependency * \[ ] Deploy and monitor ## 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. **Test unsupported feature handling**: confirm your app gracefully handles `notSupported` errors 5. **Monitor performance**: compare upload speeds 6. **Review costs**: check your billing ## Next steps * [Adapter Reference](/migration/pinata/adapter-reference): full v2 and legacy adapter method docs * [Code Examples](/migration/pinata/examples): side-by-side Pinata vs Pinner code * [SDK Getting Started](/ipfs/sdk/getting-started): using Pinner without the adapter # Pinata Adapter Reference Complete method reference for the Pinata compatibility adapters. For setup instructions, see the [Migration Guide](/migration/pinata). ## v2 Adapter The v2 adapter mirrors the Pinata SDK 2.x structure with `public`/`private` namespaces. It uses a builder pattern: chain methods and call `.execute()`. ### Upload ```typescript // Upload a single file const result = await adapter.upload.public.file(file) .name("my-file") .keyvalues({ category: "image" }) .execute(); // Upload JSON const jsonResult = await adapter.upload.public.json({ hello: "world" }) .name("data") .execute(); // Upload multiple files (directory) const dirResult = await adapter.upload.public.fileArray([file1, file2]) .name("my-dir") .execute(); // Pin by CID const pinResult = await adapter.upload.public.cid("Qm...").execute(); // Base64 upload const b64Result = await adapter.upload.public.base64(base64String).execute(); ``` ### Files ```typescript // List files (returns a thenable builder) const files = await adapter.files.public.list() .name("my-file") .limit(10) .order("DESC"); // Get a specific file const file = await adapter.files.public.get("Qm..."); // Delete files await adapter.files.public.delete(["Qm..."]); // Update file metadata await adapter.files.public.update({ id: "Qm...", keyvalues: { tags: "updated" } }); // Pin queue const queue = await adapter.files.public.queue().limit(10); ``` ### Gateways ```typescript // Get gateway info for a CID const info = adapter.gateways.public.get("Qm..."); console.log(info.url); // "https://QmExample.ipfs.inbrowser.link" // Convert IPFS URL to gateway URL const gatewayUrl = await adapter.gateways.public.convert("ipfs://Qm..."); ``` ## Legacy Adapter The legacy adapter uses flat method names matching Pinata SDK 1.x. No builder pattern; pass options directly. ```typescript // Upload a file const result = await adapter.pinFileToIPFS(file, { metadata: { name: "my-file", keyvalues: { category: "image" } } }); // Upload JSON const jsonResult = await adapter.pinJSONToIPFS({ hello: "world" }, { metadata: { name: "data" } }); // Pin by CID const pinResult = await adapter.pinByHash("Qm...", { metadata: { name: "pinned-file" } }); // List pinned files const { files } = await adapter.pinList({ limit: 10 }); // Unpin await adapter.unpin("Qm..."); // Update metadata await adapter.hashMetadata("Qm...", { tags: "updated" }); // Pin jobs const { rows } = await adapter.pinJobs({ limit: 10 }); ``` ## Unsupported features Some Pinata features throw when called through the adapter because Pinner has no equivalent: Feature | v2 Adapter | Legacy Adapter | What to do instead | \---------|:----------:|:--------------:|-------------------| Private uploads | Throws `notSupported` | Throws `notSupported` | Use Pinner's native API | Groups | `list()` returns empty; create/add/remove throw | N/A | Use key-value metadata for categorisation | Signed URLs | Throws `notSupported` | Returns gateway URL fallback | Use public gateway URLs | | Swap CID | Throws `notSupported` | Throws `notSupported` | Not available; delete and re-pin | URL uploads | Throws `notSupported` | N/A | Fetch the URL yourself and upload the file | Analytics | Returns empty data | Returns empty data | Not available | ## Next steps * [Code Examples](/migration/pinata/examples): side-by-side Pinata vs Pinner vs native API code * [Migration Guide](/migration/pinata): setup and step-by-step instructions * [SDK Getting Started](/ipfs/sdk/getting-started): using Pinner without the adapter # Pinata Code Examples Side-by-side code for every common operation. For setup instructions, see the [Migration Guide](/migration/pinata). For the full adapter method reference, see [Adapter Reference](/migration/pinata/adapter-reference). ## Pinata SDK 2.x ### File Upload ```typescript // Pinata SDK 2.x 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); ``` ```typescript // Pinner with Adapter import { Pinner, pinataAdapter } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); 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 ```typescript // Pinata SDK 2.x const data = { name: "test", value: 123 }; const upload = await pinata.upload.public.json(data); ``` ```typescript // Pinner with Adapter const data = { name: "test", value: 123 }; const upload = await adapter.upload.public.json(data) .name("data.json") .execute(); ``` ### Base64 Upload ```typescript // Pinata SDK 2.x const base64String = "SGVsbG8gV29ybGQh"; const upload = await pinata.upload.public.base64(base64String); ``` ```typescript // Pinner with Adapter const base64String = "SGVsbG8gV29ybGQh"; const upload = await adapter.upload.public.base64(base64String) .name("file.txt") .execute(); ``` ### Pin by CID ```typescript // Pinata SDK 2.x const pin = await pinata.upload.public.cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"); ``` ```typescript // Pinner with Adapter const pin = await adapter.upload.public.cid("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco") .execute(); ``` ### List Files ```typescript // Pinata SDK 2.x const files = await pinata.files.public.list() .limit(10) .all(); for (const file of files) { console.log(file.name, file.cid); } ``` ```typescript // Pinner with Adapter const files = await adapter.files.public.list() .limit(10) .all(); for (const file of files) { console.log(file.name, file.cid); } ``` ### Directory Upload ```typescript // Pinata SDK 2.x const files = [ new File(["content"], "file1.txt"), new File(["content"], "file2.txt"), ]; const upload = await pinata.upload.public.fileArray(files); ``` ```typescript // Pinner with Adapter 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 ```typescript // Pinata SDK 2.x await pinata.files.public.delete(["QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"]); ``` ```typescript // Pinner with Adapter await adapter.files.public.delete(["QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"]); ``` ## Pinata SDK 1.x Legacy ### File Upload ```typescript // Pinata SDK 1.x const file = new File(["Hello!"], "hello.txt", { type: "text/plain" }); const upload = await pinata.pinFileToIPFS(file); console.log("IpfsHash:", upload.IpfsHash); ``` ```typescript // Pinner with Legacy Adapter import { Pinner, pinataLegacyAdapter } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); const adapter = pinataLegacyAdapter(pinner); const file = new File(["Hello!"], "hello.txt", { type: "text/plain" }); const upload = await adapter.pinFileToIPFS(file); console.log("CID:", upload.cid); ``` ### JSON Upload ```typescript // Pinata SDK 1.x const data = { name: "test", value: 123 }; const upload = await pinata.pinJSONToIPFS(data); ``` ```typescript // Pinner with Legacy Adapter const data = { name: "test", value: 123 }; const upload = await adapter.pinJSONToIPFS(data); ``` ### Pin by CID ```typescript // Pinata SDK 1.x const pin = await pinata.pinByHash("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"); ``` ```typescript // Pinner with Legacy Adapter const pin = await adapter.pinByHash("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"); ``` ### List Pins ```typescript // Pinata SDK 1.x const pins = await pinata.pinList({ limit: 10 }); for (const pin of pins.files) { console.log(pin.ipfs_pin_hash); } ``` ```typescript // Pinner with Legacy Adapter const pins = await adapter.pinList({ limit: 10 }); for (const pin of pins.files) { console.log(pin.cid); } ``` ### Unpin ```typescript // Pinata SDK 1.x await pinata.unpin("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"); ``` ```typescript // Pinner with Legacy Adapter await adapter.unpin("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco"); ``` ### Update Metadata ```typescript // Pinata SDK 1.x await pinata.hashMetadata("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco", { project: "my-app", version: "2.0" }); ``` ```typescript // Pinner with Legacy Adapter await adapter.hashMetadata("QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco", { project: "my-app", version: "2.0" }); ``` ## Native Pinner API Skip the adapter and use Pinner directly for the best experience. ### File Upload ```typescript import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); const file = new File(["Hello!"], "hello.txt", { type: "text/plain" }); const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ### Upload with Progress ```typescript 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`); } }); const result = await operation.result; console.log("CID:", result.cid); 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 ```typescript // Pinata SDK try { const upload = await pinata.upload.public.file(file); } catch (error) { console.error("Pinata error:", error.message); } ``` ```typescript // Pinner SDK import { 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); } } ``` # Quickstart Choose your path; both get a file pinned to IPFS in under 5 minutes. :::code-group ```bash [CLI] # 1. Install (macOS / Linux) curl -fsSL https://get.pinner.xyz | sh # Windows: iex (irm https://get.pinner.xyz/install.ps1) # 2. Set up (walks you through auth + config) pinner setup # 3. Upload a file pinner upload myfile.txt # Done: your CID is printed to the terminal ``` ```typescript [SDK] npm install @lumeweb/pinner import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); // Upload and wait for the CID const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ::: ## Go deeper * [CLI install & setup guide](/ipfs/cli/getting-started): all install methods, flags, troubleshooting * [SDK getting started](/ipfs/sdk/getting-started): install, configure, first upload, type your config * [Understand CIDs](/ipfs/concepts/cids) * [Check if your content is pinned](/ipfs/how-to/check-pin-status) # Upload a Single File Pin a file to IPFS and get its Content Identifier (CID). This is the most common upload: a single file, named, and confirmed. ## Quick upload :::code-group ```bash [CLI] pinner upload myfile.txt ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const file = new File(["Hello, IPFS!"], "hello.txt", { type: "text/plain" }); const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ::: The CLI prints the CID, gateway URL, size, and upload time. The SDK returns an `UploadResult` with the same fields. ## Name your upload By default the CLI uses the filename and the SDK uses the `File` object's name. Override it when you want a different name on the pin record. :::code-group ```bash [CLI] pinner upload ./report.pdf --name "Q4 Report" ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const file = new File([buffer], "report.pdf", { type: "application/pdf" }); const result = await pinner.uploadAndWait(file, { name: "Q4 Report" }); console.log("CID:", result.cid); ``` ::: ## Add metadata Attach key-value pairs for organisation and search. Metadata is returned when you [list pins](/ipfs/how-to/list-pins). :::code-group ```bash [CLI] pinner upload logo.svg --meta project=acme --meta env=prod ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const file = new File([svgData], "logo.svg", { type: "image/svg+xml" }); const result = await pinner.uploadAndWait(file, { keyvalues: { project: "acme", env: "prod" }, }); console.log("CID:", result.cid); ``` ::: ## Fire-and-forget vs. wait for confirmation By default both the CLI and `uploadAndWait` wait until the pin is confirmed. For large files or CI pipelines, you can skip the wait. :::code-group ```bash [CLI] pinner upload bigfile.tar.gz --no-wait ``` ```typescript [SDK – fire and forget] const operation = await pinner.upload(file); // Don't await operation.result; the upload is in progress console.log("Upload started, operation ID:", operation.progress); // Later, poll for the result: const result = await operation.result; console.log("CID:", result.cid); ``` ```typescript [SDK – wait with custom polling] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const operation = await pinner.upload(file); const result = await pinner.waitForOperation(operation, { interval: 3000, timeout: 120000, }); console.log("CID:", result.cid); ``` ::: ## Use the builder pattern The SDK's builder pattern chains upload options in a fluent style. ```typescript import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const operation = await pinner.upload .file(file) .name("config.json") .keyvalues({ team: "platform" }) .pin(); const result = await operation.result; console.log("CID:", result.cid); ``` ## Node.js: read from the filesystem In Node.js 20+, the global `File` class is available. Read a file from disk and upload it: ```typescript import { Pinner } from "@lumeweb/pinner"; import { readFileSync } from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const buffer = readFileSync("./data.csv"); const file = new File([buffer], "data.csv", { type: "text/csv" }); const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ## Upload from stdin The CLI can read from a pipe: ```bash cat config.json | pinner upload --name "config.json" ``` When no file path is given and stdin is piped, the CLI reads from the pipe. The default name is `"stdin"`; override it with `--name`. ## Next steps * [Upload an entire directory](/ipfs/how-to/upload-directory) * [Upload CAR files for pre-calculated CIDs](/ipfs/how-to/upload-car) * [Check the status of your pin](/ipfs/how-to/check-pin-status) * [Add and update metadata on existing pins](/ipfs/how-to/manage-metadata) # Upload a Directory Upload multiple files as an IPFS directory and get the root CID that addresses the whole structure. ## Upload the directory :::code-group ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const files = [ new File(["Hello, IPFS!"], "hello.txt", { type: "text/plain" }), new File(['{"name": "config"}'], "config.json", { type: "application/json", }), ]; const operation = await pinner.uploadDirectory(files); const result = await operation.result; console.log("Root CID:", result.cid); ``` ```bash [CLI] # Upload an entire directory pinner upload ./my-directory/ # The root CID, gateway URL, size, and duration are printed to the terminal ``` ::: ## What is the root CID? When you upload a directory, IPFS creates a tree of CIDs: one for each file, plus intermediate nodes. The **root CID** addresses the top of that tree. With the root CID alone you can reach every file inside the directory. ## Verify the upload You can check that the content is reachable through a public gateway: ``` https://.ipfs.inbrowser.link/hello.txt ``` Or check the pin status: :::code-group ```typescript [SDK] const pinInfo = await pinner.pins.get(result.cid); console.log(pinInfo.status); ``` ```bash [CLI] pinner status ``` ::: ## Next steps * [Upload CAR files for pre-calculated CIDs](/ipfs/how-to/upload-car) * [Understand how CIDs work](/ipfs/concepts/cids) # Upload CAR Files CAR (Content Addressable Archive) files let you upload IPFS data with the CID already known before the upload even starts. This is useful for bulk uploads and when you need to guarantee the CID ahead of time. Not sure what CAR files are or why you'd use them? See [CAR Files](/ipfs/concepts/car-files). ## Upload a CAR file :::code-group ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const carData = fs.readFileSync("./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); ``` ```typescript [SDK – Builder] import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const carData = fs.readFileSync("./data.car"); const carFile = new File([carData], "data.car", { type: "application/vnd.ipld.car", }); const result = await pinner.upload .raw(carFile) .name("data.car") .waitForOperation(true) .pin(); // result is an UploadResult with .cid console.log("Root CID:", result.cid); ``` ```bash [CLI] pinner upload data.car ``` ::: ### ReadableStream input `uploadCar` also accepts a `ReadableStream`, which is useful in browser environments or when streaming CAR data without buffering the entire file: ```typescript const stream: ReadableStream = getCarStream(); const operation = await pinner.uploadCar(stream, { name: "data.car", size: carByteLength, }); ``` When providing a `ReadableStream`, set `name` and `size` in the options so the upload handler can choose the right protocol (XHR vs. TUS resumable) and apply the correct MIME type. ### Marking regular uploads as CAR If you use the generic `pinner.upload()` method (not `uploadCar`), you can pass `isCarFile: true` in the options to skip CAR preprocessing and upload the file as-is: ```typescript await pinner.upload(carFile, { isCarFile: true }); ``` This is useful when you already have a CAR file and don't want the SDK to re-process it into another CAR. ## Why CAR instead of a regular upload? With a CAR file the root CID is deterministic; it's computed from the IPFS block structure before the upload begins. You can verify the CID locally and know it won't change after pinning. Regular uploads also work fine. CAR is for cases where you need the CID upfront: deterministic builds, verification pipelines, or bulk transfers of pre-packaged IPFS data. ## Next steps * [Learn how CAR files work](/ipfs/concepts/car-files) * [Upload a directory](/ipfs/how-to/upload-directory) # List & Search Pins See what you've pinned, filter by name or status, and paginate through large lists of pins. ## List all pins :::code-group ```bash [CLI] pinner pins ls ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pins = await pinner.listPins(); for (const pin of pins) { console.log(`${pin.cid} ${pin.name ?? "(unnamed)"} ${pin.status}`); } ``` ::: The CLI prints a table with CID, name, status, and created date. The SDK returns an array of `RemotePin` objects. ## Filter by name Find pins whose name matches a string. :::code-group ```bash [CLI] pinner pins ls --name "logo" ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pins = await pinner.listPins({ name: "logo" }); ``` ::: ## Filter by status Show only pins in a specific state. Status values: `queued`, `pinning`, `pinned`, `failed`. :::code-group ```bash [CLI] pinner pins ls --status failed ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pins = await pinner.listPins({ status: ["failed"] }); ``` ::: Combine name and status filters: ```bash pinner pins ls --name "deploy" --status pinned ``` ## Limit results Control how many results come back per page. The CLI defaults to 10; the SDK defaults to the server's page size. :::code-group ```bash [CLI] pinner pins ls --limit 50 ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pins = await pinner.listPins({ limit: 50 }); ``` ::: ## Paginate with the SDK `listPins()` returns a flat array; fine for most cases. When you need more control (streaming large result sets, custom pagination), use the async generator directly: ```typescript import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); // Stream pins one at a time for await (const pin of pinner.pins.ls()) { console.log(pin.cid, pin.name, pin.status); // Break early if you only need a window if (someCondition) break; } ``` The generator handles pagination internally; it fetches the next page when you exhaust the current one. Pass `cursor` to resume from a specific point: ```typescript for await (const pin of pinner.pins.ls({ cursor: previousCursor })) { // picks up where you left off } ``` ## Watch for status changes The CLI can continuously monitor pin status: ```bash pinner pins ls --watch ``` The table refreshes in place, showing the current status of each pin. Useful when you're waiting for bulk pins to settle. Press `Ctrl+C` to stop. ## Look up a single pin by CID If you already know the CID, use `pins.get` instead of listing and filtering: :::code-group ```bash [CLI] pinner pins status bafybeig... ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; import { CID } from "multiformats"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pin = await pinner.pins.get(CID.parse("bafybeig...")); console.log(pin.status); ``` ::: See [Check Pin Status](/ipfs/how-to/check-pin-status) for the full guide. ## What you'll see Each pin has these fields: | Field | Description | |-------|-------------| | `cid` | The Content Identifier for the pinned data | | `name` | Human-readable name (if set during upload) | | `status` | Current state: `queued`, `pinning`, `pinned`, or `failed` | | `created` | When the pin was created | | `metadata` | Key-value pairs attached to the pin | For a deeper explanation of what each status means and how pins transition between them, see [Pin Lifecycle](/ipfs/concepts/pin-lifecycle). ## Next steps * [Check a specific pin's status](/ipfs/how-to/check-pin-status) * [Remove pins you no longer need](/ipfs/how-to/delete-pins) * [Add and update metadata on pins](/ipfs/how-to/manage-metadata) # Check Pin Status Find out whether your content is queued, pinning, pinned, or failed. :::code-group ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pin = await pinner.pins.get(CID.parse("bafybeig...")); console.log(pin.status); // → "pinned" ``` ```bash [CLI] pinner pins status bafybeig... ``` ::: ## What the statuses mean | Status | Meaning | |--------|---------| | `queued` | Waiting to be pinned | | `pinning` | Pin is being processed | | `pinned` | Stored and retrievable | | `failed` | Pinning failed; check the error message | For a deeper explanation of these states, see [Pin Lifecycle](/ipfs/concepts/pin-lifecycle). # Delete / Unpin Content Remove a pin by its CID. Once unpinned, the content is no longer stored by Pinner and may become unavailable on the IPFS network. :::info Unpinning removes the content from Pinner's storage. If other nodes on the IPFS network still pin the same CID, the content remains accessible through those nodes. If your CID is only pinned by Pinner, it may become unreachable after unpinning. ::: ## Unpin a single CID :::code-group ```bash [CLI] pinner pins rm bafybeig... --force ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); await pinner.unpin("bafybeig..."); ``` ::: The CLI prints `Unpinned CID: ` on success. The SDK method resolves when the pin is removed. Without `--force`, the CLI prompts for confirmation: ```bash pinner pins rm bafybeig... # → "Are you sure you want to unpin bafybeig...?" ``` ## Unpin multiple CIDs Remove several pins in one command. The CLI accepts space-separated CIDs. :::code-group ```bash [CLI] pinner pins rm bafybeig...abc bafybeig...def --force --parallel 5 ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const cids = ["bafybeig...abc", "bafybeig...def"]; for (const cid of cids) { await pinner.unpin(cid); console.log(`Unpinned ${cid}`); } ``` ::: The CLI's `--parallel` flag controls how many unpin operations run concurrently. `--continue` keeps going even if some operations fail; for example: ```bash pinner pins rm cid1 cid2 cid3 --force --parallel 3 --continue ``` ## Unpin from a file Read CIDs from a text file (one per line): ```bash pinner pins rm --file cids-to-remove.txt --force ``` ## Unpin by request ID If you have a pin request ID instead of a CID (for example, from a failed upload), remove it by request ID: ```typescript await pinner.unpinByRequestId("request-id-here"); ``` The CLI does not expose request IDs directly; use `pinner pins ls` to find the CID, then unpin by CID. ## Unpin all pins Remove every pin at once. Requires `--force` for safety. ```bash pinner pins rm --all --force ``` Combine with `--status` to unpin only pins in a specific state: for example, remove all failed pins: ```bash pinner pins rm --all --status failed --force ``` ## Dry run Preview what would be removed without making any changes: ```bash pinner pins rm bafybeig... --dry-run ``` The CLI prints the operations it would perform, including the endpoint and CIDs, but does not actually unpin anything. ## Next steps * [List your pins](/ipfs/how-to/list-pins) * [Check a specific pin's status](/ipfs/how-to/check-pin-status) * [Understand the pin lifecycle](/ipfs/concepts/pin-lifecycle) # Batch Pin Files Pin many files or CIDs without running commands one at a time. ## Pin from a list of CIDs :::code-group ```bash [CLI] # One CID per line in a text file pinner pins add --file cids.txt # Multiple CIDs as arguments pinner pins add bafybeig...abc bafybeig...def --parallel 5 # Pipe from stdin echo "bafybeig...abc" | pinner pins add ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const cids = fs.readFileSync("cids.txt", "utf-8") .split("\n") .filter(Boolean); for (const cid of cids) { try { const generator = pinner.pins.add(cid); for await (const pinned of generator) { console.log(`Pinned ${pinned}`); } } catch (err) { console.error(`Failed to pin ${cid}:`, err); } } ``` ::: ## Upload multiple files as a directory :::code-group ```bash [CLI] # Upload every file in a directory (creates a single root CID) pinner upload ./my-files/ ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; import path from "path"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const dir = "./my-files"; const files = fs.readdirSync(dir).map((name) => { const buffer = fs.readFileSync(path.join(dir, name)); return new File([buffer], name); }); const operation = await pinner.uploadDirectory(files); const result = await operation.result; console.log("Root CID:", result.cid); ``` ::: # Manage Pin Metadata Attach key-value pairs to your pins for organisation and search. ## Set metadata :::code-group ```bash [CLI] pinner pins update bafybeig... --meta project=acme --meta env=prod ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); await pinner.pins.setMetadata(cid, { project: "acme", env: "prod" }); ``` ::: ## Clear metadata Use `--clear-meta` to remove all existing metadata before setting new values. :::code-group ```bash [CLI] # Clear all metadata pinner pins update bafybeig... --clear-meta # Clear and set fresh values in one command pinner pins update bafybeig... --clear-meta --meta fresh=start ``` ```typescript [SDK] // Clear all metadata by passing undefined await pinner.pins.setMetadata(cid, undefined); ``` ::: ## Read metadata :::code-group ```bash [CLI] pinner pins status bafybeig... ``` ```typescript [SDK] const pin = await pinner.pins.get(cid); console.log(pin.metadata); ``` ::: # Automate Pinning with GitHub Actions Automatically upload build artifacts to IPFS every time you push to your repository. ## Set up secrets Add your Pinner API key as a repository secret: 1. Go to **Settings → Secrets and variables → Actions** 2. Click **New repository secret** 3. Name: `PINNER_AUTH_TOKEN` 4. Value: your API key from [API Keys](/account/api-tokens) ## Create the workflow Create `.github/workflows/pinner.yml`: ```yaml name: Pin to IPFS on: push: branches: [main] jobs: pin: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Pinner CLI run: go install go.lumeweb.com/pinner-cli/cmd/pinner@latest - name: Authenticate run: pinner auth ${{ secrets.PINNER_AUTH_TOKEN }} - name: Upload build output id: upload run: pinner upload ./dist/ --name "ci-build" - name: Verify pin run: pinner status ${{ steps.upload.outputs.cid }} ``` ## How it works 1. On every push to `main`, the workflow installs the Pinner CLI 2. It authenticates with your JWT token using `pinner auth` 3. It uploads the `./dist/` directory with `pinner upload` 4. It verifies the pin was created with `pinner status` The `pinner upload` command outputs the CID, gateway URL, size, and duration. The `pinner status` command checks whether the pin has reached a terminal state (`pinned` or `failed`). ## Using the SDK instead If you prefer the JavaScript SDK in a Node.js script: ```yaml - name: Upload with SDK env: PINNER_AUTH_TOKEN: ${{ secrets.PINNER_AUTH_TOKEN }} run: node scripts/upload-to-ipfs.mjs ``` ```javascript // scripts/upload-to-ipfs.mjs import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); const files = fs.readdirSync("./dist").map((name) => { const buffer = fs.readFileSync(`./dist/${name}`); return new File([buffer], name); }); const operation = await pinner.uploadDirectory(files, { name: "ci-build" }); const result = await operation.result; console.log("Root CID:", result.cid); ``` ## Next steps * [Check pin status programmatically](/ipfs/how-to/check-pin-status) * [Batch pin files](/ipfs/how-to/batch-pin) # Access Pinned Content Pinner pins your content to IPFS. To retrieve it, use one of these options. ## Pinner API The IPFS plugin exposes an authenticated content retrieval endpoint. Send a `GET` request to the IPFS subdomain: ``` GET https://ipfs.pinner.xyz/ipfs/ ``` This returns the raw content for the specified CID and supports streaming for large files. You can also send a `HEAD` request to check whether content exists without downloading it. All requests require a valid JWT token (see [API Keys](/account/api-tokens)). Example with `curl`: ```bash curl -H "Authorization: Bearer " \ "https://ipfs.pinner.xyz/ipfs/bafybeig...example" ``` ## ipfs.pub [ipfs.pub](https://ipfs.pub) is Pinner's own IPFS gateway. It serves content pinned or hosted through Pinner; it's not an anonymous public gateway, so it's fast and reliable for your content. ``` https://ipfs.pub/ipfs/ ``` ## Pinner CLI The Pinner CLI provides three content retrieval commands: * **`pinner cat `**: stream content to stdout (like `ipfs cat`, but through Pinner's API) * **`pinner download `**: save content to a file (supports `CID/path` for specific files, `--force` to overwrite, `--output` to set a path) * **`pinner ls `**: list the contents of a pinned directory All CLI commands authenticate using your stored token (set up via `pinner auth login`). Examples: ```bash # Stream a file to stdout pinner cat bafybeig...example # Download to a specific file pinner download bafybeig...example -o myfile.txt # List directory contents pinner ls bafybeig...example ``` ## Website hosting If you're serving content to the public, the best option is [website hosting](/hosting/quickstart). Your content gets its own domain with automatic SSL, no CID in the URL: ``` https://mysite.example.com ``` See [How Hosting Works](/hosting/concepts/how-hosting-works) for the full setup. ## Public gateways Community-operated public gateways let you retrieve any IPFS content over HTTP. The main surviving public gateway is: | Gateway | URL format | Notes | |---------|------------|-------| | `inbrowser.link` | `https://.ipfs.inbrowser.link` | IPFS Project's service worker gateway. Runs in subdomain mode with origin isolation. Drop-in replacement for the retired `dweb.link`. | [inbrowser.link](https://inbrowser.link) uses a service worker to fetch and verify IPFS content directly in the browser, reducing reliance on a single HTTP backend. It's the IPFS Project's recommended public gateway. Public gateways are not affiliated with Pinner. Availability and performance vary; for production use, we recommend [ipfs.pub](https://ipfs.pub) (via [website hosting](/hosting/quickstart)) or running your own gateway. For a live list of public gateways and their current availability and latency, see [check.ipfs.pub](https://check.ipfs.pub) (our deployment of [check.ipfs.network](https://check.ipfs.network)) or the [IPFS Public Gateway Checker](https://ipfs.github.io/public-gateway-checker/). ### SDK default gateway The Pinner JS SDK uses `https://inbrowser.link` as its default gateway for content retrieval. You can override this by setting the `gateway` option in your `PinnerConfig`: ```typescript const pinner = new Pinner({ jwt: "", gateway: "https://ipfs.pub", // override the default }); ``` The CLI falls back to `https://ipfs.pinner.xyz/ipfs/` when no custom gateway is configured, or you can set `gateway_endpoint` in your config. ## Local IPFS If you run IPFS locally, you already have access: * **IPFS Desktop**: graphical app with built-in gateway at `http://localhost:8080` * **IPFS Companion**: browser extension that resolves IPFS addresses automatically * **ipfs-cli**: `ipfs cat ` from the terminal ## Custom or private gateway If you need guaranteed uptime, low latency, or private access, run your own gateway. Popular options include [Rainbow](https://github.com/ipfs/rainbow) and [Kubo](https://github.com/ipfs/kubo) with the gateway enabled. ## Why doesn't Pinner include a public anonymous gateway? See [Why No Public Gateway](/ipfs/concepts/why-no-gateway) for the reasoning. # Troubleshoot Uploads Fix common problems when uploading or pinning content. If you don't see your issue here, run `pinner doctor` to gather diagnostic information about your CLI version, configuration, and authentication status. For specific error codes, see [Error Reference](/ipfs/reference/errors). ## Auth issues ### "Not authenticated" No auth token is configured. Authenticate with a JWT token: ```bash pinner auth YOUR_JWT_TOKEN ``` Or log in interactively (prompts for email, password, and 2FA if enabled): ```bash pinner auth --email your@email.com ``` Or fully interactive: ```bash pinner auth ``` ### "Authentication expired or invalid" Your session token has expired or is invalid. Re-authenticate: ```bash pinner auth --email your@email.com ``` Or set a token directly: ```bash pinner auth YOUR_JWT_TOKEN ``` ### Email confirmation not received After registration you must confirm your email. If the verification email doesn't arrive: 1. Check your spam folder 2. Verify the email address is correct 3. Run the confirmation manually with the token from the email: ```bash pinner confirm-email --email your@email.com --token VERIFICATION_TOKEN ``` ## Connection errors ### "Connection failed" or "Network timeout" 1. Check your internet connection 2. Verify the API endpoint is reachable: ```bash curl -I https://pinner.xyz ``` 3. Check firewall or proxy settings 4. Try with verbose output to see where it fails: ```bash pinner --verbose upload myfile.txt ``` ## File errors ### "File not found" Verify the file path is correct. Use absolute paths if relative paths fail: ```bash pinner upload /absolute/path/to/file.txt ``` ### "Permission denied" Check file ownership and permissions: ```bash ls -la /path/to/file chmod 644 /path/to/file ``` ### "Cannot upload empty file" Empty files (0 bytes) are rejected. Verify the file has content: ```bash ls -la /path/to/file ``` ### Upload interrupted If an upload is interrupted (Ctrl+C or network drop), retry the same command. The CLI uses the TUS resumable upload protocol for files larger than the upload limit (fetched from your account, default 100 MB), and XHR upload for smaller files: ```bash pinner upload file.dat ``` By default, the command waits for pinning to complete. Use `--no-wait` to return immediately after the upload finishes: ```bash pinner upload file.dat --no-wait ``` ## Pinning errors ### "Pin not found" The CID is not pinned in your account. Verify: ```bash pinner list ``` ### "Pinning failed" 1. Verify the CID exists on IPFS 2. Check your network connection 3. Check the pin status: ```bash pinner status bafybeig... ``` 4. Try again later; transient issues do happen ### Invalid CID format CIDs must start with a valid multibase prefix. Common CID formats include: * `Qm...`: CIDv0 (Base58BTC, SHA-2 256, 32 bytes) * `bafy...`: CIDv1 (DAG-PB, SHA-2 256, mutable-length multihash) * `bafk...`: CIDv1 (raw or single-block, SHA-2 256) CIDs are case-sensitive. ## Config issues ### Config file location | OS | Path | |----|------| | Linux / macOS | `~/.config/pinner/config.yaml` | | Windows | `%APPDATA%\pinner\config.yaml` | ### "Configuration not found" Run setup to create the config file: ```bash pinner setup ``` ### "Configuration invalid" Check your current config: ```bash pinner config ``` Inspect a specific key: ```bash pinner config get base_endpoint ``` Reset if needed: ```bash pinner setup --reset ``` ## Still stuck? 1. Run `pinner doctor`; it reports CLI version, OS, config location and status, endpoint, memory limit, retry count, and authentication status 2. Search [existing issues](https://github.com/lumeweb/pinner-cli/issues) 3. Open a new issue with the error message, steps to reproduce, and `pinner doctor` output # CIDs (Content Identifiers) A CID is a unique address for a piece of content on IPFS. It's derived from the content itself; the same bytes always produce the same CID. Change a single byte and the CID changes completely. CIDs are how IPFS finds content. Instead of asking "get this file from that server," you ask "get the content with this CID." Any node that has the content can serve it. ## CIDv0 The original format. CIDv0 always starts with `Qm` and supports only the DAG-PB codec (one content type). It is implicitly base58btc-encoded. ``` QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco ``` CIDv0 is still common but is considered a legacy format. Pinner accepts CIDv0 as input but immediately normalizes it to CIDv1 internally (see below). ## CIDv1 The modern format. CIDv1 includes a multicodec identifier so it can represent many content types, not just DAG-PB. It also carries an explicit multibase prefix, which means the same CIDv1 can appear in different text encodings. By convention, IPFS tools default to base32 encoding (lowercase), which is why CIDv1 strings typically start with `b`. ``` bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq ``` The first few characters of a base32 CIDv1 hint at its codec: | Prefix | Codec | Example | |--------|-------|---------| | `bafybei…` | DAG-PB (0x70) | Content addressed as a protobuf MerkleDAG node | | `bafkrei…` | Raw (0x55) | Raw leaf data, no DAG wrapping | | `bafyrci…` | DAG-CBOR (0x71) | Content addressed as a CBOR-encoded IPLD node | CIDv1 is more flexible, more self-describing, and works better with modern IPFS tools and gateways. ## How Pinner handles CIDs Pinner always normalizes to CIDv1, even when you upload content that would have a CIDv0. This gives you: * **One consistent format** across all your content * **Better interoperability** with modern gateways and tools * **Support for multiple content codecs** beyond just DAG-PB The content itself doesn't change; only the way the address is written. ### Uploads default to CIDv1 When Pinner builds CAR files from your uploads, it uses CIDv1. The supported codecs are: * **DAG-PB**: the default for files and directories uploaded through Pinner * **Raw**: used for leaf data blocks * **DAG-CBOR**: supported for IPLD nodes encoded as CBOR ### Known limitations * **CIDv0 is not accepted as an IPNS target.** Because CIDv0 strings use the same base58btc multihash encoding as libp2p peer IDs, they can be confused with peer IDs. Use the CIDv1 equivalent instead. * **CAR uploads must have a single root.** CAR files with zero or multiple roots are rejected. Most IPFS tools produce single-root CARs by default. ## Key mental model * A CID is an address, not a URL. It doesn't tell you where the content is, only what it is. * The same content always has the same CID, regardless of who pinned it or where. * CIDv0 and CIDv1 can refer to the same content; they're just different ways to write the address. Pinner always stores and returns the CIDv1 form. # Pin Lifecycle When you pin content on Pinner, it goes through a defined sequence of states. Each state reflects what the system is doing with your content and what you can expect from it. ## States A pin follows one of two paths depending on the upload method: **Upload path** (file or CAR upload: status never enters `queued`): ``` pinning → pinned ↘ failed ``` **Remote pin path** (pin a CID that already exists on the network): ``` queued → pinning → pinned ↘ failed ``` ### queued You've requested a pin for a CID that already exists on the IPFS network. Pinner has recorded the request but hasn't started the pin workflow yet. The system allocates resources and prepares to fetch the content from the network. This is the default status for remote pin requests. ### pinning The pin workflow is running. Depending on the upload method, Pinner is either processing uploaded blocks into the local blockstore (for file and CAR uploads) or downloading the content from the IPFS network and storing it (for remote pins of existing CIDs). This state covers everything from block ingestion, through quota validation and upload-record creation, until all operations complete successfully. ### pinned Your content is stored in the local blockstore and is available for retrieval on the IPFS network. This is the steady state; a pin stays `pinned` until you explicitly delete it or replace it. Like all IPFS content, continued availability depends on at least one node holding the data. ### failed Something went wrong during the pin workflow. Common reasons: * The source CID doesn't exist or isn't reachable on the IPFS network * The uploaded content could not be parsed (invalid CAR, corrupt file, unsupported format) * Quota validation failed (upload or storage quota exceeded) * Block-level storage errors prevented writing The `info` field on a failed pin may contain additional diagnostic detail. You can retry by submitting a new pin request for the same CID. ## Transition triggers | From | To | Trigger | | --- | --- | --- | | N/A | `queued` | Remote pin request (CID already on the network) | | N/A | `pinning` | Upload request (file or CAR): goes directly to pinning, skipping `queued` | | `queued` | `pinning` | Pin workflow starts: the system begins fetching the CID from the network | | `pinning` | `pinned` | All workflow operations succeed: blocks stored, upload records created, core pin records created | | `pinning` | `failed` | Any step in the workflow fails (download, parsing, quota, storage) | ## What happens during pinning The pin workflow performs several steps regardless of upload method: 1. **Block processing**: content blocks are extracted and written to the local blockstore 2. **Quota validation**: upload and storage quotas are checked against the actual DAG size 3. **Upload and pin record creation**: an upload record and a core pin record are created for every block in the DAG 4. **Root pin creation**: an IPFS-specific pin record is created for the root CID, initially in `queued` status 5. **Status update to pinned**: the root pin's status is updated to `pinned` after all preceding steps complete 6. **DAG validation**: the system checks whether the new pin completes a partial DAG structure with related CIDs already pinned by the same user ## What happens after pinning Once a pin reaches `pinned`, it stays there until you explicitly delete it or replace it. Pinner maintains the pin as long as your account is active and within quota. Content remains in the blockstore and available for retrieval. ## Replacing a pin You can replace an existing pin with a new CID using the replace endpoint. This atomically deletes the old pin record and creates a new one in `queued` status, then starts the pin workflow for the new CID. The replacement pin goes through the same lifecycle as a new pin. ## What happens on unpin (delete) When you delete a pin: 1. The IPFS pin record is **soft-deleted** from the database 2. Pinner checks if any other pin records for the same user still reference the same CID 3. If no other pins reference the CID, the core pin is removed, the underlying data is unpinned from the network, and associated file paths are cleaned up 4. If other pins still reference the CID (for example, multiple pins for the same content), only the pin record is removed; the data stays This reference-counting approach means you can safely pin the same CID multiple times and delete individual pin records without affecting the remaining pins. ## No automatic replication Pinner does not perform automatic multi-node replication. Content is written to the local blockstore and made available through the IPFS node. Availability on the broader network depends on the normal IPFS content routing and bitswap mechanisms; Pinner does not copy content to additional storage nodes. # CAR Files A CAR (Content Addressable Archive) is a container format for bundling IPFS blocks together. It's the native way to package and transfer content-addressed data in the IPFS ecosystem. Pinner uses CAR files internally for **all uploads**: whether you upload a single file, a directory, an archive, or a pre-built CAR. Everything is converted to (or passed through as) a CAR file before reaching the server. ## What's inside a CAR file A CAR file contains: * **A header**: a varint-prefixed pragma (version number) followed by a varint-length-prefixed root CID list encoded in DAG-JSON * **Blocks**: raw IPLD data blocks, each prefixed with their length and CID ``` ┌─────────────────────────────────────┐ │ Header │ │ - varint: CAR version (1 or 2) │ │ - varint-prefixed DAG-JSON: │ │ { "roots": [CID, ...] } │ ├─────────────────────────────────────┤ │ Block 1 │ │ - length (varint) │ │ - CID (content identifier) │ │ - block data (raw bytes) │ ├─────────────────────────────────────┤ │ Block 2 ... │ └─────────────────────────────────────┘ ``` ### CAR versions There are two CAR format versions: * **CARv1**: the original format with a simple header and sequential blocks * **CARv2**: adds an index trailer for random-access block lookup, wrapping a CARv1 payload Pinner supports both CARv1 and CARv2 uploads. The server uses `go-car/v2` for reading, which handles both formats transparently. ### Single-root requirement Pinner currently requires CAR files to have **exactly one root CID**. CAR files with multiple roots will be rejected. This is because a single pin record maps to one root CID; if your data has multiple logical roots, upload each as a separate CAR file. ## Why CAR files matter * **Bulk uploads**: package many blocks into one file instead of uploading individually * **Data portability**: move IPLD data between systems in a single transfer * **Streaming**: read and write CAR files incrementally without loading everything into memory * **Directory preservation**: maintain the full IPLD graph structure including directories ## The key advantage: pre-calculable CIDs Because CAR files are made of raw IPFS blocks with their CIDs already computed, you know the root CID **before** you upload. This is impossible with non-IPFS formats like ZIP, where the server must extract, re-chunk, and re-encode the data before it can compute the CID. ## CAR files vs. archives | | CAR Files | Archives (ZIP, tar) | |--|-----------|---------------------| | CID calculation | Client-side: you know it before uploading | Server-side: must wait for processing | | CID known before upload | Yes | No | | Processing time | Instant (blocks are already formed) | Requires server re-chunking and encoding | | Pre-computation | Possible | Not possible | | Deduplication | Block-level: each block is stored by CID | File-level only: entire archive treated as one blob | With a CAR file, the CID is deterministic and locally verifiable. You're guaranteed the content will resolve to the same CID once pinned. With an archive, you must wait for the server to process it and then poll for the result. ## How Pinner handles CAR files Pinner's upload pipeline works differently depending on whether you provide a pre-built CAR or a regular file: ### Pre-built CAR passthrough If you upload a file that is already a valid CAR (detected by MIME type `application/vnd.ipld.car`, the `.car` extension, or the `isCarFile` option), Pinner skips preprocessing and uploads the CAR as-is. In the JS SDK, you can do this explicitly: * `pinner.upload.raw(carFile)`: upload a pre-built CAR with the builder API * `pinner.uploadCar(file, { isCarFile: true })`: direct CAR upload method The server's `CARProcessor` validates the roots, checks quotas against the raw block data size (not the CAR file size which includes headers), and stores the file directly. ### Automatic CAR preprocessing When you upload a regular file, directory, or stream, the JS SDK automatically converts it to CAR format before uploading. This happens client-side using Helia: 1. The input is chunked and encoded into UnixFS DAG blocks 2. A root CID is computed from the block graph 3. The result is streamed to the server as a CAR file On the server, the upload is passed to `CARBlockProcessor`, which reads blocks individually from the CAR stream and stores each block by its CID, enabling **block-level deduplication** across all uploads. ### Block-level deduplication Pinner deduplicates at the **block level**, not the file level. When a CAR file is processed, each block is stored by its CID. If the same block already exists in the blockstore (because it was part of a previous upload), it is not stored again. This means: * Two different files that share common data blocks will only store those blocks once * Re-uploading the same content is effectively free after the first upload * CAR files inherently contain unique, deduplicated blocks; the server does not perform additional dedup checks during block processing because it would add overhead without benefit ## When to use CAR files * You need the CID before the upload completes (deterministic builds, CI pipelines) * You're bulk-transferring pre-packaged IPFS data * You want to verify content integrity locally before trusting a remote service * You're working with existing IPFS tooling that produces CAR output (e.g., `ipfs dag export`) When you don't need these guarantees, a regular upload works just fine; Pinner converts your data into CAR format internally either way. # Why No Public Gateway Pinner is a pinning and hosting service, not a public anonymous IPFS gateway. We don't offer open, unauthenticated read access to arbitrary IPFS content. This is a deliberate choice. ## Fair pricing Public anonymous gateways are expensive infrastructure with unpredictable bandwidth costs. Bundling gateway access into a pinning service forces every user to subsidize a feature they may not need. By treating them as separate concerns, you pay only for storage and pinning, and choose your own retrieval method. ## Sustainability Serving arbitrary open web content through anonymous gateways carries significant liability and abuse potential. Making gateways profitable means pricing high enough to offset that risk, which isn't fair to users who just want their content pinned. Focusing on pinning keeps the service sustainable long-term. ## Focused purpose Pinning and gateways serve fundamentally different needs. Pinning keeps your content available on the decentralized network. Gateways translate that content into HTTP for the traditional web. We excel at pinning; for gateway access, we offer [ipfs.pub](https://ipfs.pub), which serves your content over HTTP when you configure a domain through Pinner's website hosting. ## How ipfs.pub works [ipfs.pub](https://ipfs.pub) is Pinner's DNSLink gateway. It's not an anonymous public gateway that serves arbitrary IPFS content; it specifically resolves `_dnslink` records for domains configured through Pinner's website hosting and serves the corresponding IPFS content over HTTP. When you [host a website](/hosting/quickstart) on your own domain through Pinner, ipfs.pub is what actually serves it: 1. You point your domain's DNS at Pinner 2. Pinner configures a `_dnslink` record mapping your domain to your IPFS content 3. When a browser requests your domain, ipfs.pub resolves the `_dnslink` record and serves the content over HTTP This means website hosting and ipfs.pub aren't separate things; **website hosting is how you configure ipfs.pub to serve your content on your domain**. ## What we also offer * **[Website hosting](/hosting/quickstart)**: configure ipfs.pub to serve your IPFS content on your own domain with automatic SSL. This is the recommended way to make content accessible over HTTP. * **[check.ipfs.pub](https://check.ipfs.pub)**: our deployment of [ipfs-check](https://check.ipfs.network), an IPFS content availability checker. Verify that your DNSLink domains and pinned content are retrievable from the IPFS network. ## What if Pinner goes away? IPFS is content-addressed, not cloud storage. Your data is identified by its CID (a cryptographic hash), not locked behind a proprietary API. That means: * **Your CIDs are portable.** The same CID works everywhere on IPFS. Pin your CIDs on another provider and your content is immediately preserved. No re-upload, no format conversion. * **You can export your pin list at any time.** Pinner lets you download a complete list of your pinned CIDs, so you always know what to re-pin elsewhere. * **Content pinning elsewhere survives.** If you run your own IPFS node, use another pinning service, or anyone else on the network has fetched and kept your content, it remains available. The honest caveat: if Pinner is the **only** node holding your content and it becomes unavailable, that content becomes unreachable until it's pinned again or another node has a copy. This is true of any single pinning provider; it's the nature of IPFS. We recommend pinning important content with at least two providers, or running your own node as a backup. That said, the architecture works in your favor: because IPFS is decentralized, there's no vendor lock-in at the data layer. Your content isn't stored in a proprietary format behind a proprietary API; it's on an open network with open addressing. Migration is a pin list, not a data migration. ## Accessing your content Since Pinner doesn't include a public anonymous gateway, you retrieve your content through ipfs.pub (configured via website hosting), public gateways, local IPFS, or your own private gateway. See [Access Pinned Content](/ipfs/how-to/access-pinned-content) for practical options. # SDK Architecture The Pinner SDK is built around one core class: `Pinner`. Everything flows through it. ## The Pinner class `Pinner` is the entry point. You create an instance with your configuration and then access all functionality through property-access namespaces: * **`pinner.upload`**: upload files, directories, JSON, CAR data, and more * **`pinner.pins`**: check status, update metadata, list and manage pins * **`pinner.ipns`**: create and manage IPNS keys and publish records * **`pinner.websites`**: publish content as a website with SSL support Each namespace groups related operations together. You pass your JWT once in the configuration; the `Pinner` instance handles authentication for everything. ## Upload: callable namespace and builder The `upload` property is unique: it works as both a direct method **and** a builder namespace. This is implemented via a JavaScript `Proxy` that delegates to whichever side you need: * **As a method:** `pinner.upload(file, options)`: upload a file immediately * **As a namespace:** `pinner.upload.file(file)`, `pinner.upload.json(data)`, and so on: returns a builder for chaining options ### Builder pattern When you use the builder namespace, each content method returns a typed builder you chain options onto before calling `.pin()` to commit the upload: 1. Choose what to upload: `.file()`, `.json()`, `.base64()`, `.url()`, `.csv()`, `.text()`, `.raw()` 2. Optionally chain `.name()`, `.keyvalues()`, `.waitForOperation()`, or `.operationPollingOptions()` 3. Call `.pin()` to execute This keeps the API composable; you only specify what you need, and each step returns a type-safe builder for the next. ### Convenience methods Beyond the builder, `Pinner` also exposes shorthand methods for common workflows: * **`uploadAndWait()`**: upload a file and wait for the result in one call * **`waitForOperation()`**: poll an operation until it reaches a settled state * **`uploadDirectory()`**: upload an array of files as a directory (preprocessed to CAR internally) * **`uploadCar()`**: upload a raw CAR file without preprocessing ### Upload pipeline: XHR vs TUS The `UploadManager` selects a transport strategy based on file size: * **Small files** (below the account's upload limit, defaulting to 100 MB) use XHR upload * **Large files** use TUS resumable upload Both transports are built on Uppy internally. CAR files are automatically detected by MIME type or extension, or can be forced via the `isCarFile` option. ## Pinata adapters Pinner ships with two adapter functions that wrap a `Pinner` instance in a Pinata SDK-compatible interface: * **`pinataAdapter`**: matches Pinata SDK 2.x API shape (namespaced with `upload.public`, `files.public`, etc.) * **`pinataLegacyAdapter`**: matches Pinata SDK 1.x API shape (flat methods like `pinFileToIPFS`, `pinJSONToIPFS`) The adapters translate Pinata-style calls into Pinner operations behind the scenes. Features that Pinner does not support (groups, analytics, private uploads, signed URLs) return empty data or throw a "not supported" error. Your existing Pinata code can migrate with only the initialization changed. ## Error hierarchy Errors in the SDK follow a single-root hierarchy so you can catch at the granularity you need: * **`PinnerError`**: base class for all SDK errors; carries a `code`, a `retryable` flag, and an optional `cause` * **`ConfigurationError`**: missing or invalid configuration (not retryable) * **`AuthenticationError`**: JWT or access failure (not retryable) * **`EnvironmentError`**: unsupported runtime environment (not retryable) * **`UploadError`**: any upload failure * **`NetworkError`**: request-level failure (retryable) * **`ValidationError`**: invalid input; includes an optional `field` * **`EmptyFileError`**: zero-byte file upload attempt * **`TimeoutError`**: request exceeded its deadline (retryable) * **`PinError`**: any pin-management failure * **`NotFoundError`**: pin or resource does not exist (not retryable) * **`RateLimitError`**: too many requests; includes `retryAfter` seconds (retryable) The `retryable` flag on `PinnerError` lets you decide programmatically whether to retry. The SDK also exports type-guard functions: `isRetryable(error)` and `isAuthenticationError(error)` for common checks. ## Key design principles * **One entry point.** Create a `Pinner` instance once, use it everywhere. * **Dual-mode namespace.** `upload` is both a direct method and a builder namespace, via a `Proxy`. * **Builder pattern for uploads.** Start with a content method, chain options, commit with `.pin()`. * **Automatic transport selection.** XHR for small uploads, TUS for large; transparent to the caller. * **Adapter compatibility.** Pinata SDK 2.x and 1.x drop-in replacements for easy migration. # Getting Started Set up the Pinner SDK and pin your first file in about five minutes. ## Prerequisites * An API key: [create one in your account](/account/api-tokens) * Node.js 20+ or a modern browser ## Install the package :::code-group ```bash [npm] npm install @lumeweb/pinner ``` ```bash [pnpm] pnpm add @lumeweb/pinner ``` ::: ## Initialize Pinner The SDK needs an API key to authenticate. In a browser, pass it directly. On the server, read it from the environment. :::code-group ```typescript [Browser] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: "YOUR_JWT" }); ``` ```typescript [Node.js] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); ``` ::: ## Upload your first file `uploadAndWait` pins the file and waits for the pin to settle. It returns an `UploadResult` with the CID (may be `undefined` for TUS uploads until the server assigns it), name, size, and more. :::code-group ```typescript [Browser] const file = new File(["Hello, Pinner!"], "hello.txt", { type: "text/plain" }); const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ```typescript [Node.js] import { readFileSync } from "fs"; // Node.js 20+ provides the global File class const buffer = readFileSync("./hello.txt"); const file = new File([buffer], "hello.txt", { type: "text/plain" }); const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ::: `uploadAndWait` handles the full cycle: it uploads the file, waits for pinning to complete, and returns an `UploadResult` with the CID (may be `undefined` for TUS uploads until the server assigns it), name, size, and more. ## Verify the connection Upload a small file and confirm you get a CID back. This proves both authentication and connectivity work end-to-end. ```typescript const file = new File(["ping"], "ping.txt", { type: "text/plain" }); const result = await pinner.uploadAndWait(file); if (result.cid) { console.log("Connected! CID:", result.cid); } else { console.log("Upload sent, operation ID:", result.id); } ``` ## Type your config If you prefer explicit types, import `PinnerConfig`: :::code-group ```typescript [Browser] import { Pinner, PinnerConfig } from "@lumeweb/pinner"; const config: PinnerConfig = { jwt: "YOUR_JWT", endpoint: "https://ipfs.pinner.xyz" }; const pinner = new Pinner(config); ``` ```typescript [Node.js] import { Pinner, PinnerConfig } from "@lumeweb/pinner"; const config: PinnerConfig = { jwt: process.env.PINNER_AUTH_TOKEN, endpoint: "https://ipfs.pinner.xyz" }; const pinner = new Pinner(config); ``` ::: *** Ready to customize? See [Configuration](/ipfs/sdk/configuration) for endpoints, gateways, storage backends, and environment-specific setup. # Configuration The `PinnerConfig` interface controls how the SDK connects, where it stores local data, and which gateway it uses to retrieve content. ## PinnerConfig interface ```typescript interface PinnerConfig { /** JWT authentication token. Required. */ jwt: string; /** API endpoint. Defaults to "https://ipfs.pinner.xyz". */ endpoint?: string; /** IPFS gateway for content retrieval. Defaults to "https://inbrowser.link". */ gateway?: string; /** Allowed MIME types. If omitted, all types are accepted. */ allowedFileTypes?: string[]; /** Custom fetch implementation. */ fetch?: typeof fetch; /** Custom datastore for Helia. Highest storage priority. */ datastore?: Datastore; /** Custom storage instance for Helia blockstore and datastore. */ storage?: Storage; /** Base name for default Helia storage. Only used when neither datastore nor storage are provided. Defaults to "pinner-helia-data". */ datastoreName?: string; /** Upload request timeout in milliseconds. Applied to XHR uploads (TUS uses retryDelays instead). Defaults to 120_000. */ timeout?: number; /** Number of retry attempts for failed uploads. Applied to XHR uploads (TUS uses retryDelays instead). Defaults to 3. */ retries?: number; } ``` The interface is the same in browser and Node; only the values you pass differ. ## Endpoint By default the SDK talks to `https://ipfs.pinner.xyz`. Point it at a different host for staging or self-hosted instances: :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT", endpoint: "https://staging.ipfs.pinner.xyz" }); ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, endpoint: "https://staging.ipfs.pinner.xyz" }); ``` ::: ## Gateway The gateway URL is used when the SDK resolves CIDs to retrievable HTTP URLs. The default is `https://inbrowser.link`. For Pinner-pinned content, `https://ipfs.pub` offers lower latency since it serves from the same infrastructure that stores your pins: :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT", gateway: "https://ipfs.pub" }); ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, gateway: "https://inbrowser.link" }); ``` ::: ## Custom fetch Pass your own `fetch` when you need request interceptors, custom headers, or a non-global HTTP client. :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT", fetch: window.fetch }); ``` ```typescript [Node.js] import { fetch } from "undici"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, fetch }); ``` ::: ## Storage configuration The SDK uses Helia under the hood. Storage is resolved in priority order: | Priority | Option | What it does | |----------|--------|--------------| | 1 (highest) | `datastore` | Custom `Datastore` instance, used directly by Helia. | | 2 | `storage` | Custom `unstorage` instance applied to both blockstore and datastore. | | 3 | `datastoreName` | Custom base name for the default storage driver. Only used when neither `datastore` nor `storage` are provided. | | 4 (default) | *(none)* | Uses `"pinner-helia-data"` as the base name. | ### Browser: storage levels ```typescript // Level 1: default (IndexedDB with base "pinner-helia-data") const pinner = new Pinner({ jwt: "YOUR_JWT" }); // Level 2: custom base name const pinner = new Pinner({ jwt: "YOUR_JWT", datastoreName: "my-app-storage" }); // Level 3: custom storage instance import { createStorage } from "unstorage"; import indexeddbDriver from "unstorage/drivers/indexedb"; const storage = createStorage({ driver: indexeddbDriver({ base: "my-app" }) }); const pinner = new Pinner({ jwt: "YOUR_JWT", storage }); // Level 4: custom datastore (highest priority) import { MemoryDatastore } from "interface-datastore"; const datastore = new MemoryDatastore(); const pinner = new Pinner({ jwt: "YOUR_JWT", datastore }); ``` ### Node.js: storage levels ```typescript // Level 1: default (filesystem in ./.pinner-*) const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); // Level 2: custom base name const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, datastoreName: "my-app-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_AUTH_TOKEN, storage }); // Level 4: custom datastore (highest priority) import { MemoryDatastore } from "interface-datastore"; const datastore = new MemoryDatastore(); const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, datastore }); ``` ## Timeout By default, XHR uploads time out after 120 000 ms (2 minutes). Pass a custom `timeout` value in milliseconds to increase or decrease the limit. Note that TUS uploads do not use this option; they rely on their own `retryDelays` mechanism instead. :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT", timeout: 300_000 // 5 minutes }); ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, timeout: 300_000 // 5 minutes }); ``` ::: ## Retries The number of retry attempts for failed XHR uploads. Defaults to `3`. As with `timeout`, this only applies to XHR uploads; TUS uploads use their own retry mechanism. :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT", retries: 5 }); ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN, retries: 5 }); ``` ::: ## Environment-specific setup Keep configuration out of your codebase. Use environment variables and separate config modules for dev versus prod. ```typescript title="config/development.ts" export const config = { jwt: process.env.DEV_PINNER_AUTH_TOKEN, endpoint: "https://staging.ipfs.pinner.xyz" }; ``` ```typescript title="config/production.ts" export const config = { jwt: process.env.PROD_PINNER_AUTH_TOKEN, endpoint: "https://ipfs.pinner.xyz" }; ``` Feed the config object straight into `new Pinner(config)` and the SDK picks the right endpoint without branching logic in your app code. # SDK Error Handling The SDK throws structured error classes that extend a common `PinnerError` base. Every error carries a machine-readable `code`, a human-readable `message`, a `retryable` flag, and an optional `cause`. ## Error class hierarchy ``` PinnerError ├── ConfigurationError ├── AuthenticationError ├── UploadError │ ├── NetworkError │ ├── ValidationError │ │ └── EmptyFileError │ └── TimeoutError ├── PinError │ ├── NotFoundError │ └── RateLimitError └── EnvironmentError ``` ## Error classes ### `PinnerError` The base class for all SDK errors. Every subclass inherits these properties: | Property | Type | Description | |----------|------|-------------| | `code` | `string` | Machine-readable error code | | `message` | `string` | Human-readable error description | | `retryable` | `boolean` | Whether the operation can be retried | | `cause` | `Error \| undefined` | The underlying error that caused this one | ### `ConfigurationError` Thrown when the SDK is misconfigured (e.g. missing JWT token). * **Code:** `CONFIGURATION_ERROR` * **Retryable:** `false` ### `AuthenticationError` Thrown for HTTP 401 and 403 responses when a request fails authentication or authorization. * **Code:** `AUTHENTICATION_ERROR` * **Retryable:** `false` ### `UploadError` Base class for upload-related errors. * **Code:** `UPLOAD_ERROR` * **Retryable:** set per subclass ### `NetworkError` Thrown when a network request fails (HTTP errors that don't map to a more specific class, or non-HTTP network failures). * **Code:** `NETWORK_ERROR` * **Retryable:** `true` ### `ValidationError` Thrown for HTTP 400 responses (invalid request data). Also used for HTTP 410 in the websites API (target is broken or gone). * **Code:** `VALIDATION_ERROR` * **Retryable:** `false` * **Extra property:** `field?: string`, the field that failed validation ### `EmptyFileError` Thrown when attempting to upload an empty file or stream. Extends `ValidationError` with `field` set to `"file"`. * **Code:** `EMPTY_FILE_ERROR` * **Retryable:** `false` ### `TimeoutError` Thrown when a request times out. * **Code:** `TIMEOUT_ERROR` * **Retryable:** `true` ### `PinError` Base class for pin operation errors. * **Code:** `PIN_ERROR` * **Retryable:** set per subclass ### `NotFoundError` Thrown for HTTP 404 responses or when a pin is not found for a given CID. * **Code:** `NOT_FOUND` * **Retryable:** `false` ### `RateLimitError` Thrown for HTTP 429 rate-limit responses. * **Code:** `RATE_LIMIT_EXCEEDED` * **Retryable:** `true` * **Extra property:** `retryAfter?: number`, seconds to wait before retrying ### `EnvironmentError` Thrown when the runtime environment is not supported. * **Code:** `ENVIRONMENT_ERROR` * **Retryable:** `false` ## HTTP-to-error mapping The SDK maps HTTP status codes to error classes as follows: | HTTP status | Error class | Notes | |------------|-------------|-------| | 400 | `ValidationError` | Invalid request data | | 401 | `AuthenticationError` | Bad or expired JWT | | 403 | `AuthenticationError` | No permission for this resource | | 404 | `NotFoundError` | Resource doesn't exist | | 410 | `ValidationError` | Target is broken or gone (websites API) | | 429 | `RateLimitError` | Too many requests; wait `retryAfter` seconds then retry | | Other HTTP errors | `NetworkError` | Generic HTTP failure | | Network failures | `NetworkError` | No response received | ## Retryability Each error class sets the `retryable` flag: | Error class | `retryable` | |------------|-------------| | `NetworkError` | `true` | | `TimeoutError` | `true` | | `RateLimitError` | `true` | | All others | `false` | ## Type guards The SDK exports two type guards from `@lumeweb/pinner`: ```typescript import { isRetryable, isAuthenticationError } from "@lumeweb/pinner"; // Check if an error can be retried if (isRetryable(error)) { // wait and retry } // Check if an error is authentication-related if (isAuthenticationError(error)) { // prompt user to re-authenticate } ``` You can also use `instanceof` checks directly: ```typescript import { NotFoundError, RateLimitError } from "@lumeweb/pinner"; if (error instanceof NotFoundError) { // handle 404 } if (error instanceof RateLimitError) { console.log(`Retry after ${error.retryAfter} seconds`); } ``` ## Example: Handling errors ```typescript import { Pinner, AuthenticationError, EmptyFileError, RateLimitError, isRetryable, } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); try { const result = await pinner.upload(file); } catch (error) { if (error instanceof AuthenticationError) { console.error( "Bad or expired API key. Re-authenticate and try again." ); } else if (error instanceof EmptyFileError) { console.error("Cannot upload an empty file."); } else if (error instanceof RateLimitError) { console.error( `Rate limit exceeded. Retry after ${error.retryAfter ?? 60} seconds.` ); } else if (error instanceof Error && isRetryable(error as any)) { console.error("Transient error; retry:", error.message); } else { console.error("Upload failed:", error); } } ``` ## See also * [Error Reference](/ipfs/reference/errors): complete list of API error codes * [CLI Troubleshooting](/ipfs/how-to/troubleshoot-uploads): common upload errors and fixes # Upload Progress & Events `pinner.upload()` returns a `Promise` that gives you real-time progress, pause/resume control, and a promise that resolves when the upload finishes. ## UploadOperation :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT" }); const operation = await pinner.upload(file); // Wait for the upload to finish const result = await operation.result; console.log("CID:", result.cid); console.log("Operation ID:", result.operationId); // Control the upload operation.pause(); // toggles pause state (TUS uploads only) operation.resume(); // toggles pause state (TUS uploads only) operation.cancel(); // aborts the upload ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); const operation = await pinner.upload(file); // Wait for the upload to finish const result = await operation.result; console.log("CID:", result.cid); console.log("Operation ID:", result.operationId); // Control the upload operation.pause(); // toggles pause state (TUS uploads only) operation.resume(); // toggles pause state (TUS uploads only) operation.cancel(); // aborts the upload ``` ::: ### UploadOperation API | Member | Type | Description | |--------|------|-------------| | `result` | `Promise` | Resolves with the CID, operation ID, and upload metadata | | `progress` | `Readonly` | Snapshot of the current progress | | `cancel()` | `() => void` | Abort the upload | | `pause()` | `() => void` | Toggle pause on a TUS upload | | `resume()` | `() => void` | Toggle resume on a paused TUS upload | :::warning `pause()` and `resume()` both toggle the Uppy pause state; they are not independently scoped. Only TUS uploads support pause/resume; XHR uploads ignore these calls. ::: ## Progress events Pass an `onProgress` callback in `UploadOptions` to get notified as bytes transfer: :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT" }); const operation = await pinner.upload(file, { onProgress: (progress) => { console.log(`Progress: ${progress.percentage.toFixed(2)}%`); console.log(`Uploaded: ${progress.bytesUploaded} / ${progress.bytesTotal} bytes`); if (progress.speed != null) { console.log(`Speed: ${progress.speed} bytes/sec`); } if (progress.eta != null) { console.log(`ETA: ${progress.eta} seconds`); } } }); const result = await operation.result; console.log("Complete:", result.cid); ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); const operation = await pinner.upload(file, { onProgress: (progress) => { console.log(`Progress: ${progress.percentage.toFixed(2)}%`); console.log(`Uploaded: ${progress.bytesUploaded} / ${progress.bytesTotal} bytes`); if (progress.speed != null) { console.log(`Speed: ${progress.speed} bytes/sec`); } if (progress.eta != null) { console.log(`ETA: ${progress.eta} seconds`); } } }); const result = await operation.result; console.log("Complete:", result.cid); ``` ::: ### UploadProgress properties | Property | Type | Description | |----------|------|-------------| | `percentage` | `number` | Upload progress from 0 to 100 | | `bytesUploaded` | `number` | Bytes transferred so far | | `bytesTotal` | `number` | Total bytes to transfer | | `speed` | `number \| undefined` | Current speed in bytes per second (optional) | | `eta` | `number \| undefined` | Estimated seconds remaining (optional) | ## Build a progress bar Wire `onProgress` into any UI framework. A plain-DOM example: ```typescript const bar = document.getElementById("progress-bar"); const operation = await pinner.upload(file, { onProgress: (progress) => { bar.style.width = `${progress.percentage}%`; bar.textContent = `${progress.percentage.toFixed(0)}%`; } }); const result = await operation.result; bar.textContent = `Done: ${result.cid}`; ``` In React, store `progress.percentage` in state and render it the same way. ## Cancel an upload Call `cancel()` on the operation to abort an in-flight upload: ```typescript const operation = await pinner.upload(file); // Some time later: user clicked "Cancel" or a timeout fired operation.cancel(); try { await operation.result; // rejects after cancel } catch (error) { console.log("Upload was cancelled"); } ``` After `cancel()`, the `result` promise rejects. Catch it to clean up UI state. ## waitForOperation Uploading is only the first step; the pin must also settle on the network. `waitForOperation` polls until the pin reaches a settled state. :::code-group ```typescript [Browser] const pinner = new Pinner({ jwt: "YOUR_JWT" }); const operation = await pinner.upload(file); const uploadResult = await operation.result; // Wait for pinning to complete const result = await pinner.waitForOperation(uploadResult, { interval: 1000, // poll every 1s timeout: 60000 // give up after 60s }); console.log("CID:", result.cid); console.log("Operation ID:", result.operationId); ``` ```typescript [Node.js] const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); const operation = await pinner.upload(file); const uploadResult = await operation.result; // Wait for pinning to complete const result = await pinner.waitForOperation(uploadResult, { interval: 1000, // poll every 1s timeout: 60000 // give up after 60s }); console.log("CID:", result.cid); console.log("Operation ID:", result.operationId); ``` ::: ### Input types `waitForOperation` accepts either an `UploadResult` or a numeric operation ID: ```typescript // Recommended: pass the full UploadResult // Uses uploadResult.operationId if present, otherwise looks up by CID const result = await pinner.waitForOperation(uploadResult); // Also works: pass just the operation ID (number) const result = await pinner.waitForOperation(uploadResult.operationId!); ``` :::info When you pass an `UploadResult`, `waitForOperation` uses the `operationId` field directly if present. If `operationId` is missing but `cid` is available, it looks up the operation by CID first. This means even uploads that don't return an operation ID can still be awaited. ::: ### Polling options | Option | Type | Default | Description | |--------|------|---------|-------------| | `interval` | `number` | `2000` | Polling interval in milliseconds | | `timeout` | `number` | `300000` | Maximum wait time in milliseconds (5 minutes) | | `settledStates` | `string[]` | `["completed", "failed", "error"]` | States that count as settled | *** Next: [Error Handling](/ipfs/sdk/errors): classify and recover from SDK errors. # Getting Started with the CLI Install the Pinner CLI, walk through the setup wizard, and confirm everything works. If you just want to pin a file right now, see [Quickstart](/ipfs/quickstart). ## Install :::code-group ```bash [macOS / Linux] curl -fsSL https://get.pinner.xyz | sh ``` ```powershell [Windows] irm https://get.pinner.xyz/install.ps1 | iex ``` ::: The installer detects your package manager and uses it automatically. On macOS it prefers Homebrew; on Linux it tries `dpkg` then `rpm` before falling back to a binary download. On Windows it tries winget, then Scoop, then falls back to a binary download. All methods verify SHA256 checksums. ### Other install methods The universal installer handles most cases. Use these when you need more control. | Method | Command | |--------|---------| | Homebrew | `brew install lumeweb/tap/pinner` | | winget | `winget install Pinner.Cli` | | Scoop | `scoop bucket add lumeweb https://github.com/LumeWeb/scoop-bucket` then `scoop install pinner` | | Debian/Ubuntu | `sudo dpkg -i pinner-cli_VERSION_linux_amd64.deb` | | Fedora/RHEL | `sudo rpm -i pinner-cli_VERSION_linux_amd64.rpm` | | Go | `go install go.lumeweb.com/pinner-cli/cmd/pinner@latest` | | Binary | Download from [GitHub Releases](https://github.com/LumeWeb/pinner-cli/releases) |
Installer flags **macOS / Linux** (`install.sh`): | Flag | Description | |------|-------------| | `--system` | Install to `/usr/local/bin` (requires sudo if not writable) | | `--bin-dir DIR` | Install to a custom directory | | `--arch ARCH` | Override detected architecture (`amd64` or `arm64`) | | `--no-pkg` | Skip package manager detection, use binary install | | `--uninstall` | Remove the Pinner CLI | | `--debug` | Enable verbose output | **Windows** (`install.ps1`): | Flag | Description | |------|-------------| | `-System` | Install to Program Files (requires admin) | | `-NoPkg` | Skip winget/scoop detection | | `-CI` | Enable CI mode (also activated by `CI=true` env var) | | `-Uninstall` | Remove the Pinner CLI |
## Set up After installing, run the setup wizard: ```bash pinner setup ``` The wizard walks you through: * **Authentication**: log in or register * **Configuration**: set API endpoint, memory limits, and other options If you've already authenticated or configured, the wizard detects that and skips the step. ### Skip steps ```bash # Skip authentication (already logged in) pinner setup --skip-auth # Skip configuration (already configured) pinner setup --skip-config # Reset everything and start fresh pinner setup --reset ``` ### Non-interactive environments For CI, Docker, or scripts, skip the wizard and use individual commands: ```bash # Authenticate with a JWT token pinner auth YOUR_TOKEN # Or log in interactively pinner auth --email you@example.com # Configure pinner config set memory_limit 256 pinner config set secure true ``` ## Verify ```bash # Print version pinner --version # Display diagnostic information for troubleshooting pinner doctor ``` If `pinner doctor` reports problems, see [CLI Doctor](/ipfs/cli/doctor) for troubleshooting. ## Next steps * [Upload your first file](/ipfs/quickstart) * [Configure CLI settings](/ipfs/cli/config) * [Manage API keys](/ipfs/cli/auth) # CLI Auth The CLI handles API key creation automatically. You don't need to visit the dashboard to generate keys; `pinner auth` creates them for you. ## Log in ### Interactive (recommended for local development) ```bash pinner auth ``` Prompts for email and password. The CLI authenticates, generates an API key, and stores it locally. ### With email, prompt for password ```bash pinner auth --email you@example.com # or with the short flag pinner auth -e you@example.com ``` ### Provide an existing token (CI/CD) If you already have a JWT auth token (for example, from another machine), pass it directly: ```bash pinner auth YOUR_JWT_TOKEN ``` The CLI stores it locally for future use. This is the non-interactive path; use it for CI/CD pipelines and scripts. ### Non-interactive (CI / scripts) ```bash # Email and password from flags pinner auth --email you@example.com --password yourpassword # Email and password from environment variables PINNER_EMAIL=you@example.com PINNER_PASSWORD=yourpassword pinner auth # With 2FA pinner auth --email you@example.com --password yourpassword --otp-code 123456 # With 2FA from environment variables PINNER_EMAIL=you@example.com PINNER_PASSWORD=yourpassword PINNER_OTP=123456 pinner auth # Password via stdin (more secure than --password flag) echo "yourpassword" | pinner auth --email you@example.com ``` ### Named keys Give your key a name to track which machine or environment it belongs to: ```bash pinner auth --email you@example.com --key-name "ci-pipeline" # or with the short flag pinner auth -e you@example.com -k "ci-pipeline" ``` If not specified, keys are named `cli-generated` by default. ### Skip API key creation To save the login token directly without creating an API key: ```bash pinner auth --no-create-key ``` ### Overwrite existing credentials To overwrite an existing auth token without a confirmation prompt: ```bash pinner auth --force ``` ## Register Create a new Pinner account from the CLI: ```bash # Interactive: prompts for all fields pinner register # Non-interactive pinner register --email you@example.com --first-name Jane --last-name Doe # Provide password non-interactively pinner register --email you@example.com --first-name Jane --last-name Doe --password yourpassword ``` The `register` command also supports short flags: `-e` (email), `-f` (first-name), `-l` (last-name), `-p` (password). After registering, confirm your email: ```bash pinner confirm-email --email you@example.com --token abc123def456 # or with short flags pinner confirm-email -e you@example.com -t abc123def456 ``` ## Check authentication status ```bash pinner auth status ``` Verifies that your stored auth token is valid by making a request to the Pinner.xyz API. ## Key management The CLI creates API keys automatically when you run `pinner auth`. You can also manage keys directly from the command line. ### List keys ```bash pinner account api-keys list # Filter by name pinner account api-keys list --search my-key ``` ### Create a key ```bash pinner account api-keys create my-key-name ``` The key token is displayed once; save it securely, as it cannot be retrieved later. ### Delete a key ```bash pinner account api-keys delete my-key-name # or by UUID pinner account api-keys delete 00000000-0000-0000-0000-000000000001 # Force deletion of the currently active key pinner account api-keys delete my-key-name --force ``` If you delete the key currently used for authentication, you must re-authenticate with `pinner auth`. ### Rotate a key ```bash # 1. Create a new named key pinner auth --email you@example.com --key-name "rotation-$(date +%Y%m%d)" # 2. Update your environment variables and deployments with the new key # 3. Delete the old key pinner account api-keys delete old-key-name ``` ## 2FA / OTP Secure your account with one-time passwords: ```bash # Enable 2FA (interactive) pinner account otp enable # Enable 2FA non-interactively pinner account otp enable --otp 123456 # Disable 2FA pinner account otp disable # Disable 2FA non-interactively pinner account otp disable --password mypassword ``` When 2FA is active, include your OTP code when authenticating: ```bash PINNER_EMAIL=you@example.com PINNER_PASSWORD=*** PINNER_OTP=123456 pinner auth ``` ## Flags reference ### `pinner auth` | Flag | Short | Description | Env var | |------|-------|-------------|---------| | `--email` | `-e` | Email address for login | `PINNER_EMAIL` | | `--password` | `-p` | Password for login (insecure; prefer stdin or env var) | `PINNER_PASSWORD` (also reads from stdin) | | `--otp-code` | `-o` | OTP code for 2FA (6 digits) | `PINNER_OTP` | | `--key-name` | `-k` | Custom name for created API key (default: `cli-generated`) | N/A | | `--no-create-key` | N/A | Skip API key creation, save token directly | N/A | | `--force` | N/A | Overwrite existing auth token without confirmation | N/A | ## Environment variables | Environment variable | Purpose | Used by | |---------------------|---------|---------| | `PINNER_AUTH_TOKEN` | Your auth token (JWT), used by all commands | CLI, SDK | | `PINNER_EMAIL` | Email address for `pinner auth` | CLI | | `PINNER_PASSWORD` | Password for `pinner auth` | CLI | | `PINNER_OTP` | OTP code for 2FA during `pinner auth` | CLI | The SDK and CLI check `PINNER_AUTH_TOKEN` when you don't pass a token directly. The email, password, and OTP env vars are only used by `pinner auth`. # CLI Config View and change CLI configuration from the command line. ## Usage ```bash # Show all configuration values pinner config # Get a specific value pinner config get # Set a value pinner config set ``` ## Show all configuration ```bash pinner config ``` Displays all configuration values in a table with Key, Value, Env, and Description columns. ## Get a value ```bash pinner config get base_endpoint pinner config get secure ``` ## Set a value ```bash # Change the base endpoint pinner config set base_endpoint "pinner.xyz" # Disable HTTPS pinner config set secure false # Increase retry attempts pinner config set max_retries 5 # Set memory limit for CAR generation (MB) pinner config set memory_limit 256 # Set IPFS gateway override pinner config set gateway_endpoint "https://inbrowser.link" # Set default API timeout pinner config set default_timeout 60s # Preview a change without saving pinner config set memory_limit 256 --dry-run ``` ## Configuration keys | Key | Type | Default | Description | |-----|------|---------|-------------| | `base_endpoint` | string | `""` | Base URL for Pinner service (e.g., `pinner.xyz`). Subdomains are derived automatically (e.g., `account.pinner.xyz`, `ipfs.pinner.xyz`). Leave empty for default. | | `secure` | bool | `true` | Use HTTPS for endpoints | | `max_retries` | int | `3` | Maximum retry attempts for failed operations (0–10) | | `memory_limit` | int | `100` | Memory limit in MB for CAR generation (1–10240) | | `gateway_endpoint` | string | `""` | IPFS gateway URL (e.g., `https://inbrowser.link`). Falls back to the `ipfs` subdomain of the base endpoint. | | `default_timeout` | duration | `30s` | Default timeout for API operations (e.g., `30s`, `1m`) | | `upload_timeout` | duration | `5m` | Timeout for upload, download, and benchmark operations (e.g., `5m`, `10m`) | | `sync_timeout` | duration | `1m` | Timeout for reconciliation, cleanup, and sync operations (e.g., `1m`, `2m`) | | `auth_token` | string | `""` | JWT authentication token for API access. Managed by `pinner auth`. | ## Environment variables Every configuration key can also be set via an environment variable with the `PINNER_` prefix, uppercased and with hyphens replaced by underscores: ```bash # Examples export PINNER_BASE_ENDPOINT="pinner.xyz" export PINNER_SECURE=false export PINNER_MAX_RETRIES=5 export PINNER_MEMORY_LIMIT=256 export PINNER_GATEWAY_ENDPOINT="https://inbrowser.link" export PINNER_AUTH_TOKEN="your-token" ``` | Config key | Environment variable | |------------|----------------------| | `base_endpoint` | `PINNER_BASE_ENDPOINT` | | `secure` | `PINNER_SECURE` | | `max_retries` | `PINNER_MAX_RETRIES` | | `memory_limit` | `PINNER_MEMORY_LIMIT` | | `gateway_endpoint` | `PINNER_GATEWAY_ENDPOINT` | | `default_timeout` | `PINNER_DEFAULT_TIMEOUT` | | `upload_timeout` | `PINNER_UPLOAD_TIMEOUT` | | `sync_timeout` | `PINNER_SYNC_TIMEOUT` | | `auth_token` | `PINNER_AUTH_TOKEN` | ## Configuration file The CLI stores configuration in a YAML file on disk. The auth token is managed separately by `pinner auth`. | Platform | Path | |----------|------| | Linux / macOS | `~/.config/pinner/config.yaml` | ## Options | Option | Description | |--------|-------------| | `--dry-run` | Preview changes without saving | ## Subcommands | Subcommand | Description | |------------|-------------| | *(none)* | Show all configuration values | | `get ` | Get a specific configuration value | | `set ` | Set a configuration value | # CLI Setup Use the interactive setup wizard to authenticate and configure the CLI in one go. ```bash pinner setup ``` The wizard walks you through four steps: * **Authentication**: create an account, sign in, or skip * **Configuration**: set API endpoint and HTTPS preferences, or use defaults * **Shell Completion**: enable auto-completion for your shell * **Quick Tutorial**: browse common commands and examples If you've already authenticated, the wizard detects your existing auth token and skips the authentication step. Similarly, the configuration step is skipped if a custom endpoint is already set (unless `--reset` is used). ## Skip steps ```bash # Skip authentication (already logged in) pinner setup --skip-auth # Skip configuration (already configured) pinner setup --skip-config # Skip both: use when you only want to run the completion and tutorial steps pinner setup --skip-auth --skip-config ``` ## Reset configuration Start fresh: clears your config and runs the wizard from scratch: ```bash pinner setup --reset ``` ## Non-interactive environments The setup wizard requires an interactive terminal. For CI, scripts, or other non-interactive contexts, use individual commands instead: ```bash # Authenticate with a JWT token pinner auth YOUR_JWT_TOKEN # Authenticate interactively (supports --email, --password, --otp-code flags) pinner auth --email user@example.com # Configure settings pinner config set base_endpoint api.example.com pinner config set secure true pinner config set memory_limit 256 ``` ## Authentication step When the wizard reaches authentication, you choose one of: 1. **Create a new account**: directs you to the registration page, then helps you sign in 2. **Sign in with existing account**: prompts for email and password (plus OTP if 2FA is enabled) 3. **Skip**: configure later with `pinner auth` ## Configuration step When the wizard reaches configuration, you choose one of: 1. **Use default settings**: HTTPS enabled, default production endpoint 2. **Customize API endpoint**: enter a custom domain and choose HTTP or HTTPS 3. **Skip**: keep current or default settings ## Options | Option | Description | |--------|-------------| | `--skip-auth` | Skip the authentication step | | `--skip-config` | Skip the configuration step | | `--reset` | Reset configuration and start fresh | | `--non-interactive` | Not supported; errors with a message to use `pinner auth` and `pinner config` instead | # CLI Doctor Run `pinner doctor` when something isn't working or you want to verify your setup. ```bash pinner doctor ``` ## Example output ``` Version: 0.1.0 OS: linux/amd64 Config: ~/.config/pinner/config.yaml Config Status: Found Endpoint: https://pinner.xyz Memory limit: 100 (default) Max retries: 3 Authentication: Authenticated Shell completion: Enabled (bash, zsh) ``` When memory limit is set to a custom value, it displays the number in megabytes: ``` Memory limit: 256 MB ``` ## JSON output For scripting or piping to other tools: ```bash pinner doctor --json # or pinner doctor -j ``` Returns structured JSON with all diagnostic information. ## What gets checked | Check | What it tells you | |-------|-------------------| | **Version** | CLI version and build info | | **OS** | Operating system and architecture | | **Config** | Where your config file lives and whether it was found | | **Config Status** | Whether the config file exists on disk (`Found` or `Not found (will use defaults)`) | | **Endpoint** | Which API endpoint the CLI is pointing at (derived from `base_endpoint` and `secure` settings) | | **Memory limit** | CAR generation memory ceiling (shows `(default)` when using the built-in 100 MB default) | | **Max retries** | Maximum retry attempts on failure | | **Authentication** | Whether an auth token is configured | | **Shell completion** | Which shells have tab-completion configured (checks bash, zsh, fish; pwsh on Windows) | ## When to run it * You're reporting a bug and need to share your environment details * The CLI can't connect to the API * Authentication isn't working as expected * You changed your config and want to confirm it took effect * You're setting up the CLI on a new machine ## Next steps If `pinner doctor` reports a problem: * **Authentication: Not authenticated**: run `pinner auth` to authenticate * **Config: Not found (will use defaults)**: run `pinner setup` to create one * **Endpoint showing wrong URL**: check `pinner config get base_endpoint` and `pinner config get secure` See [CLI Auth](/ipfs/cli/auth) for authentication details, or [CLI Config](/ipfs/cli/config) for configuration options. # Error Reference Every API error response includes a `reason` field for programmatic handling. This page lists every code the API can return, when it happens, what to do about it, and whether it's safe to retry. CLI users see friendly messages instead of raw codes; the [CLI error table](#cli-error-messages) maps them. ## Error response format ```json { "error": { "reason": "UNAUTHORIZED", "details": "Access denied. Please check your credentials and try again." } } ``` | Field | Type | Description | |-------|------|-------------| | `reason` | `string` | Error code for programmatic handling | | `details` | `string` | Human-readable description | ## Authentication (401 / 403) | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `UNAUTHORIZED` | 401 | API key is invalid, expired, or revoked | Run `pinner auth` to re-authenticate. Check `PINNER_AUTH_TOKEN` env var. | No | | `PERMISSION_DENIED` | 403 | You don't have access to this resource | Check your plan limits. Contact support if you think this is wrong. | No | ## Upload | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `UnsupportedFormat` | 422 | File format isn't supported | Use a supported format (ZIP or CAR). | No | | `CorruptedFile` | 500 | File is corrupted | Re-export or re-download the file and retry. | No | | `EmptyZIP` | 500 | ZIP archive has no files | Add content to the ZIP and retry. | No | | `PasswordProtected` | 500 | File is password-protected | Remove password protection and retry. | No | | `FileUploadFailed` | 500 | Upload failed mid-transfer | Check network and retry with backoff. | Yes | | `FILE_UPLOAD_API_FAILED` | 500 | Upload failed due to an internal error | Retry with backoff. If it persists, check status.pinner.xyz. | Yes | | `FILE_PROCESSING_FAILED` | 500 | Server couldn't process the file | Check file integrity. Retry with backoff. | Yes | | `UPLOAD_NOT_FOUND` | 404 | Upload ID doesn't exist | Check the upload ID. It may have expired or been deleted. | No | | `BLOCK_NOT_FOUND` | 404 | Requested block doesn't exist | The content may not be fully pinned yet. Wait and retry. | Yes | | `CID_PARSE_FAILED` | 400 | CID format is invalid | Check the CID string for typos or encoding issues. | No | ## Pin | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `PIN_FETCH_FAILED` | 500 | Couldn't retrieve pin info | Retry with backoff. If it persists, the pin may not exist. | Yes | | `INVALID_CID` | 422 | CID format is invalid or unsupported | Verify the CID. See [CIDs](/ipfs/concepts/cids). | No | | `INVALID_TARGET` | 422 | Target hash or peer ID is invalid | Check the target value. | No | ## Quota (429) | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `UPLOAD_QUOTA_EXCEEDED` | 429 | Too many uploads in a period | Wait and retry. Reduce upload frequency or upgrade your plan. | Yes (after cooldown) | | `STORAGE_QUOTA_EXCEEDED` | 429 | Storage limit reached | Delete unused pins or upgrade your plan. | No | | `DOWNLOAD_QUOTA_EXCEEDED` | 429 | Too many downloads in a period | Wait and retry. Reduce download frequency or upgrade your plan. | Yes (after cooldown) | ## Website | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `WEBSITE_NOT_FOUND` | 404 | Website doesn't exist | Check the website ID. See [Websites](/hosting/concepts/how-hosting-works). | No | | `INVALID_WEBSITE_STATUS` | 500 | Website status value isn't valid | Check the status you're setting. | No | | `INVALID_TARGET_TYPE` | 500 | Target type isn't valid | Must be `ipfs` or `ipns`. | No | | `INVALID_TIMESTAMP` | 500 | Timestamp format is invalid | Use ISO 8601 / RFC 3339 format. | No | ## SSL | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `INVALID_SSL_STATUS` | 500 | SSL status value isn't valid | Check the status you're setting. | No | | `SSL_STATUS_UPDATE_FAILED` | 500 | Couldn't update SSL status | Check domain ownership. Retry with backoff. | Yes | ## DNS | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `INVALID_DOMAIN` | 400 | Domain name is invalid | Check domain format. Must be a valid FQDN. | No | | `INVALID_DOMAIN_FORMAT` | 400 | Domain format isn't supported | Use a standard domain format. | No | | `INVALID_ZONE_STATUS` | 500 | DNS zone status is invalid | Check the status value. | No | | `ZONE_NOT_FOUND` | 404 | DNS zone doesn't exist | Create the zone first. See [DNS and Domains](/hosting/concepts/dns-and-domains). | No | | `RECORD_NOT_FOUND` | 404 | DNS record doesn't exist | Check the record ID. | No | | `INVALID_RECORD_TYPE` | 500 | DNS record type isn't supported | Use a supported record type (A, AAAA, CNAME, TXT, MX, NS). | No | | `DUPLICATE_RECORD` | 409 | DNS record already exists | Use the existing record or delete it first. | No | | `DELETE_FAILED` | 500 | Couldn't delete DNS zone | Retry with backoff. | Yes | | `UPDATE_FAILED` | 500 | Couldn't update DNS zone | Retry with backoff. | Yes | | `VALIDATION_FAILED` | 500 | DNS validation failed | Check nameserver configuration. Retry after fixing. | Yes | ## Validation | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `INVALID_REQUEST` | 422 | Request parameter is invalid | Check the API docs for required parameters. | No | | `INVALID_IDENTIFIER` | 422 | ID format is invalid | Check the identifier format. | No | | `INVALID_UUID_FORMAT` | 400 | UUID format is invalid | Use standard UUID format (e.g. `550e8400-e29b-41d4-a716-446655440000`). | No | | `METADATA_FETCH_FAILED` | 500 | Couldn't fetch metadata | Retry with backoff. | Yes | ## CLI error messages The CLI translates API and SDK errors into friendly messages. If you see one of these, here's what it means: | CLI message | Cause | What to do | |-------------|-------|------------| | `not authenticated` | No API key configured | Run `pinner auth` | | `path is required` | No file path given | Provide a file path argument | | `CID is required` | No CID argument given | Provide a CID argument | | `invalid CID` | CID format is wrong | Check the CID string | | `no CIDs provided` | Empty CID list | Provide at least one CID | | `file not found` | File doesn't exist locally | Check the file path | | `directory not found` | Directory doesn't exist locally | Check the directory path | | `permission denied` | Can't read the file | Check file permissions | | `pin not found` | Pin doesn't exist on Pinner | Check the CID; it may not be pinned | | `pinning failed` | Pin operation failed on the server | Retry. If it persists, check status.pinner.xyz | | `upload failed` | Upload didn't complete | Check the error details. Retry with backoff. | | `upload interrupted` | You pressed Ctrl+C or the network dropped | The CLI supports resuming; just retry the same command | | `network timeout` | Server didn't respond in time | Check your connection. Retry with backoff. | | `connection failed` | Can't reach Pinner servers | Check your internet connection and firewall | | `configuration not found` | No config file | Run `pinner setup` | | `configuration invalid` | Config file is malformed | Run `pinner setup --reset` | | `service unavailable` | Server is temporarily down | Retry after a few minutes. Check status.pinner.xyz | ## Retry strategy Some errors are safe to retry. Use exponential backoff: start at 1 second, double each attempt: * **Always retry**: 429 (quota exceeded: wait for cooldown), 500 (server error: temporary) * **Never retry**: 400, 401, 403, 404, 409, 410, 422 (client error: you must fix the request) ```typescript import { 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; } await new Promise(r => setTimeout(r, 1000 * attempt)); } } throw lastError; } ``` # Websites & Domains Pinner lets you host content on IPFS and make it accessible through a domain name. There are two paths depending on what kind of name you're using. ## Which path are you on? ### I have an onchain name (`.eth`, etc.) Onchain names resolve through the blockchain, not DNS. No DNS configuration, no SSL certificates: just point your name at your content and set the contenthash. → [**Onchain Names Quickstart**](/onchain/quickstart) ### I have a DNS domain (`.com`, `.org`, etc.) DNS domains resolve through traditional nameservers. Pinner can host your DNS, provision SSL certificates, and serve your content through ipfs.pub. → [**Website Hosting Quickstart**](/hosting/quickstart) *** ## What's the difference? | | Onchain Names | Website Hosting | |---|---|---| | **Name type** | `.eth` and other onchain names | `.com`, `.org`, `.io`, any DNS domain | | **Resolution** | On-chain contenthash → IPNS → IPFS | DNS → CNAME/DNSLink → ipfs.pub | | **Setup** | `pinner point` + set contenthash | Create website + configure DNS + validate | | **DNS hosting** | Not needed | Optional (Pinner can host your DNS) | | **SSL** | Not needed (handled by resolver) | Automatic via Let's Encrypt | | **Updates** | `pinner point` again (same contenthash) | Republish IPNS or update website target | Both paths serve content from IPFS. The difference is how the domain reaches that content: through the blockchain or through DNS. # Onchain Names Quickstart If you have an onchain name like `.eth`, you can point it at your IPFS content with a single command. No DNS configuration, no SSL certificates, no domain validation: just point and go. ## What you need * An onchain name (e.g., `vitalik.eth`) * Content already pinned to IPFS (you'll need the CID) * The [Pinner CLI installed](/ipfs/cli/getting-started) and [authenticated](/ipfs/cli/auth) ## Point your name at content ```bash pinner point vitalik.eth --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e ``` You'll see output like: ``` Domain pointed successfully Name vitalik.eth CID bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e IPNS Name k51qzi5uqu5djx... Contenthash ipns://k51qzi5uqu5djx... ``` ## Set your contenthash Copy the `Contenthash` value and set it in your name manager. For ENS names, this is the `contenthash` record in your ENS controller. Once set, your name resolves to your content. ## Verify Visit your name through a resolver: ``` https://vitalik.eth.limo ``` Your content should load. If it doesn't, check that: 1. The contenthash is set correctly in your name manager 2. The CID matches your content 3. You're using the IPNS contenthash (starts with `ipns://`), not a raw CID ## Update your content When you have new content, just point again with the new CID: ```bash pinner point vitalik.eth --cid bafybeig...updated-cid ``` The same IPNS key is reused and republished. The contenthash in your name manager stays the same: it points at the IPNS name, not the CID directly. ## Remove a pointing ```bash pinner unpoint vitalik.eth ``` This deletes the IPNS key. The name will no longer resolve to IPFS content. :::warning `unpoint` deletes the IPNS key. Any name pointing at this IPNS name will stop resolving immediately. ::: ## How it works Under the hood, `point` creates an IPNS key named after your domain, publishes your CID to it, and gives you the contenthash to set in your name manager. When someone visits your name, the resolver follows the contenthash → IPNS → CID path to serve your content. For more detail, see [How Onchain Names Work](/onchain/concepts/how-onchain-names-work) and the [point & unpoint reference](/onchain/how-to/point-unpoint). ## Next steps * [Point & unpoint reference](/onchain/how-to/point-unpoint): all flags, JSON output, troubleshooting * [How onchain names work](/onchain/concepts/how-onchain-names-work): IPNS, contenthash, and the resolution path * [IPNS and mutable addresses](/hosting/concepts/ipns): deeper dive into IPNS # Point & Unpoint The `point` and `unpoint` commands manage the link between an onchain name and IPFS content via IPNS. ## `point` Point an onchain name at IPFS content. ```bash pinner point --cid ``` ### Arguments | Argument | Description | |----------|-------------| | `` | The onchain name to point (e.g., `vitalik.eth`) | ### Flags | Flag | Alias | Description | |------|-------|-------------| | `--cid` | `-c` | CID to point the name at (required) | ### Behavior `point` is idempotent: 1. If no IPNS key exists for the name, one is created automatically 2. If an IPNS key already exists, it's reused and the new CID is published to it 3. The contenthash (`ipns://`) is returned for you to set in your name manager ### Examples Point a name at content: ```bash pinner point vitalik.eth --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e ``` Update content (same command, new CID): ```bash pinner point vitalik.eth --cid bafybeig...updated-cid ``` Get JSON output: ```bash pinner point vitalik.eth --cid bafybeig... --json ``` ```json { "name": "vitalik.eth", "cid": "bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e", "ipns_name": "k51qzi5uqu5djx...", "contenthash": "ipns://k51qzi5uqu5djx...", "created": true } ``` The `created` field is `true` when a new IPNS key was created, `false` when an existing key was reused. ### What to do after running `point` `point` handles Pinner's side: the IPNS key is created and the CID is published. But your name won't resolve until you set the contenthash in your name manager. 1. Copy the `Contenthash` value from the output 2. Set it as the `contenthash` record for your name (in your ENS controller, etc.) 3. Verify by visiting your name through a resolver (e.g., `https://vitalik.eth.limo` for ENS names) ### Troubleshooting | Problem | Cause | Fix | |---------|-------|-----| | `name is required` | No name argument provided | Pass the name: `pinner point vitalik.eth --cid ` | | `failed to publish to IPNS` | CID is invalid or network error | Verify the CID is valid and try again | | Name doesn't resolve after setting contenthash | Contenthash not set, or set incorrectly | Verify the contenthash matches the output from `point` exactly; it should start with `ipns://` | | Name resolves but shows old content | IPNS record hasn't propagated | Wait a moment for IPNS propagation and refresh | *** ## `unpoint` Remove the IPNS key for an onchain name. ```bash pinner unpoint ``` ### Arguments | Argument | Description | |----------|-------------| | `` | The onchain name to unpoint (e.g., `vitalik.eth`) | ### Behavior 1. Finds the IPNS key for the given name 2. Deletes the key 3. The name will no longer resolve to IPFS content :::warning `unpoint` deletes the IPNS key immediately. Any name pointing at this IPNS name will stop resolving. This cannot be undone; you'll need to run `point` again to create a new key, and then update your contenthash to the new IPNS name. ::: ### Examples Remove a pointing: ```bash pinner unpoint vitalik.eth ``` You'll see output like: ``` Domain unpointed successfully Name vitalik.eth IPNS Name k51qzi5uqu5djx... ``` JSON output: ```bash pinner unpoint vitalik.eth --json ``` ```json { "name": "vitalik.eth", "ipns_name": "k51qzi5uqu5djx...", "deleted": true } ``` ### Troubleshooting | Problem | Cause | Fix | |---------|-------|-----| | `no IPNS key found for "vitalik.eth"` | No IPNS key with that name exists | The name may have already been unpointed, or was never pointed | | Name still resolves after unpoint | Contenthash still set in name manager | Remove the contenthash from your name manager separately; `unpoint` only handles Pinner's side | *** ## When to use `point` vs lower-level IPNS commands `point` is the recommended way to manage onchain names. It handles key creation, publishing, and idempotent updates in a single command. Use the lower-level `ipns` commands when you need more control: | What you need | Command | |---------------|---------| | Quick onchain name setup | `pinner point --cid ` | | Import an existing private key | `pinner ipns keys create --key ` | | Set a custom TTL on the IPNS record | `pinner ipns publish --key-name --ttl 24h` | | Resolve an IPNS name | `pinner ipns resolve ` | | Republish without changing the CID | `pinner ipns republish ` | | List all IPNS keys | `pinner ipns keys list` | | Delete a key by name or ID | `pinner ipns keys delete ` | See [Publish with IPNS](/hosting/how-to/publish-ipns) for the full IPNS reference. # How Onchain Names Work Onchain name systems like ENS (Ethereum Name Service) let you map a human-readable name to IPFS content. This page explains how that resolution works and where Pinner fits in. ## The resolution path When someone visits `vitalik.eth` in a browser, here's what happens: 1. **The browser queries the name resolver.** For ENS names, services like eth.limo handle this. 2. **The resolver looks up the `contenthash` record.** This is an onchain record stored in the naming contract. It points at either an IPFS CID or an IPNS name. 3. **The resolver follows the contenthash.** If it's an IPNS name, the resolver looks up the current CID that the IPNS name points to. If it's a direct CID, it fetches that. 4. **The content is served from IPFS.** The resolver's gateway fetches the CID from the IPFS network and serves it to the browser. ``` vitalik.eth → contenthash: ipns://k51qzi5uqu5djx... → IPNS resolves to: bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e → Content served from IPFS ``` ## Why IPNS, not direct CIDs You could set your contenthash to a direct CID (`ipfs://bafybeig...`) instead of an IPNS name. Some people do. But there's a trade-off: | Approach | Update process | Trade-off | |----------|---------------|-----------| | Direct CID | Change the contenthash on-chain every time | Requires an on-chain transaction for each update (costs gas, takes time) | | IPNS name | Update the IPNS record off-chain; contenthash stays the same | One on-chain transaction to set the contenthash, then all updates happen on Pinner's side | IPNS adds a layer of indirection. Your contenthash points at a stable IPNS name, and you update what that IPNS name resolves to without touching the blockchain again. When you run `pinner point`, you're updating the IPNS record; no on-chain transaction needed. ## Where Pinner fits Pinner's job in this flow is straightforward: 1. **Pin your content**: make sure it stays available on IPFS 2. **Manage the IPNS key**: create it, publish CIDs to it 3. **Keep the IPNS record current**: republish when you update content `pinner point` does steps 2 and 3 in one command. Step 1 happens separately when you upload or pin content. ``` Your content → Pinner pins it → Pinner publishes to IPNS → You set contenthash → Resolver serves it ``` Pinner doesn't handle the on-chain transaction (setting the contenthash). That's done in your name manager: your ENS controller or smart contract. ## What about DNS? Onchain name resolution doesn't use DNS. There are no CNAME records, nameserver delegation, or DNS hosting involved. The contenthash is stored on-chain, and resolvers read it directly from the blockchain. This is different from [website hosting with a DNS domain](/hosting/concepts/dns-and-domains), where Pinner sets up DNS records and SSL certificates. Onchain names skip all of that; the blockchain is the name resolution layer. ## Key terminology | Term | What it means | |------|---------------| | **Contenthash** | The on-chain record that maps a name to content. Set in your name manager. | | **IPNS** | InterPlanetary Name System: a mutable address that always points at the current CID | | **IPNS key** | A cryptographic key used to publish and update an IPNS record. Pinner manages these for you. | | **Resolver** | A service that looks up on-chain records and serves the content (e.g., eth.limo for ENS names) | ## Related * [Onchain Names Quickstart](/onchain/quickstart): get running in under a minute * [Point & unpoint reference](/onchain/how-to/point-unpoint): command details and troubleshooting * [IPNS and mutable addresses](/hosting/concepts/ipns): deeper explanation of how IPNS works * [How website hosting works](/hosting/concepts/how-hosting-works): the DNS-based alternative for .com domains # Quickstart Deploy a static site to IPFS and make it accessible via a custom domain, all in under 5 minutes. You need a Pinner account and an API key. Don't have one? [Create an API key](/account/api-tokens). :::code-group ```bash [CLI] # 1. Authenticate (interactive: prompts for email and password) pinner auth # 2. Build your site (example with Vite) npm run build # 3. Deploy: create a website pointing to your CID pinner websites create mysite.pinner.xyz --cid QmYourCIDHere # Done: your site is live on IPFS ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); // Create a website pointing to your Content Identifier (CID) const site = await pinner.websites.createWebsite({ domain: "mysite.pinner.xyz", target_type: "ipfs", target_hash: "QmYourCIDHere", }); console.log("Site ID:", site.id); console.log("Domain:", site.domain); console.log("Status:", site.status); ``` ::: ### Non-interactive (CI/CD) If you already have an API key from another machine or for automation: ```bash pinner auth YOUR_JWT ``` ## What just happened The CLI created a website record that maps a domain to an IPFS CID. You provide the domain and CID, and Pinner handles routing and DNS validation. The SDK does the same thing; you give it a `domain`, `target_type`, and `target_hash` (CID), and Pinner handles the rest. ## Next steps * [Deploy a static site end-to-end](/hosting/tutorials/deploy-static-site): a full walkthrough with custom domains * [Add a custom domain](/hosting/how-to/custom-domains) * [Understand how website hosting works](/hosting/concepts/how-hosting-works) # Deploy a Static Site This tutorial walks you through the full process: build a static site, deploy it to IPFS, and make it accessible at a custom domain. By the end, you'll understand each step of the deployment pipeline. ## Prerequisites * A Pinner account with an [API key](/account/api-tokens) * The [Pinner CLI installed and authenticated](/ipfs/cli/auth) * A static site to deploy (we'll use Vite as an example) * A domain you control (for custom domain setup) ## Step 1: Build your site Build your project the way you normally would. For a Vite project: ```bash npm run build ``` This outputs static files to `./dist`. Any static site generator works: Next.js export, Astro, Hugo, plain HTML. As long as the output is a directory of static files, Pinner can host it. ## Step 2: Upload with the CLI ```bash pinner upload ./dist ``` This uploads the directory to IPFS and prints the CID. Note the CID from the output; you'll use it in the next step. ## Step 3: Create the website Create a website record linking your domain to the uploaded content: ```bash pinner websites create mysite.example.com --cid ``` :::info The CLI provides an interactive wizard that guides you through the full process: ```bash pinner websites wizard ``` The wizard handles authentication, content source, target type, domain, DNS mode, and validation in one guided flow. ::: ## Step 4: (SDK) Upload and create the website If you're using the SDK, use `uploadDirectory` for directory uploads and handle the website creation separately: ```typescript import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; import path from "path"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); // Upload the build output as a directory const files: File[] = []; for (const entry of fs.readdirSync("./dist", { recursive: true })) { const fullPath = path.join("./dist", entry.toString()); if (!fs.statSync(fullPath).isFile()) continue; const buffer = fs.readFileSync(fullPath); files.push(new File([buffer], entry.toString())); } const operation = await pinner.uploadDirectory(files); const result = await operation.result; console.log("CID:", result.cid); // Create the website const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: result.cid, }); console.log("Website created:", site.id, site.domain); ``` ## Step 5: Set up your custom domain If you used a custom domain (not a `*.pinner.xyz` subdomain), you need to prove you own it. The website response includes a `validation_token` field. Add a DNS TXT record at your domain registrar: | Record | Name | Value | |--------|------|-------| | TXT | (shown in website details) | (the `validation_token` value, e.g. `pinner-verify=`) | The exact name for the TXT record is shown in your website details. You also need a DNSLink TXT record pointing to your content: | Record | Name | Value | |--------|------|-------| | TXT | `_dnslink.` | `dnslink=/ipfs/` | Then trigger validation: ```typescript const validation = await pinner.websites.validateWebsite(site.id); console.log("Valid:", validation.valid, validation.message); ``` Or with the CLI: ```bash pinner websites validate mysite.example.com ``` ## Step 6: Add a CNAME record Point your domain to Pinner's hosting infrastructure. The gateway domain is shown in your website details or configuration: ```bash pinner websites config ``` Use the gateway domain shown for your CNAME record: | Record | Name | Value | |--------|------|-------| | CNAME | `www` | (the gateway domain from config) | For root domains, delegate to Pinner's nameservers using the `--dns-hosting` flag when creating the website, or use an A record if your DNS provider supports it. ## Step 7: Wait for SSL Pinner provisions an SSL certificate automatically after your DNS records are in place. No configuration on your part; just wait. Check the status with the CLI: ```bash pinner websites ssl status mysite.example.com ``` Or with the SDK: ```typescript const ssl = await pinner.websites.getSSLStatus("mysite.example.com"); console.log("SSL status:", ssl.status); ``` SSL moves through these states: `pending` → `issuing` → `ready`. If something goes wrong, it lands in `failed`. You can also watch for SSL readiness: ```typescript const watcher = pinner.websites.watchSSL("mysite.example.com", { interval: 5000, // check every 5 seconds timeout: 300000, // give up after 5 minutes }); await watcher.start({ onReady: (status) => console.log("SSL ready!", status), onError: (error) => console.error("SSL error:", error.message), onStatus: (status) => console.log("SSL progress:", status.status), }); ``` ## Step 8: Visit your site Once DNS propagates and SSL is ready, your site is live at `https://mysite.example.com`. ## Recap You built a static site, uploaded it to IPFS, created a website record, proved domain ownership with DNS validation records, pointed DNS at Pinner, and let SSL provision automatically. That's the full cycle. ## Next steps * [Update your site with a new deploy](/hosting/how-to/manage-websites) * [Use IPNS for updatable content addresses](/hosting/concepts/ipns) * [Automate deploys with GitHub Actions](/hosting/how-to/deploy-github-actions) # Deploy with GitHub Actions Deploy your static site to IPFS via Pinner automatically every time you push to your repository, using the [`lumeweb/pinner-deploy-action`](https://github.com/lumeweb/pinner-deploy-action) GitHub Action. ## Set up secrets Add your Pinner API key as a repository secret: 1. Go to **Settings → Secrets and variables → Actions** 2. Click **New repository secret** 3. Name: `PINNER_AUTH_TOKEN` 4. Value: your API key from [API Keys](/account/api-tokens) ## Create the workflow Create `.github/workflows/deploy.yml`: ```yaml name: Deploy to Pinner on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: npm ci && npm run build - name: Deploy to Pinner uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_AUTH_TOKEN }} path: ./dist ``` This workflow builds your site and deploys the `./dist` directory to IPFS via Pinner in a single step. The action outputs the content CID and a gateway URL, which you can use in subsequent steps. ## Upload and set up a domain To automatically point a domain at the deployed content, add the `domain` input. The action will create or update the website for you: ```yaml - name: Deploy to Pinner uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_AUTH_TOKEN }} path: ./dist domain: myapp.example.com ``` When `domain` is set, the action looks up an existing website for that domain and updates its `target_hash`, or creates a new website if none exists. ## Deploy with IPNS for continuous updates If your site updates frequently, use IPNS so the domain always resolves to the latest content. Pass an `ipns-key` (a key name or numeric ID) to publish the CID under that key: ```yaml - name: Deploy to Pinner uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_AUTH_TOKEN }} path: ./dist ipns-key: my-app domain: myapp.example.com ``` The action resolves the key name to its numeric ID, publishes the new CID to IPNS, and updates the website's domain, all in one step. ## Clean up previous pins Set `remove-previous: true` to automatically unpin the old CID after a successful deploy. This requires either `ipns-key` or `domain` so the action can identify the previous CID: ```yaml - name: Deploy to Pinner uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_AUTH_TOKEN }} path: ./dist ipns-key: my-app domain: myapp.example.com remove-previous: true ``` ## Pin an existing CID If you already have a CID (for example, from a previous build step), you can skip the upload and just pin it: ```yaml - name: Pin existing CID uses: lumeweb/pinner-deploy-action@v0 with: api-key: ${{ secrets.PINNER_AUTH_TOKEN }} cid: QmExampleCID... ``` Either `path` or `cid` must be provided. ## Action inputs | Input | Required | Default | Description | | ----------------- | -------- | ------------------------- | ----------------------------------------------------------------- | | `api-key` | Yes | N/A | Pinner API key | | `path` | No | N/A | Local directory or file to upload to IPFS | | `cid` | No | N/A | Existing CID to pin (skip upload) | | `endpoint` | No | `https://ipfs.pinner.xyz` | Pinner API endpoint | | `ipns-key` | No | N/A | IPNS key name or ID to publish under | | `domain` | No | N/A | Domain for gateway website setup | | `remove-previous` | No | `false` | Remove previous pin after deploy. Requires `ipns-key` or `domain` | Either `path` or `cid` must be provided. ## Action outputs | Output | Description | | ------------- | ----------------------------------- | | `cid` | CID of the uploaded/pinned content | | `gateway-url` | Gateway URL for the content | | `ipns-name` | IPNS name if `ipns-key` was set | | `website-id` | Website ID if `domain` was set | ## Deploy with the SDK If you prefer programmatic control, you can call the [`@lumeweb/pinner`](https://www.npmjs.com/package/@lumeweb/pinner) SDK directly from a custom script: ```yaml - name: Deploy with SDK env: PINNER_AUTH_TOKEN: ${{ secrets.PINNER_AUTH_TOKEN }} run: node scripts/deploy.mjs ``` ```javascript // scripts/deploy.mjs import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; import path from "path"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); // Upload a directory const files: File[] = []; for (const entry of fs.readdirSync("./dist", { recursive: true })) { const fullPath = path.join("./dist", entry.toString()); if (!fs.statSync(fullPath).isFile()) continue; files.push(new File([fs.readFileSync(fullPath)], entry.toString())); } const operation = await pinner.uploadDirectory(files, { name: "my-site" }); const result = await operation.result; console.log("CID:", result.cid); // Update existing website or create a new one const siteId = Number(process.env.WEBSITE_ID); if (siteId) { await pinner.websites.updateWebsite(siteId, { domain: "mysite.example.com", target_hash: result.cid, target_type: "ipfs", }); console.log("Updated website", siteId); } else { const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_hash: result.cid, target_type: "ipfs", }); console.log("Created website", site.id); } ``` ### Publish to IPNS with the SDK ```javascript // scripts/deploy-ipns.mjs import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; import path from "path"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN }); // Upload a directory const files: File[] = []; for (const entry of fs.readdirSync("./dist", { recursive: true })) { const fullPath = path.join("./dist", entry.toString()); if (!fs.statSync(fullPath).isFile()) continue; files.push(new File([fs.readFileSync(fullPath)], entry.toString())); } const operation = await pinner.uploadDirectory(files, { name: "my-site" }); const result = await operation.result; // Publish to IPNS const IPNS_KEY_ID = Number(process.env.IPNS_KEY_ID); await pinner.ipns.publish({ cid: result.cid, key_id: IPNS_KEY_ID, }); console.log("Published to IPNS:", result.cid); ``` The domain resolves to the IPNS name, which always points at the latest CID. ## Next steps * [Understand IPNS publishing](/hosting/concepts/ipns) * [Set up a custom domain](/hosting/how-to/custom-domains) * [Monitor SSL provisioning](/hosting/how-to/check-ssl) # Manage Websites List your websites, update their content, and delete sites you no longer need. ## List websites :::code-group ```bash [CLI] pinner websites list ``` ```typescript [SDK] const result = await pinner.websites.listWebsites(); console.log(`You have ${result.total} website(s)`); for (const site of result.data) { console.log(`- ${site.domain} (${site.status}) → ${site.target_hash}`); } ``` ::: ## Get a single website ```typescript const site = await pinner.websites.getWebsite(42); console.log(site.domain, site.status, site.target_hash); ``` ## Update a website Point your domain at new content after a rebuild: :::code-group ```bash [CLI] # Deploy new content and update the website record pinner websites update --cid ``` ```typescript [SDK] // Upload new content first (for a directory of files) const operation = await pinner.uploadDirectory(newFiles); const result = await operation.result; // Update the website to point at the new CID const updated = await pinner.websites.updateWebsite(42, { domain: "example.com", target_type: "ipfs", target_hash: result.cid, }); console.log("Updated:", updated.domain, "→", updated.target_hash); ``` ::: The `updateWebsite` method accepts a `WebsiteRequest` object with `domain`, `target_type`, and `target_hash`. All fields are sent in the request; omit a field to leave it unchanged, or set it to update. To enable DNS hosting, include `dns_hosting_enabled`: ```typescript const updated = await pinner.websites.updateWebsite(42, { domain: "newdomain.example.com", target_type: "ipfs", target_hash: "QmNewCID", dns_hosting_enabled: true, }); ``` :::tip When changing `target_hash`, you must also provide `target_type` (`"ipfs"` or `"ipns"`). Omitting `target_type` when `target_hash` is present will return a validation error. ::: ## Delete a website :::code-group ```bash [CLI] pinner websites delete ``` ```typescript [SDK] await pinner.websites.deleteWebsite(42); ``` ::: :::warning Deleting a website performs a soft delete: the record is marked as deleted but preserved. DNS records for the domain are cleaned up automatically. Your content stays on IPFS unless you also unpin it, but the domain mapping and SSL certificate are removed. Blocked websites cannot be deleted. ::: ## Validate domain ownership If your custom domain hasn't been validated yet, trigger a validation check: ```typescript const result = await pinner.websites.validateWebsite(42); console.log(result.valid, result.reason, result.message); ``` Validation reasons: | Reason | Meaning | |--------|---------| | `validated` | Domain ownership confirmed | | `token_expired` | Validation token has expired, redeploy | | `dns_missing` | The TXT record wasn't found | | `dns_mismatch` | TXT record exists but token doesn't match | | `token_missing` | No validation token on the website record | ## Website properties Every website has these fields: | Field | Description | |-------|-------------| | `id` | Unique identifier | | `domain` | The domain name for the website | | `target_type` | Content address type (`ipfs` or `ipns`) | | `target_hash` | The CID or IPNS name the domain points to | | `status` | Current website status (see below) | | `active_cid` | The currently-published content CID (present for IPNS targets) | | `validation_token` | Token for proving domain ownership | | `validation_record_host` | DNS hostname for the TXT record (e.g. `pinner-verify.example.com`) | | `validation_expires_at` | When the validation token expires | | `dns_hosting_enabled` | Whether Pinner manages DNS for this domain | | `dns_zone_id` | ID of the DNS zone hosting this website's records (when DNS hosting enabled) | | `ipns_key_id` | ID of the linked IPNS key (set when DNS hosting auto-creates a key) | | `is_subdomain` | Whether this is a subdomain of a shared DNS zone | | `gateway_domain` | Gateway domain for constructing public URLs | | `last_checked_at` | When the website was last checked by the janitor | | `ssl` | SSL certificate status (see [SSL](/hosting/concepts/ssl)) | | `expired` | Whether the validation token has expired | | `created` / `updated` | Timestamps | ### Website statuses A website can be in one of four statuses: | Status | Description | |--------|-------------| | `pending_validation` | Awaiting domain ownership validation (initial status after creation) | | `active` | Domain validated and website is live | | `broken` | Target content is unreachable | | `blocked` | Website has been blocked by an administrator | # Publish with IPNS :::tip Using an onchain name like `.eth`? The `pinner point` command handles IPNS key creation, publishing, and updates in one step. See [Onchain Names Quickstart](/onchain/quickstart). ::: IPNS (InterPlanetary Name System) lets you publish content under a stable name that can be updated to point at new CIDs. This is useful for websites that update frequently; instead of changing DNS records every time, you update the IPNS record. ## How IPNS fits into website hosting When you create a website, you can set `target_type` to either `ipfs` or `ipns`: * **`ipfs`**: The domain points directly at a CID. To update, change `target_hash` on the website record. * **`ipns`**: The domain points at an IPNS name. To update, republish the IPNS name with a new CID. The domain doesn't change. IPNS adds a layer of indirection. You update content by republishing the IPNS name instead of updating the website record. ## Manage IPNS keys ### List keys :::code-group ```bash [CLI] pinner ipns keys list ``` ```typescript [SDK] const keys = await pinner.ipns.listKeys(); console.log(`You have ${keys.length} key(s)`); for (const key of keys) { console.log(`- ${key.id}: ${key.ipns_name}`); } ``` ::: ### Create a key :::code-group ```bash [CLI] pinner ipns keys create my-website-key ``` ```typescript [SDK] const key = await pinner.ipns.createKey({ name: "my-website-key", }); console.log("IPNS name:", key.ipns_name); ``` ::: ### Delete a key :::code-group ```bash [CLI] pinner ipns keys delete ``` ```typescript [SDK] await pinner.ipns.deleteKey(keyId); ``` ::: :::warning Deleting an IPNS key means the IPNS name can no longer be resolved. Any domains pointing at this name will break. ::: ## Publish content to an IPNS name Point an IPNS name at a CID: :::code-group ```bash [CLI] pinner ipns publish --key-name ``` ```typescript [SDK] const result = await pinner.ipns.publish({ key_id: key.id, cid: "QmYourCID", }); console.log("Published:", result.name, "→", result.value); ``` ::: ## Create a website with an IPNS target ```typescript const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipns", target_hash: key.ipns_name, }); ``` Now when you update your site, publish a new CID to the same IPNS name; the domain follows automatically. ## Republish IPNS records have a limited lifetime. Republish to extend it: :::code-group ```bash [CLI] pinner ipns republish ``` ```typescript [SDK] await pinner.ipns.republish(key.ipns_name); ``` ::: ## Resolve an IPNS name Look up what CID an IPNS name currently points to: :::code-group ```bash [CLI] pinner ipns resolve k51qzi5... ``` ```typescript [SDK] const resolved = await pinner.ipns.resolve("k51qzi5..."); console.log("Points to:", resolved.value); ``` ::: ## When to use IPNS vs direct IPFS | Scenario | Use | |----------|-----| | Static content that rarely changes | Direct IPFS (`target_type: "ipfs"`) | | Frequently updated sites | IPNS (`target_type: "ipns"`) | || CI/CD automated deploys | IPNS: update once, domain follows | || One-off deployments | Direct IPFS: simpler | IPNS resolution is slightly slower than direct IPFS because it adds a lookup step. For most sites, the difference is negligible. # Custom Domains By default, your site is available at a `*.pinner.xyz` subdomain. To use your own domain, you need to prove ownership and configure DNS. ## Subdomain vs custom domain | Type | Example | Setup | |------|---------|-------| | Subdomain | `mysite.pinner.xyz` | Automatic; no DNS needed | | Custom domain | `mysite.example.com` | Requires DNS validation and records | ## Step 1: Create the website with your domain :::code-group ```typescript [SDK] const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: "QmYourCID", }); ``` ```bash [CLI] pinner website deploy ./dist --domain mysite.example.com ``` ::: Pinner assigns a `validation_token` to the website. You'll need this in the next step. :::info `dns_hosting_enabled` defaults to `true` for new websites. When enabled, Pinner automatically creates a DNS zone and the required DNS records for you. If you prefer to manage DNS yourself at your registrar, set `dns_hosting_enabled: false`. ::: ## Step 2: Add DNS records At your domain registrar, create the following DNS records: | Record | Name | Value | |--------|------|-------| | TXT | `_dnslink` | `dnslink=/ipfs/QmYourCID` | | TXT | `` | `` | **DNSLink record**: The `_dnslink` TXT record tells IPFS gateways how to resolve your domain. Set the value to a `dnslink=` path matching your target type and hash: * IPFS target: `dnslink=/ipfs/QmYourCID` * IPNS target: `dnslink=/ipns/` **Validation record**: The `validation_record_host` field on the website tells you the exact hostname for the verification TXT record (e.g. `pinner-verify.mysite.example.com`). The `validation_token` field contains the full TXT record value (e.g. `pinner-verify=abc123def456`). :::info The exact verification TXT record hostname and value depend on the server configuration. Always use the `validation_record_host` and `validation_token` from the website response; don't hardcode the prefix. ::: ## Step 3: Route traffic to the gateway Add a DNS record to point your domain at the Pinner gateway: | Record | Name | Value | |--------|------|-------| | ALIAS | `@` (root) | `` | For root domains (`example.com`), use an ALIAS record pointing to the gateway domain. For subdomains, a CNAME record also works. Use `getWebsiteConfig()` to discover the gateway domain: ```typescript const config = await pinner.websites.getWebsiteConfig(); console.log("Gateway domain:", config.gateway_domain); console.log("Nameservers:", config.nameservers); ``` ## Step 4: Trigger validation :::code-group ```typescript [SDK] const result = await pinner.websites.validateWebsite(site.id); if (result.valid) { console.log("Domain validated!"); } else { console.log("Not yet valid:", result.reason, result.message); } ``` ```bash [CLI] pinner websites validate ``` ::: DNS propagation can take a few minutes. If validation fails with `dns_missing` or `dns_mismatch`, wait a few minutes and retry. Validation checks both the DNSLink record and the verification token record. A `dns_mismatch` reason means the DNSLink record was found but doesn't match the expected target. A `token_missing` reason means the verification TXT record was not found. ## Step 5: Wait for SSL SSL provisions automatically after DNS is in place. See [SSL](/hosting/concepts/ssl) for details on checking status. ## Use Pinner-managed DNS If you don't want to manage DNS at your registrar, enable Pinner's DNS hosting. This delegates DNS management to Pinner entirely. DNS hosting is enabled by default for new websites. ### Disable during website creation ```typescript const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: "QmYourCID", dns_hosting_enabled: false, }); ``` ### Update on an existing website ```typescript await pinner.websites.updateWebsite(site.id, { dns_hosting_enabled: false, }); ``` When DNS hosting is enabled, Pinner creates a DNS zone for your domain and handles the DNSLink, validation, and ALIAS records for you. Update your domain registrar's nameservers to point to Pinner's nameservers (shown in `getWebsiteConfig()`). See [Manage DNS Zones](/dns/how-to/manage-zones) and [Manage DNS Records](/dns/how-to/manage-records) for the DNS-specific commands. ## Troubleshooting ### Validation keeps failing with `dns_missing` DNS propagation can take up to 48 hours (usually a few minutes). Verify the DNSLink and verification TXT records exist: ```bash dig TXT _dnslink.mysite.example.com dig TXT ``` ### Validation fails with `dns_mismatch` The DNSLink TXT record was found but doesn't match the expected target. Check that the `_dnslink` TXT record value is correct; it should be `dnslink=/ipfs/` or `dnslink=/ipns/`. ### Validation fails with `token_missing` The verification TXT record was not found at the expected hostname. Check that you added the TXT record at `` with the value from `validation_token`. ### Validation fails with `token_expired` The validation token has expired. Re-create the website or update it to get a fresh token. # Check SSL Status Pinner provisions SSL certificates automatically for custom domains. Once DNS is configured and validated, Pinner's certificate manager (backed by Caddy) handles provisioning; you just wait for the process to complete. ## Check the current status ```typescript const response = await pinner.websites.getSSLStatus("mysite.example.com"); const ssl = response.ssl; if (ssl) { console.log("Status:", ssl.status); if (ssl.error) { console.log("Error:", ssl.error); } if (ssl.issued_at) { console.log("Issued at:", ssl.issued_at); } } ``` ## SSL states | Status | Meaning | |--------|---------| | `pending` | Waiting to start provisioning: DNS may not have propagated yet | | `issuing` | Certificate is being requested and provisioned | | `ready` | Certificate issued and active; the site is serving HTTPS | | `failed` | Provisioning failed; check the `error` field for details | ## Using the CLI You can also check SSL status from the command line: ```bash # Check once pinner websites ssl status example.com # Watch for changes (polls until settled) pinner websites ssl status example.com --watch # JSON output pinner websites ssl status example.com --json ``` ## Troubleshooting ### SSL stuck at `pending` DNS records haven't propagated yet. This is normal; it can take a few minutes to a few hours depending on your registrar. Verify your CNAME and TXT records are in place: ```bash dig CNAME mysite.example.com dig TXT pinner-verify.mysite.example.com ``` ### SSL in `failed` state Check the `error` field for details. Common causes: * **DNS not pointing at Pinner**: The CNAME or A record is missing or pointing elsewhere * **Domain validation expired**: Re-validate the domain (see [Custom Domains](/hosting/how-to/custom-domains)) * **Rate limit hit**: Too many SSL requests in a short time; wait and retry ### Keep watching for SSL readiness Use the CLI's `--watch` flag to poll until the certificate reaches `ready` or `failed`: ```bash pinner websites ssl status example.com --watch ``` # How Website Hosting Works Pinner hosts static websites on IPFS and makes them accessible through regular domain names with HTTPS. No servers to manage, no containers to configure; just upload your files and point a domain at them. ## The pieces Website hosting on Pinner involves three systems working together: 1. **IPFS storage**: Your site's files are pinned on IPFS and addressed by Content Identifier (CID) 2. **DNS**: Uses DNSLink TXT records to map your domain to IPFS content 3. **SSL**: Provides HTTPS for your domain automatically via Caddy on-demand TLS ## The flow ``` Build output → Upload to IPFS → Get CID → Create website record → Configure DNSLink → Validate DNS → SSL provisions → Site is live ``` Here's what happens at each step: ### Upload Your static files (HTML, CSS, JS, images) get uploaded to IPFS as a directory. The directory gets a Content Identifier (CID), a content-addressed identifier that represents your entire site. ### Website record A website record on Pinner maps a domain to IPFS content. It stores: * The **domain** name (e.g. `mysite.example.com`) * The **target type**: either `ipfs` (direct IPFS CID) or `ipns` (IPNS name for updatable content) * The **target hash**: the CID or peer ID identifying the content * The **validation token** used to prove you own the domain * The **SSL status** tracking certificate provisioning (`pending` → `issuing` → `ready` or `failed`) ### DNSLink Pinner uses **DNSLink** to map your domain to IPFS content. DNSLink is an IPFS convention that uses a `TXT` record at `_dnslink.` to point to content on the IPFS network. When Pinner manages your DNS, it creates these records: | Record | Type | Purpose | |--------|------|---------| | `_dnslink.mysite.example.com` | `TXT` | DNSLink target (e.g. `dnslink=/ipfs/Qm...` or `dnslink=/ipns/12D3KooW...`) | | `pinner-verify.mysite.example.com` | `TXT` | Validation token, proves domain ownership (e.g. `pinner-verify=a1b2c3...`) | | `mysite.example.com` | `ALIAS` | Points to the gateway domain so traffic reaches Pinner | For self-managed DNS, you need to add the `_dnslink` TXT record yourself so the gateway can resolve your content. The DNSLink value follows the format `dnslink=/ipfs/` for direct IPFS targets or `dnslink=/ipns/` for IPNS targets. ### DNS hosting For Pinner to serve your domain, DNS must point at Pinner's infrastructure. You can either: * **Use Pinner-managed DNS**: Delegate the DNS zone to Pinner's nameservers (powered by PowerDNS). Pinner automatically creates and manages the DNSLink, validation, and ALIAS records for you. This also enables automatic updates; when you change your site's content, Pinner updates the DNSLink record to point at the new CID. * **Bring your own DNS**: Add the required `_dnslink` TXT record and other DNS entries at your registrar. You're responsible for updating them when content changes. When you use Pinner-managed DNS with a direct IPFS target, Pinner automatically creates an IPNS key and converts the target to IPNS. This lets Pinner publish new CIDs under the same IPNS name, and the DNSLink record always resolves to the latest content; no manual DNS updates needed. ### SSL Once DNS is in place, Pinner automatically provisions an SSL certificate for your domain via Caddy's on-demand TLS. Before issuing a certificate, Caddy checks with Pinner's gateway to confirm the domain is allowed. You don't need to buy, install, or renew certificates; Pinner handles the entire lifecycle. ### How the gateway serves your site When a visitor hits `https://mysite.example.com`, the request flows through Pinner's gateway: 1. **Access check**: The gateway extracts the domain from the `Host` header and checks with Pinner's API to verify the website is active and allowed 2. **Path rewrite**: The request path is rewritten to `/ipfs/` or `/ipns/` based on the target type 3. **Content resolution**: The gateway uses DNSLink resolution and the Boxo IPFS library to fetch your content from the IPFS network 4. **Serve**: The content is served to the browser with proper headers (ETag, Cache-Control, range request support) If the website is pending validation, the gateway shows a friendly waiting page. If it's broken or gone, it returns an appropriate error. ## Content addressing vs traditional hosting | Traditional hosting | Pinner website hosting | |---------------------|------------------------| | Files live on a specific server | Files live on IPFS, a distributed network | | Server URL identifies the content | CID identifies the content | | Updating means replacing files on the server | Updating means uploading new files and pointing the domain at the new CID | | Server downtime = site downtime | No single server to go down | ## Subdomains If you just want to get something live quickly, Pinner provides `*.pinner.xyz` subdomains. No DNS configuration needed; the subdomain is set up automatically when you create the website. Custom domains require DNS validation and DNS configuration, but give you full branding control. ## Updating your site With **IPNS targeting** (the default for Pinner-managed DNS), you publish the new CID under the same IPNS name. Pinner automatically updates the IPNS record, and the domain resolves to the new content. This is the recommended approach for sites that change frequently. With **direct IPFS targeting**, you update content by uploading new files and updating the website's target hash to the new CID. The DNSLink record must also be updated to point at the new CID. The old CID stays on IPFS until you unpin it. See [IPNS](/hosting/concepts/ipns) for more on the IPNS approach. # IPNS IPNS (InterPlanetary Name System) is a naming system for IPFS. It lets you associate a stable name with a Content Identifier (CID), and update that association over time. For website hosting, this means you can update your site's content without changing the domain configuration. ## The problem IPNS solves IPFS uses content addressing: a CID is derived from the content itself. Changed content means a new CID. This is a strength (integrity) but a challenge for websites: every time you update your site, the CID changes. Without IPNS, updating a website looks like this: 1. Upload new content → get new CID 2. Update the website record to point at the new CID 3. Wait for DNS to propagate (if applicable) With IPNS, step 2 goes away: 1. Upload new content → get new CID 2. Publish (or republish) the IPNS name to point at the new CID 3. The domain still points at the same IPNS name, which now resolves to the new CID ## How IPNS works on Pinner ### Keys An IPNS key is an Ed25519 cryptographic keypair. Every key has two identifiers: * **IPNS name**: the public key encoded as a CIDv1 with the `libp2p-key` codec (e.g. `k51qzi5...`). This is the stable name you use in DNSLink records and gateways. * **Peer ID**: the base36-encoded libp2p peer identity (e.g. `12D3KooW...`). This is used internally for publishing and resolving. Both are derived from the same public key. You can use either to refer to the key, but the IPNS name (`k51...`) is what appears in `/ipns/` paths. When you create a key, Pinner returns: * `id`: the numeric ID used in subsequent API calls * `name`: the human-readable label you assigned * `ipns_name`: the public IPNS name (e.g. `k51qzi5...`) * `peer_id`: the libp2p peer ID (e.g. `12D3KooW...`) ### Creating and importing keys You can **create** a new Ed25519 keypair on Pinner, or **import** an existing private key (base64-encoded). Importing lets you reuse a key that was generated elsewhere, for example, migrating from another IPFS node. Pinner also supports **exporting** a key's private key so you can back it up or use it on another node. ### Publishing Publishing creates a signed IPNS record that associates an IPNS name with a CID. You specify the key ID and the target CID. You can optionally set a TTL (time-to-live) for the record. Each publish increments the IPNS sequence number and sets a validity period. The IPNS name now resolves to that CID. Publish again with a different CID; same IPNS name, different CID. The domain follows automatically. ### Resolving Anyone can resolve an IPNS name to find the current CID. Resolution returns the CID the IPNS name currently points to, along with the record's sequence number, expiration time, and whether the record has expired. ### Republishing IPNS records have a validity period (end-of-life). Republishing re-signs and re-broadcasts the record to extend its lifetime. Pinner also runs a **background republisher** that automatically republishes all keys with a known published CID on a configurable interval (default: every hour). You rarely need to republish manually. ## Auto-created IPNS keys for websites When you create a website with `target_type=ipns` and provide a CID as the `target_hash`, Pinner automatically: 1. Creates an IPNS key named `{domain}-auto` 2. Publishes the CID to that key 3. Links the key to the website This means you can point a website at an IPNS name without manually creating and publishing a key first. If a key with that name already exists, Pinner reuses it and publishes the new CID. :::tip The auto-created key is linked to the website via `ipns_key_id`. Pinner uses this link to resolve the website's **active CID**, the currently-published content, separately from the IPNS target hash. ::: ## IPNS vs direct IPFS for websites | | Direct IPFS | IPNS | |---|-----------|------| | **Update method** | Change website `target_hash` | Republish IPNS name | | **Domain config changes** | Yes, every update | No, set once | | **Resolution speed** | One hop (CID direct) | Two hops (IPNS → CID) | | **Best for** | Rarely updated sites | Frequently updated sites, CI/CD | The resolution overhead of IPNS is small, typically under a second on Pinner's infrastructure. For most sites, it's not noticeable. ## IPNS key lifecycle ``` Create key → Publish CID → (update) → Republish CID → ... → Delete key ``` * **Create**: Generate an Ed25519 keypair on Pinner, or import an existing key * **Publish**: Sign an IPNS record associating the key's name with a CID * **Republish**: Re-sign the record to extend its validity (automatic via background republisher) * **Delete**: Soft-delete the key :::warning Pinner prevents you from deleting an IPNS key that is referenced by an active website. You must update or remove the website first. If you manage to delete a key that a domain depends on, that domain will stop resolving. ::: # SSL Pinner provisions and renews SSL certificates for your custom domains automatically, using Caddy's On-Demand TLS with Let's Encrypt. You don't need to buy certificates, configure web servers, or set up renewal cron jobs. ## How it works 1. You add a CNAME or A record pointing your domain at Pinner's infrastructure 2. When the first HTTPS request arrives, Caddy checks if the domain is authorized by querying the gateway's `/allowed` endpoint 3. The gateway validates the domain: it checks the DNSLink record and confirms the website is active in the portal 4. If the domain passes validation, Caddy requests a certificate from Let's Encrypt via the HTTP-01 ACME challenge 5. Caddy completes the ACME challenge (served on port 80) and obtains the certificate 6. Caddy calls back to the portal's internal webhook to update the SSL status to `ready` 7. Your site is served over HTTPS You don't touch any of this. The entire process runs in the background. ### Prerequisites For SSL provisioning to work: * **Port 80** must be accessible from the internet (required for the HTTP-01 ACME challenge) * **Port 443** must be accessible for HTTPS traffic * Your domain's DNSLink record (`_dnslink.yourdomain.com`) must be properly configured * Your website must be in `active` status in the portal ## SSL states SSL moves through these states: ``` pending → issuing → ready ↘ failed ``` | State | What's happening | |-------|-----------------| | `pending` | Waiting to start: the domain hasn't been validated or no HTTPS request has triggered certificate issuance yet | | `issuing` | Caddy is obtaining the certificate from Let's Encrypt via the ACME protocol | | `ready` | Certificate issued and serving HTTPS traffic | | `failed` | Provisioning failed; the `error` field on the SSL status has details | There is no separate `valid` or `error` state. A `failed` state includes an error message explaining what went wrong. ## Checking status You can check SSL status through the SDK. The `GetSSLStatus` method returns the current state, and the `WaitForSSLStatusReady` method polls until the certificate reaches `ready` or `failed`. ```go // Check current status resp, err := client.Websites().GetSSLStatus(ctx, "example.com") fmt.Println("SSL status:", resp.Ssl.Status) // Wait until settled (ready or failed) status, err := client.Websites().WaitForSSLStatusReady(ctx, "example.com") ``` The response also includes `issued_at` and `last_updated_at` timestamps when available. ## Renewal Caddy automatically renews certificates before they expire (typically 30 days before). You don't need to do anything; renewal happens automatically in the background. The portal tracks the renewal via the same Caddy webhook callback, updating `last_updated_at`. If a previously valid certificate for a soft-deleted website is still within its validity period, recreating the website for the same domain will inherit the `ready` SSL status automatically. ## DNS validation token Before SSL can be provisioned, your website must be validated. When you create a website, Pinner generates a validation token and tells you to add a TXT record like: ``` pinner-verify. TXT "pinner-verify=" ``` The token has an expiry time. If it expires, Pinner generates a new one automatically when you re-validate. ## Troubleshooting ### SSL stays in `pending` No HTTPS request has triggered certificate issuance yet, or the domain hasn't been validated. Ensure: 1. Your website is in `active` status (run DNS validation first) 2. Your DNS is pointing at Pinner's infrastructure (CNAME or A record) 3. The DNSLink record exists: `dig TXT _dnslink.yourdomain.com` ### SSL stays in `issuing` The ACME challenge is in progress. Common causes for getting stuck: * **Port 80 is not accessible**: The HTTP-01 challenge requires port 80 to be reachable from the internet * **DNS hasn't propagated**: Let's Encrypt needs to resolve your domain to the correct IP * **The gateway's `/allowed` endpoint rejected the domain**: Check that the DNSLink record and website status are valid You can test the `/allowed` endpoint directly: ```bash curl "http://localhost:8080/allowed?domain=yourdomain.com" ``` A `200` response means the domain is approved for certificate issuance. ### SSL fails with `failed` Check the `error` field on the SSL status response. Common causes: * **Port 80 not accessible from the internet**: Required for the HTTP-01 ACME challenge; check firewall rules * **DNS not pointing at Pinner**: The CNAME or A record is missing or incorrect * **Website is not active**: The gateway's `/allowed` check returns 400 when the website status isn't `active` * **DNSLink record missing or incorrect**: The gateway validates the `_dnslink` TXT record before allowing certificate issuance * **Let's Encrypt rate limits**: Too many failed issuance attempts; wait and retry (see [Let's Encrypt rate limits](https://letsencrypt.org/docs/rate-limits/)) ### SSL works but the site isn't loading SSL is only one piece. Check that: 1. Your website's target hash points at a valid, pinned CID 2. The content is actually accessible on IPFS 3. DNS is correctly pointing at Pinner's infrastructure # DNS and Domains DNS is the bridge between your domain name and Pinner's hosting. You configure DNS records at your registrar (or let Pinner manage DNS entirely), and Pinner's gateway serves your IPFS content when visitors hit your domain. ## Two ways to handle DNS ### Pinner-managed DNS (recommended) The fastest way to get your site live. Enable Pinner DNS and delegate your nameservers; Pinner creates all the records automatically (validation, dnslink, gateway routing). No manual record management needed. After creating or updating a website with `--dns-hosting`, the CLI shows the nameservers for delegation automatically. You can also check them at any time: ```bash pinner websites config ``` The nameservers are also available from the website configuration API endpoint. The `nameservers` field lists the nameservers to set at your registrar. The tradeoff: Pinner handles all DNS for that domain. If you need fine-grained control over other DNS records, you can still manage them through Pinner's DNS zone editor (see [Manage DNS Zones](/dns/how-to/manage-zones) and [Manage DNS Records](/dns/how-to/manage-records)). ### Bring your own DNS You keep your existing DNS provider and add records that point to Pinner: Record | Purpose | Value | \--------|---------|-------| TXT | Domain validation (ownership) | `validation_token` from website response (e.g. `pinner-verify=abc123def456`) | TXT | dnslink (content mapping) | `dnslink=/ipfs/` or `dnslink=/ipns/` | CNAME | Route subdomain traffic to Pinner | `gateway_domain` from website response | ALIAS | Route root domain traffic to Pinner | `gateway_domain` from website response | A | Root domain routing (if ALIAS not supported) | IP from Pinner configuration | This is useful if you have existing DNS infrastructure or need to manage records that Pinner doesn't support. After creating a website, the CLI shows the required DNS records. You can also view them with: ```bash pinner websites get ``` ## DNS records for website hosting ### Validation TXT record Before Pinner serves your domain, you must prove you own it. Pinner assigns a validation token to each website. The website response provides two fields: * `validation_token`: the full TXT record value (e.g. `pinner-verify=abc123def456`) * `validation_record_host`: the hostname for the TXT record (e.g. `pinner-verify.yourdomain.com`) Add a TXT record: ``` TXT "" ``` After adding the record, trigger validation and Pinner checks that the token matches. The token has an expiration time; if it expires, Pinner generates a new one automatically on your next validation attempt. Validate from the CLI: ```bash pinner websites validate ``` ### dnslink TXT record Pinner checks a `_dnslink` TXT record that maps your domain to your content. For IPFS targets: ``` _dnslink.yourdomain.com TXT "dnslink=/ipfs/" ``` For IPNS targets: ``` _dnslink.yourdomain.com TXT "dnslink=/ipns/" ``` ### CNAME record (subdomain) For `www.yourdomain.com` or other subdomains, use the `gateway_domain` from the website response: ``` www CNAME ``` ### ALIAS record (root domain) Root domains can use an ALIAS record (supported by most modern DNS providers) pointing to the gateway: ``` @ ALIAS ``` If your DNS provider doesn't support ALIAS records, use an A record instead: ``` @ A ``` ### Nameserver delegation (Pinner-managed DNS) If you're using Pinner's DNS hosting, update your domain registrar to use Pinner's nameservers: ```bash pinner websites config ``` Or retrieve them from the website configuration API endpoint. Once propagated, Pinner controls all DNS for your domain. ## Supported DNS record types When managing DNS zones through Pinner, you can create the following user-manageable record types: Type | Description | \------|-------------| A | IPv4 address record | AAAA | IPv6 address record | ALIAS | Alias record (maps root domain to another name) | CNAME | Canonical name record | MX | Mail exchange record | NS | Nameserver record | SRV | Service record | TXT | Text record | SOA and NS records are managed automatically by PowerDNS and cannot be directly modified by users. ## DNS propagation DNS changes aren't instant. Typical propagation times: Change | Typical time | \--------|-------------| New records | 1–5 minutes | Modified records | 5–30 minutes | Nameserver changes | 1–48 hours | Pinner's SSL provisioning starts only after DNS has propagated. If SSL stays in `pending`, your DNS records may not have propagated yet. ## DNS hosting setup flow 1. Enable DNS hosting on your website: `pinner websites create --cid --dns-hosting` (or `pinner websites update --dns-hosting`) 2. Pinner creates a DNS zone with the necessary records (dnslink TXT, validation TXT, and ALIAS pointing to the gateway) 3. Get Pinner's nameservers: `pinner websites config` 4. Change your domain's nameservers at your registrar to Pinner's nameservers 5. Wait for nameserver propagation 6. Validate: `pinner dns zones validate ` 7. SSL provisions automatically See [Manage DNS Zones](/dns/how-to/manage-zones) and [Manage DNS Records](/dns/how-to/manage-records) for CLI commands. > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. A minimal, developer-focused CLI tool for pinning content to IPFS via the Pinner.xyz service. ## Install See [Getting Started with the CLI](/ipfs/cli/getting-started) to install the Pinner CLI. ## Authentication Most commands require authentication. Run `pinner setup` for the interactive wizard, or set `PINNER_AUTH_TOKEN` in your environment. ## Global Flags | Flag | Alias | Description | |------|-------|-------------| | `--json` | | Output JSON instead of human-readable | | `--verbose` | | Show detailed output | | `--quiet` | | Suppress non-error output | | `--unmask` | | Show sensitive data (tokens, passwords, secrets) unmasked | | `--auth-token` | | Auth token to override config | | `--secure` | | Use HTTPS for API connections | ## Environment Variables | Variable | Description | |----------|-------------| | `PINNER_AUTH_TOKEN` | Auth token to override config | | `PINNER_SECURE` | Use HTTPS for API connections | | `PINNER_EMAIL` | Email address for login | | `PINNER_PASSWORD` | Password for login (WARNING: insecure, prefer stdin or env var) | | `PINNER_OTP` | OTP code for 2FA (6 digits) | | `PINNER_MEMORY_LIMIT` | Memory limit for CAR generation in megabytes | ## Command Summary ### [Setup](/reference/cli/setup) | Command | Description | |---------|-------------| | [`setup`](/reference/cli/setup#setup) | Run the interactive setup wizard to configure your Pinner.xyz CLI environment. This wizard will guide you through authentication and configuration. | | [`auth`](/reference/cli/setup#auth) | Authenticate with the Pinner.xyz service. | | [`register`](/reference/cli/setup#register) | Register a new user account on Pinner.xyz. | | [`confirm-email`](/reference/cli/setup#confirm-email) | Confirm your email address using the verification token sent to your email. | | [`account`](/reference/cli/setup#account) | Manage your Pinner.xyz account settings including 2FA configuration. | ### [Content](/reference/cli/content) | Command | Description | |---------|-------------| | [`upload`](/reference/cli/content#upload) | Upload files or directories to IPFS via the Pinner.xyz service. Content is converted to CAR format before uploading. | | [`download`](/reference/cli/content#download) | Download content from IPFS by CID and save it to the local filesystem. Supports CID paths (e.g., CID/path/to/file) to download a specific file from a directory. | | [`cat`](/reference/cli/content#cat) | Stream the contents of an IPFS CID to stdout. Supports CID paths (e.g., CID/path/to/file) to cat a specific file from a directory. | | [`ls`](/reference/cli/content#ls) | List the contents of a directory pinned on IPFS by CID. Shows file names, types, and sizes for each entry. Supports CID paths (e.g., CID/path/to/dir) to list nested directories. | ### [Pinning](/reference/cli/pinning) | Command | Description | |---------|-------------| | [`pin`](/reference/cli/pinning#pin) | Shortcut for 'pinner pins add'. Pin existing IPFS content by CID. Multiple CIDs can be provided as arguments, read from a file using --file, or piped from stdin. | | [`pins`](/reference/cli/pinning#pins) | Manage your pinned IPFS content with subcommands for adding, removing, listing, checking status, and updating pin metadata. | | [`list`](/reference/cli/pinning#list) | Shortcut for 'pinner pins ls'. List your pinned content with optional filtering. | | [`status`](/reference/cli/pinning#status) | Shortcut for 'pinner pins status'. Check whether a pin has completed. If the pin is not found, account operations are checked as a fallback. | | [`unpin`](/reference/cli/pinning#unpin) | Shortcut for 'pinner pins rm'. Remove pins by CID or remove all pins. | | [`metadata`](/reference/cli/pinning#metadata) | The 'metadata' command has been removed. Use 'pinner pins update' to update pin metadata instead | ### [Management](/reference/cli/websites) | Command | Description | |---------|-------------| | [`point`](/reference/cli/websites#point) | Point an onchain/decentralized domain at IPFS content via IPNS. | | [`unpoint`](/reference/cli/websites#unpoint) | Remove the IPNS key for an onchain/decentralized domain. The domain will no longer resolve to IPFS content. | | [`operations`](/reference/cli/websites#operations) | View and monitor account operations such as uploads, pins, and other processing tasks. | | [`dns`](/reference/cli/websites#dns) | Manage DNS zones and records for your domains. DNS hosting allows you to control DNS configuration for IPFS-hosted websites. | | [`ipns`](/reference/cli/websites#ipns) | IPNS provides a mutable address scheme for IPFS content, allowing you to publish content under a stable name that you can update to point to new CIDs. | | [`websites`](/reference/cli/websites#websites) | Manage websites for your IPFS content. Websites allow you to associate domain names with CIDs, making your content accessible through custom domains. | ### [System](/reference/cli/system) | Command | Description | |---------|-------------| | [`config`](/reference/cli/system#config) | View or modify CLI configuration settings. | | [`doctor`](/reference/cli/system#doctor) | Show diagnostic information about your pinner CLI environment, including version, OS details, configuration location, and limits. | | [`bench`](/reference/cli/system#bench) | Run a benchmark by uploading random data (or a specified path) and tracking each stage of the pipeline: generate, upload, queued, pinning, pinned. | | [`generate-docs`](/reference/cli/system#generate-docs) | Generate CLI documentation in markdown, man, or JSON format | ### [Admin](/reference/cli/admin) | Command | Description | |---------|-------------| | [`admin`](/reference/cli/admin#admin) | Administrative operations for quota management, billing, and profiling. | > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. ## `setup` ```bash pinner setup [flags] ``` Run the interactive setup wizard to configure your Pinner.xyz CLI environment. This wizard will guide you through authentication and configuration. If you've already run setup, you can skip steps or reconfigure. ```bash pinner setup pinner setup --skip-auth pinner setup --skip-config pinner setup --reset ``` | Flag | Alias | Description | |------|-------|-------------| | `--skip-auth` | | Skip authentication step | | `--skip-config` | | Skip configuration step | | `--reset` | | Reset configuration and start fresh | | `--non-interactive` | | Run in non-interactive mode (skip wizard) | ## `auth` ```bash pinner auth [token] [flags] ``` Authenticate with the Pinner.xyz service. **Ways to authenticate:** **1. Provide an auth token directly:** pinner auth `` **2. Interactive login:** pinner auth (prompts for all inputs) **3. Semi-interactive:** pinner auth --email user@example.com (prompts for password and OTP if needed) **4. Non-interactive:** PINNER\_EMAIL=x PINNER\_PASSWORD=\*\*\* pinner auth **5. Non-interactive with 2FA:** PINNER\_EMAIL=x PINNER\_PASSWORD=\*\*\* PINNER\_OTP=123456 pinner auth **6. Secure non-interactive:** echo "pass" | pinner auth --email user@example.com ```bash pinner auth eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... pinner auth --email user@example.com pinner auth --email user@example.com --password mypass --otp-code 123456 PINNER_EMAIL=user@example.com PINNER_PASSWORD=mypass pinner auth echo "mypass" | pinner auth --email user@example.com --key-name "my-laptop" ``` | Flag | Alias | Description | |------|-------|-------------| | `--email` | | Email address for login \[env: `PINNER_EMAIL`] | | `--password` | | Password for login (WARNING: insecure, prefer stdin or env var) \[env: `PINNER_PASSWORD`] | | `--otp-code` | | OTP code for 2FA (6 digits) \[env: `PINNER_OTP`] | | `--key-name` | | Custom name for created API key | | `--no-create-key` | | Skip API key creation, save token directly | | `--force` | | Overwrite existing auth token without confirmation | **Subcommands:** | Subcommand | Description | |------------|-------------| | `status` | Check if you are currently authenticated with Pinner.xyz. | ### `status` ```bash pinner auth status ``` Check if you are currently authenticated with Pinner.xyz. This command verifies that your stored auth token is valid by making a request to the Pinner.xyz API. ```bash pinner auth status pinner auth status --json pinner auth status --verbose ``` ## `register` ```bash pinner register [flags] ``` Register a new user account on Pinner.xyz. After registration, you will need to confirm your email address using the 'confirm-email' command before you can authenticate. ```bash # Interactive mode (prompts for all required fields) pinner register # Non-interactive with flags pinner register --email user@example.com --first-name John --last-name Doe # Mix: provide email, prompt for other fields pinner register --email user@example.com ``` | Flag | Alias | Description | |------|-------|-------------| | `--email` | | Email address | | `--first-name` | | First name | | `--last-name` | | Last name | | `--password` | | Password (if not provided, you will be prompted) | ## `confirm-email` ```bash pinner confirm-email [flags] ``` Confirm your email address using the verification token sent to your email. After registering with 'pinner register', you will receive an email with a verification token. Use this command to confirm your email address. ```bash pinner confirm-email --email user@example.com --token abc123def456 pinner confirm-email -e user@example.com -t abc123def456 After confirmation, authenticate with: pinner auth --email user@example.com ``` | Flag | Alias | Description | |------|-------|-------------| | `--email` | | Email address **(required)** | | `--token` | | Verification token from email **(required)** | ## `account` ```bash pinner account ``` Manage your Pinner.xyz account settings including 2FA configuration. ```bash pinner account otp enable pinner account otp enable --otp 123456 pinner account otp disable pinner account otp disable --password mypassword ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `otp` | Enable or disable two-factor authentication (2FA) for your account. | | `api-keys` | Manage API keys for your Pinner.xyz account. | ### `otp` ```bash pinner account otp ``` Enable or disable two-factor authentication (2FA) for your account. When enabling 2FA, you will receive a secret key that must be added to your authenticator app (e.g., Google Authenticator, Authy). You will then need to verify the setup with a code from your app **Subcommands:** | Subcommand | Description | |------------|-------------| | `enable` | Enable 2FA for your account. This will: | | `disable` | Disable 2FA for your account. This will: | #### `enable` ```bash pinner account otp enable [flags] ``` **Enable 2FA for your account. This will:** 1. Generate a new OTP secret 2. Display a QR code/secret key for your authenticator app 3. Prompt you to verify the setup with a code from your app After successful verification, 2FA will be required for all future logins | Flag | Alias | Description | |------|-------|-------------| | `--otp` | | OTP code to verify setup (for non-interactive mode) | #### `disable` ```bash pinner account otp disable [flags] ``` **Disable 2FA for your account. This will:** 1. Prompt for your password for verification 2. Remove 2FA requirement from your account **WARNING:** This reduces your account security. Consider re-enabling 2FA | Flag | Alias | Description | |------|-------|-------------| | `--password` | | Password for verification (WARNING: insecure, prefer stdin or prompt) \[env: `PINNER_PASSWORD`] | ### `api-keys` ```bash pinner account api-keys ``` Manage API keys for your Pinner.xyz account. API keys are used to authenticate with the Pinner.xyz service without exposing your login credentials. Each key has a unique token that can be used with --auth-token or the PINNER\_AUTH\_TOKEN environment variable. ```bash pinner account api-keys list pinner account api-keys list --search my-key pinner account api-keys create my-key pinner account api-keys delete my-key pinner account api-keys delete 00000000-0000-0000-0000-000000000001 ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all API keys for your account. | | `create` | Create a new API key with the given name. | | `delete` | Delete an API key by its UUID or name. | #### `list` ```bash pinner account api-keys list [flags] ``` List all API keys for your account. Use --search to filter keys by name | Flag | Alias | Description | |------|-------|-------------| | `--search` | | Search API keys by name | #### `create` ```bash pinner account api-keys create ``` Create a new API key with the given name. The API key token will be displayed once. Save it securely — it cannot be retrieved later. **This key can be used with:** pinner auth --auth-token `` PINNER\_AUTH\_TOKEN=`` pinner `` #### `delete` ```bash pinner account api-keys delete [flags] ``` Delete an API key by its UUID or name. If the key being deleted is the one currently used for authentication, the command will be blocked unless --force is used. After deleting your current key, you must re-authenticate with 'pinner auth' | Flag | Alias | Description | |------|-------|-------------| | `--force` | | Force deletion of the API key currently used for authentication | > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. ## `upload` ```bash pinner upload [path] [flags] ``` Upload files or directories to IPFS via the Pinner.xyz service. Content is converted to CAR format before uploading. ```bash pinner upload myfile.txt pinner upload myfile.txt --name "my document" pinner upload myfile.txt --no-wait pinner upload /path/to/directory --name "project files" pinner upload largefile.zip --memory-limit 500 pinner upload myfile.txt --dry-run # Upload from stdin (pipe) cat myfile.txt | pinner upload --name "my file" echo "hello world" | pinner upload --name "greeting" curl -s https://example.com/data | pinner upload --name "downloaded" The output includes: ``` * 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 | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Custom name for the pin | | `--no-wait` | | Return immediately without waiting for pinning to complete | | `--memory-limit` | | Memory limit for CAR generation in megabytes \[env: `PINNER_MEMORY_LIMIT`] | | `--dry-run` | | Preview operations without making any changes | | `--chunk-size` | | Chunk size in bytes for UnixFS file splitting (default: 1048576) | | `--chunker` | | DAG layout strategy: balanced (default) or trickle | | `--max-links` | | Maximum number of links per DAG node (default: 174) | | `--meta` | | Set metadata as key=value (repeatable) | ## `download` ```bash pinner download [/] [flags] ``` Download content from IPFS by CID and save it to the local filesystem. Supports CID paths (e.g., CID/path/to/file) to download a specific file from a directory. For streaming content to stdout, use 'pinner cat' instead. For listing directory contents, use 'pinner ls' instead. ```bash pinner download bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner download bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e/subdir/file.txt pinner download bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e -o myfile.txt pinner download bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e -o /path/to/dir/ pinner download bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e -o existing.txt --force pinner download bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --dry-run The output includes: ``` * CID: Content identifier of the downloaded file * Path: Local file path where content was saved * Size: File size in human-readable format * Time: Download duration | Flag | Alias | Description | |------|-------|-------------| | `--output` | | Output file or directory path (defaults to CID as filename in current directory) | | `--force` | | Force operation without confirmation | | `--dry-run` | | Preview operations without making any changes | ## `cat` ```bash pinner cat [/] ``` Stream the contents of an IPFS CID to stdout. Supports CID paths (e.g., CID/path/to/file) to cat a specific file from a directory. ```bash pinner cat bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner cat bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e/path/to/file.txt pinner cat bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e > output.txt pinner cat bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e | jq . pinner cat bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e | gzip > data.gz Note: This command outputs raw content to stdout. Use --verbose or redirect stderr for progress info ``` ## `ls` ```bash pinner ls [/] [flags] ``` List the contents of a directory pinned on IPFS by CID. Shows file names, types, and sizes for each entry. Supports CID paths (e.g., CID/path/to/dir) to list nested directories. If the path targets a file, shows file metadata instead of erroring. ```bash pinner ls bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner ls bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --json pinner ls bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --limit 5 pinner ls bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e/subdir ``` | Flag | Alias | Description | |------|-------|-------------| | `--limit` | | Maximum number of results to return | > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. ## `pin` ```bash pinner pin [flags] ``` Shortcut for 'pinner pins add'. Pin existing IPFS content by CID. Multiple CIDs can be provided as arguments, read from a file using --file, or piped from stdin. ```bash pinner pin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner pin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --name "my file" pinner pin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --no-wait pinner pin bafybeig...abc bafybeig...def bafybeig...ghi --parallel 5 pinner pin --file cids.txt echo "bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e" | pinner pin pinner pin --file cids.txt --continue --parallel 10 pinner pin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --dry-run ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Custom name for the pin | | `--no-wait` | | Return immediately without waiting 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 operations without making any changes | ## `pins` ```bash pinner pins ``` Manage your pinned IPFS content with subcommands for adding, removing, listing, checking status, and updating pin metadata. ```bash pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --name "my file" pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --meta owner=alice --meta env=prod pinner pins rm bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --force pinner pins ls --status pinned pinner pins status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner pins update bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --meta owner=bob ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `add` | Pin content that is already on IPFS by providing its CID. Optionally set metadata key-value pairs at pin time using --meta. | | `rm` | Remove a pin by its CID, or remove all pins with --all. | | `ls` | List your pinned content with optional filtering. | | `status` | Check whether a pin has completed. If the pin is not found, account operations are checked as a fallback. | | `update` | Update name and/or metadata for a pin. Metadata is a set of key-value pairs (e.g., owner=alice, env=prod) that help you organize and filter pins. | ### `add` ```bash pinner pins add [flags] ``` Pin content that is already on IPFS by providing its CID. Optionally set metadata key-value pairs at pin time using --meta. ```bash pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --name "my file" pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --no-wait pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --meta owner=alice --meta env=prod pinner pins add bafybeig...abc bafybeig...def bafybeig...ghi --parallel 5 pinner pins add --file cids.txt pinner pins add bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --dry-run ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Custom name for the pin | | `--no-wait` | | Return immediately without waiting 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 operations without making any changes | | `--meta` | | Set metadata as key=value (repeatable) | ### `rm` ```bash pinner pins rm [flags] ``` Remove a pin by its CID, or remove all pins with --all. ```bash pinner pins rm bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner pins rm bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --force pinner pins rm bafybeig...abc bafybeig...def bafybeig...ghi --force pinner pins rm --file cids.txt --force pinner pins rm bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --dry-run pinner pins rm --all --force pinner pins rm --all --status failed --force ``` | Flag | Alias | Description | |------|-------|-------------| | `--force` | | Force operation without confirmation | | `--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 operations without making any changes | | `--status` | | Filter by pin status (queued, pinning, pinned, failed) | | `--all` | | Remove all pins (requires --force) | ### `ls` ```bash pinner pins ls [flags] ``` List your pinned content with optional filtering. ```bash pinner pins ls pinner pins ls --name "my-project" pinner pins ls --status pinned pinner pins ls --limit 20 pinner pins ls --watch ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Filter by name | | `--limit` | | Maximum number of results to return | | `--status` | | Filter by pin status (queued, pinning, pinned, failed) | | `--watch` | | Continuously monitor and update pin status (useful for watching uploads) | ### `status` ```bash pinner pins status [flags] ``` Check whether a pin has completed. If the pin is not found, account operations are checked as a fallback. ```bash pinner pins status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner pins status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --watch pinner pins status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--watch` | | Poll until settled | ### `update` ```bash pinner pins update [flags] ``` Update name and/or metadata for a pin. Metadata is a set of key-value pairs (e.g., owner=alice, env=prod) that help you organize and filter pins. ```bash pinner pins update bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --name "renamed" pinner pins update bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --meta owner=alice --meta env=prod pinner pins update bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --clear-meta pinner pins update bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --clear-meta --meta fresh=start pinner pins update bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --name "renamed" --meta env=prod ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Rename the pin | | `--meta` | | Set metadata as key=value (repeatable) | | `--clear-meta` | | Clear all metadata | | `--dry-run` | | Preview operations without making any changes | ## `list` ```bash pinner list [flags] ``` Shortcut for 'pinner pins ls'. List your pinned content with optional filtering. ```bash pinner list pinner list --name "my-project" pinner list --status pinned pinner list --limit 20 pinner list --watch pinner list --name backup --status failed --limit 50 echo "backup" | pinner list ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Filter by name | | `--limit` | | Maximum number of results to return | | `--status` | | Filter by pin status (queued, pinning, pinned, failed) | | `--watch` | | Continuously monitor and update pin status (useful for watching uploads) | ## `status` ```bash pinner status [flags] ``` Shortcut for 'pinner pins status'. Check whether a pin has completed. If the pin is not found, account operations are checked as a fallback. ```bash pinner status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --watch pinner status bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --json echo "bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e" | pinner status cat cids.txt | pinner status Status values: queued ``` * Pin is queued for processing pinning * Pin is being processed pinned * Pin is successfully pinned failed * Pin failed to pin **Operation status values (shown when pin is not found):** pending * Operation is queued running * Operation is in progress completed - Operation finished successfully failed * Operation failed error * Operation encountered an error | Flag | Alias | Description | |------|-------|-------------| | `--watch` | | Poll until settled | ## `unpin` ```bash pinner unpin [flags] ``` Shortcut for 'pinner pins rm'. Remove pins by CID or remove all pins. ```bash pinner unpin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner unpin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --force pinner unpin bafybeig...abc bafybeig...def bafybeig...ghi --force pinner unpin --file cids.txt --force pinner unpin --file cids.txt --force --parallel 5 --continue pinner unpin bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --dry-run pinner unpin all --force pinner unpin all --force --status failed --dry-run ``` | Flag | Alias | Description | |------|-------|-------------| | `--force` | | Force operation without confirmation | | `--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 operations without making any changes | **Subcommands:** | Subcommand | Description | |------------|-------------| | `all` | Remove all pinned content. This is a destructive operation with safety guards. | ### `all` ```bash pinner unpin all [flags] ``` Remove all pinned content. This is a destructive operation with safety guards. **This command requires two explicit confirmations:** 1. The --force flag to acknowledge the destructive nature 2. An interactive prompt requiring you to type the exact number of pins For non-interactive use (scripts, CI), use --yes to accept the safety prompt. --force is always required regardless of --yes. ```bash pinner unpin all --force pinner unpin all --force --status failed pinner unpin all --force --parallel 5 --continue pinner unpin all --force --dry-run pinner unpin all --force --yes pinner unpin all --force --status queued --dry-run ``` | Flag | Alias | Description | |------|-------|-------------| | `--force` | | Force operation without confirmation | | `--status` | | Filter by pin status (queued, pinning, pinned, failed) | | `--parallel` | | Number of parallel operations (default: 1) | | `--continue` | | Continue processing even if some operations fail | | `--dry-run` | | Preview operations without making any changes | ## `metadata` ```bash pinner metadata ``` The 'metadata' command has been removed. Use 'pinner pins update' to update pin metadata instead > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. ## `point` ```bash pinner point [flags] ``` Point an onchain/decentralized domain at IPFS content via IPNS. The command is idempotent. If an IPNS key for this domain already exists, it reuses the key and republishes the new CID. ```bash pinner point vitalik.eth --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner point vitalik.eth --cid bafybeig...updated ``` | Flag | Alias | Description | |------|-------|-------------| | `--cid` | | CID to point the name at **(required)** | ## `unpoint` ```bash pinner unpoint ``` Remove the IPNS key for an onchain/decentralized domain. The domain will no longer resolve to IPFS content. ```bash pinner unpoint vitalik.eth ``` ## `operations` ```bash pinner operations ``` View and monitor account operations such as uploads, pins, and other processing tasks. **Operations track server-side processing of your requests. Each operation has a status:** - pending * Operation is queued - running * Operation is in progress - completed - Operation finished successfully - failed * Operation failed - error * Operation encountered an error ```bash pinner operations list pinner operations list --status running pinner operations list --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --limit 20 pinner operations get 42 pinner operations get 42 --watch ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List your account operations with optional filtering. Operations track server-side processing of your requests (uploads, pins, etc.). | | `get` | Get detailed information about a specific operation. | ### `list` ```bash pinner operations list [flags] ``` List your account operations with optional filtering. Operations track server-side processing of your requests (uploads, pins, etc.). ```bash pinner operations list pinner operations list --status running pinner operations list --operation upload pinner operations list --protocol ipfs pinner operations list --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner operations list --limit 20 pinner operations list --watch ``` | Flag | Alias | Description | |------|-------|-------------| | `--status` | | Filter by status (pending, running, completed, failed, error) | | `--operation` | | Filter by operation type (e.g., upload, pin) | | `--protocol` | | Filter by protocol (e.g., ipfs) | | `--cid` | | Filter by CID | | `--limit` | | Maximum number of results to return | | `--watch` | | Continuously monitor and update pin status (useful for watching uploads) | ### `get` ```bash pinner operations get [flags] ``` Get detailed information about a specific operation. ```bash pinner operations get 42 pinner operations get 42 --watch ``` | Flag | Alias | Description | |------|-------|-------------| | `--watch` | | Wait for the operation to complete | ## `dns` ```bash pinner dns ``` Manage DNS zones and records for your domains. DNS hosting allows you to control DNS configuration for IPFS-hosted websites. **Zone operations:** * List all DNS zones * Create a new DNS zone * Get zone details * Delete a DNS zone **Record operations:** * List DNS records for a zone * Create DNS records * Get record details * Update DNS records * Delete DNS records ```bash pinner dns zones list pinner dns zones create --domain example.com pinner dns zones get example.com pinner dns zones delete example.com pinner dns records list example.com pinner dns records create example.com --name www --type CNAME --content example.com pinner dns records delete example.com --name www --type CNAME ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `zones` | Manage DNS zones for your domains. | | `records` | Manage DNS records for zones. | ### `zones` ```bash pinner dns zones ``` Manage DNS zones for your domains. ```bash pinner dns zones list pinner dns zones list --status active pinner dns zones create --domain example.com pinner dns zones create --domain example.com --nameservers ns1.example.com,ns2.example.com pinner dns zones get example.com pinner dns zones delete example.com ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all DNS zones for the authenticated user. | | `create` | Create a new DNS zone for a domain. | | `get` | Get details of a specific DNS zone. | | `delete` | Delete a DNS zone and all its records. | | `validate` | Validate that a DNS zone's nameservers are properly delegated. This checks that the domain's nameservers point to the expected Pinner.xyz nameservers. | #### `list` ```bash pinner dns zones list ``` List all DNS zones for the authenticated user. ```bash pinner dns zones list pinner dns zones list --json ``` #### `create` ```bash pinner dns zones create [flags] ``` Create a new DNS zone for a domain. ```bash pinner dns zones create --domain example.com pinner dns zones create --domain example.com --nameservers ns1.example.com,ns2.example.com pinner dns zones create --domain example.com --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--domain` | | Domain name for the website **(required)** | | `--nameservers` | | Comma-separated list of nameservers (e.g., ns1.example.com,ns2.example.com) | #### `get` ```bash pinner dns zones get ``` Get details of a specific DNS zone. ```bash pinner dns zones get example.com pinner dns zones get example.com --json ``` #### `delete` ```bash pinner dns zones delete ``` Delete a DNS zone and all its records. ```bash pinner dns zones delete example.com ``` #### `validate` ```bash pinner dns zones validate ``` Validate that a DNS zone's nameservers are properly delegated. This checks that the domain's nameservers point to the expected Pinner.xyz nameservers. ```bash pinner dns zones validate example.com pinner dns zones validate example.com --json ``` ### `records` ```bash pinner dns records ``` Manage DNS records for zones. ```bash pinner dns records list example.com pinner dns records create example.com --name www --type CNAME --content example.com pinner dns records get example.com --name www --type CNAME pinner dns records update example.com --name www --type CNAME --content new.example.com pinner dns records delete example.com --name www --type CNAME ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all DNS records for a specific zone. | | `create` | Create a DNS record in the specified zone. | | `get` | Get details of a specific DNS record. | | `update` | Update an existing DNS record. | | `delete` | Delete a DNS record from a zone. | #### `list` ```bash pinner dns records list ``` List all DNS records for a specific zone. ```bash pinner dns records list example.com pinner dns records list example.com --json ``` #### `create` ```bash pinner dns records create [flags] ``` Create a DNS record in the specified zone. ```bash pinner dns records create example.com --name www --type CNAME --content example.com pinner dns records create example.com --name _dnslink --type TXT --content "/ipfs/bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e" --ttl 3600 pinner dns records create example.com --name @ --type A --content 192.168.1.1 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | DNS record name | | `--type` | | DNS record type (A, AAAA, CNAME, TXT, MX, NS, etc.) | | `--content` | | DNS record content | | `--ttl` | | DNS record TTL in seconds (default: 3600) | | `--disabled` | | Disable the DNS record | #### `get` ```bash pinner dns records get [flags] ``` Get details of a specific DNS record. ```bash pinner dns records get example.com --name www --type CNAME pinner dns records get example.com --name www --type CNAME --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | DNS record name **(required)** | | `--type` | | DNS record type (A, AAAA, CNAME, TXT, MX, NS, etc.) **(required)** | #### `update` ```bash pinner dns records update [flags] ``` Update an existing DNS record. ```bash pinner dns records update example.com --name www --type CNAME --content new.example.com pinner dns records update example.com --name www --type CNAME --content new.example.com --ttl 7200 pinner dns records update example.com --name www --type CNAME --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | DNS record name **(required)** | | `--type` | | DNS record type (A, AAAA, CNAME, TXT, MX, NS, etc.) **(required)** | | `--content` | | DNS record content | | `--ttl` | | DNS record TTL in seconds (default: 3600) | | `--disabled` | | Disable the DNS record | #### `delete` ```bash pinner dns records delete [flags] ``` Delete a DNS record from a zone. ```bash pinner dns records delete example.com --name www --type CNAME ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | DNS record name **(required)** | | `--type` | | DNS record type (A, AAAA, CNAME, TXT, MX, NS, etc.) **(required)** | ## `ipns` ```bash pinner ipns ``` IPNS provides a mutable address scheme for IPFS content, allowing you to publish content under a stable name that you can update to point to new CIDs. **IPNS operations include:** * Managing IPNS keys (create, list, get, delete) * Publishing CIDs to IPNS names * Republishing IPNS records * Resolving IPNS names to their target CIDs **Key names and numeric IDs are interchangeable. For example:** **pinner ipns keys get my-key is equivalent to:** pinner ipns keys get 1 ```bash pinner ipns keys list pinner ipns keys create my-key pinner ipns keys get my-key pinner ipns keys delete my-key pinner ipns publish bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --key-name my-key pinner ipns republish my-key pinner ipns resolve k51qzi5uqu5djx ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `keys` | Manage your IPNS keys. Keys are used to publish content under stable IPNS names that you can update to point to new CIDs. | | `publish` | Publish a CID to an IPNS key, making it resolvable via the IPNS name. After publishing, the IPNS name will resolve to the specified CID. | | `republish` | Republish the IPNS record for a specific key. This refreshes the record on the network, extending its validity. | | `resolve` | Resolve an IPNS name to its target CID. This shows which content the IPNS name currently points to. | ### `keys` ```bash pinner ipns keys ``` Manage your IPNS keys. Keys are used to publish content under stable IPNS names that you can update to point to new CIDs. Key names and numeric IDs are interchangeable. ```bash pinner ipns keys list pinner ipns keys create my-key pinner ipns keys get my-key pinner ipns keys delete my-key ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all IPNS keys for the authenticated user. | | `create` | Create a new IPNS key with the given name. Use the key to publish content under a stable IPNS name. | | `get` | Get details of a specific IPNS key by its name or ID. | | `delete` | Delete an IPNS key by its name or ID. This operation is irreversible. | #### `list` ```bash pinner ipns keys list ``` List all IPNS keys for the authenticated user. ```bash pinner ipns keys list pinner ipns keys list --json ``` #### `create` ```bash pinner ipns keys create [flags] ``` Create a new IPNS key with the given name. Use the key to publish content under a stable IPNS name. ```bash pinner ipns keys create my-key pinner ipns keys create my-key --key `` pinner ipns keys create my-key --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--key` | | Private key to import (optional, generates a new key if not provided) | #### `get` ```bash pinner ipns keys get ``` Get details of a specific IPNS key by its name or ID. ```bash pinner ipns keys get my-key pinner ipns keys get 1 pinner ipns keys get my-key --json ``` #### `delete` ```bash pinner ipns keys delete ``` Delete an IPNS key by its name or ID. This operation is irreversible. ```bash pinner ipns keys delete my-key pinner ipns keys delete 1 ``` ### `publish` ```bash pinner ipns publish [flags] ``` Publish a CID to an IPNS key, making it resolvable via the IPNS name. After publishing, the IPNS name will resolve to the specified CID. Specify the key by name or numeric ID. ```bash pinner ipns publish bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --key-name my-key pinner ipns publish bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --key-name my-key --ttl 24h pinner ipns publish bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --key-name 1 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--key-name` | | Name or ID of the IPNS key to publish to **(required)** | | `--ttl` | | Time-to-live for the IPNS record (e.g., 24h, 7d) | | `--wait` | | Wait for the pinning operation to complete before returning | ### `republish` ```bash pinner ipns republish ``` Republish the IPNS record for a specific key. This refreshes the record on the network, extending its validity. Specify the key by name or numeric ID. ```bash pinner ipns republish my-key pinner ipns republish 1 pinner ipns republish my-key --json ``` ### `resolve` ```bash pinner ipns resolve ``` Resolve an IPNS name to its target CID. This shows which content the IPNS name currently points to. ```bash pinner ipns resolve k51qzi5uqu5djx... pinner ipns resolve k51qzi5uqu5djx... --json ``` ## `websites` ```bash pinner websites ``` Manage websites for your IPFS content. Websites allow you to associate domain names with CIDs, making your content accessible through custom domains. **Website operations include:** * List all websites * Create a new website * Get website details * Update website configuration * Delete a website * Validate website configuration ```bash pinner websites list pinner websites create example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner websites get example.com pinner websites update example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner websites delete example.com pinner websites validate example.com ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all websites for the authenticated user. | | `create` | Create a new website with the specified domain and target CID. | | `get` | Get details of a specific website by domain. | | `update` | Update an existing website with new CID, target type, or domain rename. | | `enable-ipns` | Convert a website from IPFS to IPNS targeting. | | `delete` | Delete a website by domain. This operation is irreversible. | | `validate` | Validate a website by domain to check if DNS is properly configured. | | `ssl` | View SSL certificate status for your websites. | | `config` | Show the website hosting configuration including the gateway domain. | | `wizard` | Launch an interactive wizard to create a new website step by step. | ### `list` ```bash pinner websites list ``` List all websites for the authenticated user. ```bash pinner websites list pinner websites list --json ``` ### `create` ```bash pinner websites create [flags] ``` Create a new website with the specified domain and target CID. ```bash pinner websites create example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner websites create example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --target-type ipfs pinner websites create example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --dns-hosting pinner websites create example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--cid` | | CID for the website **(required)** | | `--target-type` | | Target type (ipfs, ipns, etc.) | | `--dns-hosting` | | Enable DNS hosting for this website | | `--no-dns-hosting` | | Disable DNS hosting for this website | ### `get` ```bash pinner websites get ``` Get details of a specific website by domain. ```bash pinner websites get example.com pinner websites get example.com --json ``` ### `update` ```bash pinner websites update [flags] ``` Update an existing website with new CID, target type, or domain rename. At least one of the optional fields must be provided to update the website. When --target-type is set to "ipns" without --cid, the website will be converted from IPFS to IPNS targeting (an IPNS key is auto-created and the current CID is published to it). When --target-type is "ipns" and --cid is a regular IPFS CID (not a peer ID), an IPNS key is auto-created and that CID is published to it. ```bash pinner websites update example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --target-type ipfs pinner websites update example.com --target-type ipns pinner websites update example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e --target-type ipns pinner websites update example.com --dns-hosting pinner websites update example.com --no-dns-hosting pinner websites update example.com --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--domain` | | Domain name for the website | | `--cid` | | CID for the website | | `--target-type` | | Target type (ipfs, ipns, etc.) | | `--dns-hosting` | | Enable DNS hosting for this website | | `--no-dns-hosting` | | Disable DNS hosting for this website | ### `enable-ipns` ```bash pinner websites enable-ipns [flags] ``` Convert a website from IPFS to IPNS targeting. An IPNS key will be auto-created and the current CID will be published to it. This enables content-addressed updates without changing the domain's DNS records. If --cid is provided, the IPNS key will publish that CID instead of the current one. ```bash pinner websites enable-ipns example.com pinner websites enable-ipns example.com --cid bafybeigqaforwjgcx45jnh7dgyfgqqm2lei4hurrrnsizrpgyxz3egtd7e pinner websites enable-ipns example.com --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--cid` | | CID for the website | ### `delete` ```bash pinner websites delete ``` Delete a website by domain. This operation is irreversible. ```bash pinner websites delete example.com pinner websites delete example.com --json ``` ### `validate` ```bash pinner websites validate ``` Validate a website by domain to check if DNS is properly configured. ```bash pinner websites validate example.com pinner websites validate example.com --json ``` ### `ssl` ```bash pinner websites ssl ``` View SSL certificate status for your websites. **SSL operations include:** * Check SSL certificate status for a domain * Monitor SSL status changes in real-time ```bash pinner websites ssl status example.com pinner websites ssl status example.com --json pinner websites ssl status example.com --watch ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `status` | Get SSL certificate status for a website domain. | #### `status` ```bash pinner websites ssl status [flags] ``` Get SSL certificate status for a website domain. **This command retrieves the current SSL certificate status including:** * Certificate status (active, pending, error, etc.) * Certificate issuance date * Last update timestamp * Any error messages ```bash pinner websites ssl status example.com pinner websites ssl status example.com --json pinner websites ssl status example.com --watch ``` | Flag | Alias | Description | |------|-------|-------------| | `--watch` | | Watch for SSL status changes | ### `config` ```bash pinner websites config ``` Show the website hosting configuration including the gateway domain. Use this to find the gateway domain for setting up CNAME records with your DNS provider. ```bash pinner websites config pinner websites config --json ``` ### `wizard` ```bash pinner websites wizard ``` Launch an interactive wizard to create a new website step by step. **The wizard will guide you through:** 1. Authentication check 2. Content source (CID or upload) 3. Target type (IPFS or IPNS) 4. Domain name 5. DNS mode (Pinner-managed or self-managed) 6. Website creation 7. DNS setup 8. Validation ```bash pinner websites wizard ``` > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. ## `admin` ```bash pinner admin ``` Administrative operations for quota management, billing, and profiling. These commands require administrative privileges and are intended for system administrators. **Quota operations include:** * List, create, update, delete quota plans * Manage user quota allowances * View system statistics * Reconcile quotas and cleanup expired data * Manage user quota configurations **Billing operations include:** * Manage billing credits * View user balances * Manage price lines and pricing plans * Manage subscribers and subscriptions **Profiling operations include:** * Access Go runtime pprof profiles (heap, cpu, goroutine, etc.) * Configure block and mutex profiling rates * View profiling status ```bash pinner admin quota plans list pinner admin quota allowances list pinner admin billing credits list pinner admin billing subscribers list pinner admin pprof status pinner admin pprof heap > heap.prof ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `quota` | Manage quota plans, allowances, and user configurations. | | `billing` | Manage billing credits, price lines, pricing plans, and subscriptions. | | `websites` | Administrative operations for IPFS websites. | | `pprof` | Access Go runtime profiling data via pprof endpoints. | ### `quota` ```bash pinner admin quota ``` Manage quota plans, allowances, and user configurations. **Quota operations include:** * Plan management (list, create, update, delete) * Allowance management (list, create, update, delete) * User config management * System statistics and reconciliation ```bash pinner admin quota plans list pinner admin quota plans get `` pinner admin quota allowances list pinner admin quota stats ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `plans` | List, create, update, and delete quota plans. | | `allowances` | List, create, update, and delete quota allowances. | | `user-configs` | List and update user quota configurations. | | `stats` | View quota system statistics. | | `reconcile` | Reconcile quota data for all users or a specific user. | | `cleanup` | Cleanup expired quota data older than the specified retention period. | #### `plans` ```bash pinner admin quota plans ``` List, create, update, and delete quota plans. ```bash pinner admin quota plans list pinner admin quota plans get `` ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all available quota plans. | | `get` | Get details of a specific quota plan. | | `create` | Create a new quota plan with specified limits. | | `update` | Update an existing quota plan. | | `delete` | Delete a quota plan by ID. | | `set-default` | Set a quota plan as the default for new users. | #### `list` ```bash pinner admin quota plans list ``` List all available quota plans. ```bash pinner admin quota plans list pinner admin quota plans list --json ``` #### `get` ```bash pinner admin quota plans get ``` Get details of a specific quota plan. ```bash pinner admin quota plans get `` pinner admin quota plans get `` --json ``` #### `create` ```bash pinner admin quota plans create [flags] ``` Create a new quota plan with specified limits. ```bash pinner admin quota plans create --name "Pro" --upload 1000 --download 2000 --storage 5000 pinner admin quota plans create --name "Free" --is-active --is-default pinner admin quota plans create --name "Basic" --description "Basic tier" --is-active ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Plan name | | `--description` | | Plan description | | `--upload-limit` | | Upload limit (bytes) | | `--download-limit` | | Download limit (bytes) | | `--storage-limit` | | Storage limit (bytes) | | `--window-type` | | Window type (ROLLING, DAY, WEEK, MONTH, YEAR, LIFETIME) | | `--is-active` | | Mark plan as active | | `--is-default` | | Set as default plan for new users | #### `update` ```bash pinner admin quota plans update [flags] ``` Update an existing quota plan. ```bash pinner admin quota plans update `` --name "Updated Pro" pinner admin quota plans update `` --is-active --is-default ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Plan name | | `--description` | | Plan description | | `--upload-limit` | | Upload limit (bytes) | | `--download-limit` | | Download limit (bytes) | | `--storage-limit` | | Storage limit (bytes) | | `--window-type` | | Window type (ROLLING, DAY, WEEK, MONTH, YEAR, LIFETIME) | | `--is-active` | | Mark plan as active | | `--is-default` | | Set as default plan for new users | #### `delete` ```bash pinner admin quota plans delete ``` Delete a quota plan by ID. ```bash pinner admin quota plans delete `` ``` #### `set-default` ```bash pinner admin quota plans set-default ``` Set a quota plan as the default for new users. ```bash pinner admin quota plans set-default `` ``` #### `allowances` ```bash pinner admin quota allowances ``` List, create, update, and delete quota allowances. ```bash pinner admin quota allowances list pinner admin quota allowances create --user-id 123 --type bonus ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all quota allowances. | | `create` | Create a new quota allowance for a user. | | `update` | Update an existing quota allowance. | | `delete` | Delete a quota allowance by grant ID. | #### `list` ```bash pinner admin quota allowances list ``` List all quota allowances. ```bash pinner admin quota allowances list pinner admin quota allowances list --json ``` #### `create` ```bash pinner admin quota allowances create [flags] ``` Create a new quota allowance for a user. ```bash pinner admin quota allowances create --user-id 123 --source admin --type bonus --upload 1000 ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID | | `--source` | | Allowance source | | `--quota-type` | | Allowance type | | `--upload-limit` | | Upload allowance (bytes) | | `--download-limit` | | Download allowance (bytes) | | `--storage-limit` | | Storage allowance (bytes) | | `--expiry` | | Expiry in days from now | #### `update` ```bash pinner admin quota allowances update [flags] ``` Update an existing quota allowance. ```bash pinner admin quota allowances update `` --upload-limit 2000 ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID | | `--source` | | Allowance source | | `--quota-type` | | Allowance type | | `--upload-limit` | | Upload allowance (bytes) | | `--download-limit` | | Download allowance (bytes) | | `--storage-limit` | | Storage allowance (bytes) | | `--expiry` | | Expiry in days from now | #### `delete` ```bash pinner admin quota allowances delete ``` Delete a quota allowance by grant ID. ```bash pinner admin quota allowances delete `` ``` #### `user-configs` ```bash pinner admin quota user-configs ``` List and update user quota configurations. ```bash pinner admin quota user-configs list pinner admin quota user-configs update --user-id 1 --plan-id 19 pinner admin quota user-configs reset `` ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all user quota configurations. | | `update` | Update a user's quota configuration. | | `reset` | Reset a user's quota plan to the default. | #### `list` ```bash pinner admin quota user-configs list ``` List all user quota configurations. ```bash pinner admin quota user-configs list pinner admin quota user-configs list --json ``` #### `update` ```bash pinner admin quota user-configs update [flags] ``` Update a user's quota configuration. ```bash pinner admin quota user-configs update --user-id 1 --plan-id 19 pinner admin quota user-configs update --user-id 1 --plan-id 19 --enforcement-policy HARD_LIMITS pinner admin quota user-configs update --user-id 1 --upload-limit 5000 --download-limit 10000 pinner admin quota user-configs update --user-id 1 --plan-id 19 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID | | `--plan-id` | | Quota plan ID to assign | | `--enforcement-policy` | | Enforcement policy (HARD\_LIMITS, UNLIMITED, ALLOWANCE, THRESHOLD) | | `--upload-limit` | | Upload limit override (bytes) | | `--download-limit` | | Download limit override (bytes) | | `--storage-limit` | | Storage limit override (bytes) | | `--upload-threshold` | | Upload threshold override (bytes) | | `--download-threshold` | | Download threshold override (bytes) | | `--storage-threshold` | | Storage threshold override (bytes) | | `--window-duration` | | Window duration override | | `--window-start-hour` | | Window start hour override | | `--window-timezone` | | Window timezone override | | `--window-type` | | Window type override (ROLLING, DAY, WEEK, MONTH, YEAR, LIFETIME) | #### `reset` ```bash pinner admin quota user-configs reset ``` Reset a user's quota plan to the default. ```bash pinner admin quota user-configs reset `` ``` #### `stats` ```bash pinner admin quota stats ``` View quota system statistics. ```bash pinner admin quota stats pinner admin quota stats --json ``` #### `reconcile` ```bash pinner admin quota reconcile [flags] ``` Reconcile quota data for all users or a specific user. ```bash pinner admin quota reconcile pinner admin quota reconcile --user-id 123 ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | Specific user ID to reconcile (optional) | #### `cleanup` ```bash pinner admin quota cleanup [flags] ``` Cleanup expired quota data older than the specified retention period. ```bash pinner admin quota cleanup --retention-days 90 ``` | Flag | Alias | Description | |------|-------|-------------| | `--retention-days` | | Retention period in days | ### `billing` ```bash pinner admin billing ``` Manage billing credits, price lines, pricing plans, and subscriptions. **Billing operations include:** * Credit management (list, create, delete, restore, purge) * User balance viewing * Price line management * Pricing plan and period management * Subscriber and subscription management ```bash pinner admin billing credits list pinner admin billing price-lines list pinner admin billing pricing-plans list pinner admin billing subscribers list ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `overview` | Display an overview of billing entities and their relationships. | | `credits` | Manage billing credits for users. | | `price-lines` | List, create, update, and delete billing price lines. | | `pricing-plans` | List, create, update, delete, and sync billing pricing plans. | | `pricing-plan-periods` | List, create, update, and delete billing pricing plan periods. | | `subscribers` | List, manage, and modify billing subscribers. | #### `overview` ```bash pinner admin billing overview ``` Display an overview of billing entities and their relationships. Shows the data model hierarchy and current entity counts. ```bash pinner admin billing overview pinner admin billing overview --json ``` #### `credits` ```bash pinner admin billing credits ``` Manage billing credits for users. ```bash pinner admin billing credits list pinner admin billing credits get `` pinner admin billing credits create --user-id 123 --amount 100.00 --type manual --direction credit pinner admin billing credits delete `` pinner admin billing credits restore `` pinner admin billing credits purge pinner admin billing credits user-balance `` pinner admin billing credits user-deleted-credits `` ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all billing credits with optional filtering. | | `get` | Get details of a specific credit by its ID. | | `create` | Create a new billing credit for a user. | | `delete` | Soft-delete a credit by its ID. | | `restore` | Restore a soft-deleted credit by its ID. | | `purge` | Permanently delete soft-deleted credits older than specified duration. | | `user-balance` | Get the current balance for a specific user. | | `user-deleted-credits` | Get all soft-deleted credits for a specific user. | #### `list` ```bash pinner admin billing credits list [flags] ``` List all billing credits with optional filtering. ```bash pinner admin billing credits list pinner admin billing credits list --user-id 123 pinner admin billing credits list --direction credit pinner admin billing credits list --type manual ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | Filter by user ID | | `--direction` | | Filter by direction (credit, debit) | | `--type` | | Filter by type | #### `get` ```bash pinner admin billing credits get ``` Get details of a specific credit by its ID. ```bash pinner admin billing credits get `` pinner admin billing credits get `` --json ``` #### `create` ```bash pinner admin billing credits create [flags] ``` Create a new billing credit for a user. ```bash pinner admin billing credits create --user-id 123 --amount 100.00 --type manual --direction credit pinner admin billing credits create --user-id 123 --amount 50.00 --type promo --direction credit --description "Promotional credit" pinner admin billing credits create --user-id 123 --amount 200.00 --type manual --direction debit --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID to credit **(required)** | | `--amount` | | Credit amount (as decimal string) **(required)** | | `--type` | | Credit type (e.g., manual, promo, referral) **(required)** | | `--direction` | | Direction (credit or debit) **(required)** | | `--description` | | Credit description | | `--reference-id` | | Reference ID for this credit | | `--reference-type` | | Reference type for this credit | #### `delete` ```bash pinner admin billing credits delete ``` Soft-delete a credit by its ID. ```bash pinner admin billing credits delete `` pinner admin billing credits delete `` --json ``` #### `restore` ```bash pinner admin billing credits restore ``` Restore a soft-deleted credit by its ID. ```bash pinner admin billing credits restore `` pinner admin billing credits restore `` --json ``` #### `purge` ```bash pinner admin billing credits purge [flags] ``` Permanently delete soft-deleted credits older than specified duration. ```bash pinner admin billing credits purge pinner admin billing credits purge --older-than "30d" pinner admin billing credits purge --older-than "7d" --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--older-than` | | Delete credits deleted more than this duration ago (e.g., 30d, 1w, 24h) | #### `user-balance` ```bash pinner admin billing credits user-balance ``` Get the current balance for a specific user. ```bash pinner admin billing credits user-balance `` pinner admin billing credits user-balance `` --json ``` #### `user-deleted-credits` ```bash pinner admin billing credits user-deleted-credits [flags] ``` Get all soft-deleted credits for a specific user. ```bash pinner admin billing credits user-deleted-credits `` pinner admin billing credits user-deleted-credits `` --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--direction` | | Filter by direction | | `--type` | | Filter by type | #### `price-lines` ```bash pinner admin billing price-lines ``` List, create, update, and delete billing price lines. ```bash pinner admin billing price-lines list pinner admin billing price-lines get `` pinner admin billing price-lines create --name "Storage" --description "Storage pricing" pinner admin billing price-lines update `` --name "Updated Storage" pinner admin billing price-lines delete `` pinner admin billing price-lines add-plan `` --plan-id `` --position 1 pinner admin billing price-lines delete-plan `` --plan-id `` pinner admin billing price-lines update-plan-position `` --plan-id `` --position 2 ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all billing price lines. | | `get` | Get detailed information about a billing price line. | | `create` | Create a new billing price line. | | `update` | Update an existing price line. | | `delete` | Delete a billing price line. | | `add-plan` | Add a pricing plan to a price line. | | `delete-plan` | Remove a pricing plan from a price line. | | `update-plan-position` | Update the position of a pricing plan within a price line. | #### `list` ```bash pinner admin billing price-lines list ``` List all billing price lines. ```bash pinner admin billing price-lines list pinner admin billing price-lines list --json ``` #### `get` ```bash pinner admin billing price-lines get ``` Get detailed information about a billing price line. ```bash pinner admin billing price-lines get `` pinner admin billing price-lines get `` --json ``` #### `create` ```bash pinner admin billing price-lines create [flags] ``` Create a new billing price line. ```bash pinner admin billing price-lines create --name "Storage" --description "Storage pricing" pinner admin billing price-lines create --name "Bandwidth" --description "Monthly bandwidth" --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Price line name **(required)** | | `--description` | | Price line description | | `--is-active` | | Mark price line as active | | `--is-default` | | Mark as default price line | #### `update` ```bash pinner admin billing price-lines update [flags] ``` Update an existing price line. ```bash pinner admin billing price-lines update `` --name "Updated Storage" pinner admin billing price-lines update `` --description "New description" --is-active false --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Price line name | | `--description` | | Price line description | | `--is-active` | | Mark price line as active | | `--is-default` | | Mark as default price line | #### `delete` ```bash pinner admin billing price-lines delete ``` Delete a billing price line. ```bash pinner admin billing price-lines delete `` pinner admin billing price-lines delete `` --json ``` #### `add-plan` ```bash pinner admin billing price-lines add-plan [flags] ``` Add a pricing plan to a price line. If --position is omitted, the plan is appended to the end of the price line. ```bash pinner admin billing price-lines add-plan `` --plan-id `` pinner admin billing price-lines add-plan `` --plan-id `` --position 1 pinner admin billing price-lines add-plan `` --plan-id `` --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--plan-id` | | Pricing plan ID to add **(required)** | | `--position` | | Position of the plan in the price line (auto-appended if omitted) | #### `delete-plan` ```bash pinner admin billing price-lines delete-plan [flags] ``` Remove a pricing plan from a price line. ```bash pinner admin billing price-lines delete-plan `` --plan-id `` pinner admin billing price-lines delete-plan `` --plan-id `` --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--plan-id` | | Pricing plan ID to remove **(required)** | #### `update-plan-position` ```bash pinner admin billing price-lines update-plan-position [flags] ``` Update the position of a pricing plan within a price line. ```bash pinner admin billing price-lines update-plan-position `` --plan-id `` --position 1 pinner admin billing price-lines update-plan-position `` --plan-id `` --position 2 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--plan-id` | | Pricing plan ID to reposition **(required)** | | `--position` | | New position for the plan **(required)** | #### `pricing-plans` ```bash pinner admin billing pricing-plans ``` List, create, update, delete, and sync billing pricing plans. ```bash pinner admin billing pricing-plans list pinner admin billing pricing-plans sync `` pinner admin billing pricing-plans sync-all ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all billing pricing plans. | | `get` | Get details of a specific pricing plan. | | `create` | Create a new billing pricing plan. | | `update` | Update an existing billing pricing plan. | | `delete` | Delete a pricing plan by ID. | | `sync` | Trigger immediate synchronization of a specific pricing plan with the payment gateway. | | `sync-all` | Trigger synchronization of all pricing plans with payment gateways. | #### `list` ```bash pinner admin billing pricing-plans list ``` List all billing pricing plans. ```bash pinner admin billing pricing-plans list pinner admin billing pricing-plans list --json ``` #### `get` ```bash pinner admin billing pricing-plans get ``` Get details of a specific pricing plan. **Arguments:** `` The unique identifier of the pricing plan ```bash pinner admin billing pricing-plans get 1 pinner admin billing pricing-plans get 5 --json ``` #### `create` ```bash pinner admin billing pricing-plans create [flags] ``` Create a new billing pricing plan. **Optionally create a pricing plan period in the same command by providing:** **--quota-plan-id:** Creates a period with the specified quota plan **--price:** Required with --quota-plan-id, price in USD **--cadence:** Required with --quota-plan-id, cadence (monthly, yearly, rolling) **--rolling-days:** Optional, for rolling cadence only **--allow-free:** Optional, allows $0 price ```bash pinner admin billing pricing-plans create --name "Pro Plan" --currency USD pinner admin billing pricing-plans create --name "Basic" --currency USD --description "Basic plan" --is-active pinner admin billing pricing-plans create --name "Premium" --currency USD --is-public --json # Create plan with period in one command: pinner admin billing pricing-plans create --name "Starter" --currency USD --quota-plan-id 1 --price 9.99 --cadence monthly pinner admin billing pricing-plans create --name "Annual" --currency USD --quota-plan-id 2 --price 99.99 --cadence yearly ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Pricing plan name **(required)** | | `--currency` | | Currency code (e.g., USD, EUR) **(required)** | | `--description` | | Plan description | | `--is-active` | | Mark plan as active | | `--is-public` | | Mark plan as public | | `--priceline-id` | | Associated price line ID | | `--quota-plan-id` | | Create a period: associated quota plan ID | | `--price` | | Create a period: price in USD | | `--cadence` | | Create a period: cadence (e.g., monthly, yearly, rolling) | | `--rolling-days` | | Create a period: rolling days (for rolling cadence only) | | `--allow-free` | | Create a period: allow $0 price | #### `update` ```bash pinner admin billing pricing-plans update [flags] ``` Update an existing billing pricing plan. ```bash pinner admin billing pricing-plans update `` --name "Updated Pro" pinner admin billing pricing-plans update `` --description "New desc" --is-active false --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--name` | | Pricing plan name | | `--currency` | | Currency code (e.g., USD, EUR) | | `--description` | | Plan description | | `--is-active` | | Mark plan as active | | `--is-public` | | Mark plan as public | #### `delete` ```bash pinner admin billing pricing-plans delete ``` Delete a pricing plan by ID. ```bash pinner admin billing pricing-plans delete `` pinner admin billing pricing-plans delete `` --json ``` #### `sync` ```bash pinner admin billing pricing-plans sync ``` Trigger immediate synchronization of a specific pricing plan with the payment gateway. This command syncs a single pricing plan to ensure the payment gateway has the latest configuration. **Arguments:** `` The unique identifier of the pricing plan to sync ```bash pinner admin billing pricing-plans sync `` pinner admin billing pricing-plans sync 123 --json ``` #### `sync-all` ```bash pinner admin billing pricing-plans sync-all ``` Trigger synchronization of all pricing plans with payment gateways. This command syncs all pricing plans to ensure the payment gateways have the latest configurations. ```bash pinner admin billing pricing-plans sync-all pinner admin billing pricing-plans sync-all --json ``` #### `pricing-plan-periods` ```bash pinner admin billing pricing-plan-periods ``` List, create, update, and delete billing pricing plan periods. pinner admin billing pricing-plan-periods list **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all billing pricing plan periods. | | `get` | Get details of a specific pricing plan period. | | `create` | Create a new billing pricing plan period. | | `update` | Update an existing billing pricing plan period. | | `delete` | Delete a pricing plan period by ID. | #### `list` ```bash pinner admin billing pricing-plan-periods list ``` List all billing pricing plan periods. ```bash pinner admin billing pricing-plan-periods list pinner admin billing pricing-plan-periods list --json ``` #### `get` ```bash pinner admin billing pricing-plan-periods get ``` Get details of a specific pricing plan period. ```bash pinner admin billing pricing-plan-periods get `` pinner admin billing pricing-plan-periods get `` --json ``` #### `create` ```bash pinner admin billing pricing-plan-periods create [flags] ``` Create a new billing pricing plan period. ```bash pinner admin billing pricing-plan-periods create --plan-id 123 --price 9.99 --cadence monthly --quota-plan-id 1 pinner admin billing pricing-plan-periods create --plan-id 123 --price 99.99 --cadence yearly --quota-plan-id 1 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--plan-id` | | Pricing plan ID **(required)** | | `--price` | | Price in USD **(required)** | | `--cadence` | | Cadence (e.g., monthly, yearly) **(required)** | | `--quota-plan-id` | | Associated quota plan ID **(required)** | | `--rolling-days` | | Rolling days (for rolling periods) | | `--allow-free` | | Allow $0 price (free plan) | #### `update` ```bash pinner admin billing pricing-plan-periods update [flags] ``` Update an existing billing pricing plan period. ```bash pinner admin billing pricing-plan-periods update `` --price 19.99 pinner admin billing pricing-plan-periods update `` --cadence yearly --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--price` | | Price in USD | | `--cadence` | | Cadence (e.g., monthly, yearly) | | `--quota-plan-id` | | Associated quota plan ID | | `--rolling-days` | | Rolling days (for rolling periods) | | `--allow-free` | | Allow $0 price (free plan) | #### `delete` ```bash pinner admin billing pricing-plan-periods delete ``` Delete a pricing plan period by ID. ```bash pinner admin billing pricing-plan-periods delete `` pinner admin billing pricing-plan-periods delete `` --json ``` #### `subscribers` ```bash pinner admin billing subscribers ``` List, manage, and modify billing subscribers. ```bash pinner admin billing subscribers list pinner admin billing subscribers get `` pinner admin billing subscribers list-gateway `` pinner admin billing subscribers list-user `` pinner admin billing subscribers cancel --user-id 123 pinner admin billing subscribers abort-cancel --user-id 123 pinner admin billing subscribers change-plan --user-id 123 --plan-id "plan-abc" pinner admin billing subscribers pause --user-id 123 pinner admin billing subscribers resume --user-id 123 ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `list` | List all billing subscribers across all gateways. | | `get` | Get details of a specific subscriber. | | `list-gateway` | List all subscribers for a specific gateway. | | `list-user` | List all subscriptions for a specific user. | | `cancel` | Cancel a user's subscription. | | `abort-cancel` | Abort a scheduled subscription cancellation for a user. | | `change-plan` | Change a user's subscription plan. | | `pause` | Pause a user's subscription. | | `resume` | Resume a paused subscription for a user. | #### `list` ```bash pinner admin billing subscribers list ``` List all billing subscribers across all gateways. ```bash pinner admin billing subscribers list pinner admin billing subscribers list --json ``` #### `get` ```bash pinner admin billing subscribers get ``` Get details of a specific subscriber. ```bash pinner admin billing subscribers get `` pinner admin billing subscribers get `` --json ``` #### `list-gateway` ```bash pinner admin billing subscribers list-gateway ``` List all subscribers for a specific gateway. ```bash pinner admin billing subscribers list-gateway `` pinner admin billing subscribers list-gateway `` --json ``` #### `list-user` ```bash pinner admin billing subscribers list-user ``` List all subscriptions for a specific user. ```bash pinner admin billing subscribers list-user `` pinner admin billing subscribers list-user `` --json ``` #### `cancel` ```bash pinner admin billing subscribers cancel [flags] ``` Cancel a user's subscription. ```bash pinner admin billing subscribers cancel --user-id 123 pinner admin billing subscribers cancel --user-id 123 --mode immediate pinner admin billing subscribers cancel --user-id 123 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID **(required)** | | `--mode` | | Cancel mode: immediate, end\_of\_billing\_period, etc | #### `abort-cancel` ```bash pinner admin billing subscribers abort-cancel [flags] ``` Abort a scheduled subscription cancellation for a user. ```bash pinner admin billing subscribers abort-cancel --user-id 123 pinner admin billing subscribers abort-cancel --user-id 123 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID **(required)** | #### `change-plan` ```bash pinner admin billing subscribers change-plan [flags] ``` Change a user's subscription plan. ```bash pinner admin billing subscribers change-plan --user-id 123 --period-id 1 pinner admin billing subscribers change-plan --user-id 123 --period-id 1 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID **(required)** | | `--period-id` | | New plan period ID **(required)** | #### `pause` ```bash pinner admin billing subscribers pause [flags] ``` Pause a user's subscription. ```bash pinner admin billing subscribers pause --user-id 123 pinner admin billing subscribers pause --user-id 123 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID **(required)** | #### `resume` ```bash pinner admin billing subscribers resume [flags] ``` Resume a paused subscription for a user. ```bash pinner admin billing subscribers resume --user-id 123 pinner admin billing subscribers resume --user-id 123 --json ``` | Flag | Alias | Description | |------|-------|-------------| | `--user-id` | | User ID **(required)** | ### `websites` ```bash pinner admin websites ``` Administrative operations for IPFS websites. ```bash pinner admin websites block `` pinner admin websites unblock `` ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `block` | Block a website by its ID. | | `unblock` | Unblock a previously blocked website by its ID. | #### `block` ```bash pinner admin websites block ``` Block a website by its ID. ```bash pinner admin websites block `` pinner admin websites block `` --json ``` #### `unblock` ```bash pinner admin websites unblock ``` Unblock a previously blocked website by its ID. ```bash pinner admin websites unblock `` pinner admin websites unblock `` --json ``` ### `pprof` ```bash pinner admin pprof ``` Access Go runtime profiling data via pprof endpoints. **Profiles are binary data meant for consumption by 'go tool pprof'. Redirect output to a file, then analyze:** pinner admin pprof heap > heap.prof && go tool pprof heap.prof pinner admin pprof cpu > cpu.prof && go tool pprof cpu.prof pinner admin pprof trace > trace.out && go tool trace trace.out ```bash pinner admin pprof status pinner admin pprof heap pinner admin pprof set-block-rate 1 pinner admin pprof set-mutex-fraction 100 ``` **Subcommands:** | Subcommand | Description | |------------|-------------| | `index` | Show the pprof index page listing available profiles. | | `block` | Get block profile data for analyzing goroutine blocking events. | | `set-block-rate` | Set the block profiling rate. 0 disables, 1 captures all events, higher values sample. | | `cmdline` | Get the command line of the running program. | | `goroutine` | Get stack traces of all current goroutines. | | `heap` | Get a sampling of memory allocations of live objects. | | `mutex` | Get stack traces of holders of contended mutexes. | | `set-mutex-fraction` | Set the mutex profiling fraction. 0 disables, 1 captures all events, 100 samples 1%. | | `cpu` | Get a CPU profile for the default duration. | | `status` | Get the current block and mutex profiling rates. | | `symbol` | Look up program counters and return function names. | | `threadcreate` | Get stack traces that led to the creation of new OS threads. | | `trace` | Get an execution trace of the running program. | #### `index` ```bash pinner admin pprof index ``` Show the pprof index page listing available profiles. ```bash pinner admin pprof index ``` #### `block` ```bash pinner admin pprof block ``` Get block profile data for analyzing goroutine blocking events. ```bash pinner admin pprof block > block.prof && go tool pprof block.prof ``` #### `set-block-rate` ```bash pinner admin pprof set-block-rate ``` Set the block profiling rate. 0 disables, 1 captures all events, higher values sample. ```bash pinner admin pprof set-block-rate 1 pinner admin pprof set-block-rate 0 ``` #### `cmdline` ```bash pinner admin pprof cmdline ``` Get the command line of the running program. ```bash pinner admin pprof cmdline ``` #### `goroutine` ```bash pinner admin pprof goroutine ``` Get stack traces of all current goroutines. ```bash pinner admin pprof goroutine > goroutine.prof && go tool pprof goroutine.prof ``` #### `heap` ```bash pinner admin pprof heap ``` Get a sampling of memory allocations of live objects. ```bash pinner admin pprof heap > heap.prof && go tool pprof heap.prof ``` #### `mutex` ```bash pinner admin pprof mutex ``` Get stack traces of holders of contended mutexes. ```bash pinner admin pprof mutex > mutex.prof && go tool pprof mutex.prof ``` #### `set-mutex-fraction` ```bash pinner admin pprof set-mutex-fraction ``` Set the mutex profiling fraction. 0 disables, 1 captures all events, 100 samples 1%. ```bash pinner admin pprof set-mutex-fraction 1 pinner admin pprof set-mutex-fraction 0 ``` #### `cpu` ```bash pinner admin pprof cpu ``` Get a CPU profile for the default duration. ```bash pinner admin pprof cpu > cpu.prof && go tool pprof cpu.prof ``` #### `status` ```bash pinner admin pprof status ``` Get the current block and mutex profiling rates. ```bash pinner admin pprof status pinner admin pprof status --json ``` #### `symbol` ```bash pinner admin pprof symbol ``` Look up program counters and return function names. ```bash pinner admin pprof symbol ``` #### `threadcreate` ```bash pinner admin pprof threadcreate ``` Get stack traces that led to the creation of new OS threads. ```bash pinner admin pprof threadcreate > threadcreate.prof && go tool pprof threadcreate.prof ``` #### `trace` ```bash pinner admin pprof trace ``` Get an execution trace of the running program. ```bash pinner admin pprof trace > trace.out && go tool trace trace.out ``` > **This page is auto-generated** from the [pinner-cli](https://github.com/LumeWeb/pinner-cli) source code. Do not edit directly. ## `config` ```bash pinner config [get | set ] [flags] ``` View or modify CLI configuration settings. **Usage:** pinner config Show all configuration values pinner config get `` Get specific configuration value pinner config set `` `` Set configuration value ```bash pinner config pinner config get base_endpoint pinner config set max_retries 5 pinner config set secure false pinner config set memory_limit 256 pinner config set max_retries 5 --dry-run Configuration can also be set via environment variables with the PINNER_ prefix. For example, PINNER_BASE_ENDPOINT sets base_endpoint, PINNER_SECURE sets secure. Common keys: base_endpoint ``` * API endpoint (empty for default) secure * Use HTTPS (true/false) max\_retries * Maximum retry attempts (default: 3) memory\_limit * Memory limit for CAR generation in MB (default: 100) auth\_token * Authentication token (managed by 'pinner auth') | Flag | Alias | Description | |------|-------|-------------| | `--json` | | Output JSON instead of human-readable | | `--verbose` | | Show detailed output | | `--quiet` | | Suppress non-error output | | `--unmask` | | Show sensitive data (tokens, passwords, secrets) unmasked | | `--auth-token` | | Auth token to override config \[env: `PINNER_AUTH_TOKEN`] | | `--secure` | | Use HTTPS for API connections \[env: `PINNER_SECURE`] | | `--dry-run` | | Preview operations without making any changes | ## `doctor` ```bash pinner doctor [flags] ``` Show diagnostic information about your pinner CLI environment, including version, OS details, configuration location, and limits. This is useful when reporting issues or troubleshooting problems. ```bash pinner doctor pinner doctor --json Use this command when: ``` * Reporting bugs or issues * Troubleshooting connection problems * Verifying your configuration * Checking authentication status | Flag | Alias | Description | |------|-------|-------------| | `--json` | | Output in JSON format | ## `bench` ```bash pinner bench [path] [flags] ``` **Run a benchmark by uploading random data (or a specified path) and tracking each stage of the pipeline:** generate, upload, queued, pinning, pinned. After the benchmark completes, all uploaded pins are automatically removed (unless --no-cleanup is specified). ```bash # Quick benchmark: 1MB random file pinner bench # Benchmark with real data pinner bench ./my-project # 10 iterations of 100MB upload for avg/min/max stats pinner bench --size 100MB --iterations 10 # Simulate folder upload: 3 levels deep, 20 files, 50MB total pinner bench --size 50MB --files 20 --depth 3 # Parallel uploads (3 concurrent) pinner bench --size 10MB --iterations 9 --parallel 3 # Keep the pins around for inspection pinner bench --no-cleanup # Faster polling for stage transitions pinner bench --poll-interval 250ms The output includes per-iteration stage timing and a summary with min/max/avg/median statistics across all iterations ``` | Flag | Alias | Description | |------|-------|-------------| | `--size` | | Size of random data to generate (e.g., 1MB, 100MB, 1GB). Only used when no path is given | | `--files` | | Number of random files to generate | | `--depth` | | Create nested folder structure N levels deep with random files | | `--iterations` | | Number of upload iterations (each generates fresh random data) | | `--parallel` | | Number of parallel operations (default: 1) | | `--no-cleanup` | | Skip unpin after benchmark (pins remain on the server) | | `--poll-interval` | | Interval for polling pin status during stage tracking | | `--memory-limit` | | Memory limit for CAR generation in megabytes \[env: `PINNER_MEMORY_LIMIT`] | | `--chunk-size` | | Chunk size in bytes for UnixFS file splitting (default: 1048576) | | `--chunker` | | DAG layout strategy: balanced (default) or trickle | | `--max-links` | | Maximum number of links per DAG node (default: 174) | | `--dry-run` | | Preview operations without making any changes | ## `generate-docs` ```bash pinner generate-docs ``` Generate CLI documentation in markdown, man, or JSON format **Subcommands:** | Subcommand | Description | |------------|-------------| | `markdown` | Generate markdown documentation | | `man` | Generate man page documentation | | `json` | Generate JSON documentation | ### `markdown` ```bash pinner generate-docs markdown [flags] ``` Generate markdown documentation | Flag | Alias | Description | |------|-------|-------------| | `--output` | | Output file (default: stdout) | ### `man` ```bash pinner generate-docs man [flags] ``` Generate man page documentation | Flag | Alias | Description | |------|-------|-------------| | `--output` | | Output file (default: stdout) | | `--section` | | Man section number | ### `json` ```bash pinner generate-docs json [flags] ``` Generate JSON documentation | Flag | Alias | Description | |------|-------|-------------| | `--output` | | Output file (default: stdout) | > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. ## Install ```bash npm install @lumeweb/pinner ``` ## Quick Start ```typescript import { Pinner } from "@lumeweb/pinner"; const client = new Pinner({ authToken: process.env.PINNER_AUTH_TOKEN!, }); // Upload a file const result = await client.upload(file); console.log(result.cid); ``` ## Classes | Class | Description | |-------|-------------| | [`IpnsClient`](/reference/sdk/ipns-client) | Client for managing IPNS keys and publishing content to IPNS names. | | [`WebsitesClient`](/reference/sdk/websites-client) | Client for managing websites hosted on IPFS with custom domains and SSL. | | [`Pinner`](/reference/sdk/pinner) | | | [`UploadManager`](/reference/sdk/upload-manager) | Handles file uploads via XHR or TUS protocol based on file size. | ## Errors All SDK errors extend [PinnerError](/reference/sdk/errors#pinnererror). See [Error Classes](/reference/sdk/errors) for the full hierarchy. ## Types 36 interfaces and 34 type aliases. See [Types](/reference/sdk/types) for full definitions. > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. ## Constructor ```typescript new Pinner(config: PinnerConfig) ``` | Parameter | Type | Description | |-----------|------|-------------| | `config` | `PinnerConfig` | SDK configuration object | ## Methods | Method | Returns | Description | |--------|---------|-------------| | [`pins()`](#pins) | `RemotePins` | Access the remote pins interface. | | [`ipns()`](#ipns) | [IpnsClient](/reference/sdk/ipns-client) | Access the IPNS interface for key management and publishing. | | [`websites()`](#websites) | [WebsitesClient](/reference/sdk/websites-client) | Access the websites interface for website configuration and management. | | [`upload()`](#upload) | `UploadMethodAndBuilder` | Upload interface that works as both a method and a builder namespace. As a method: upload(file, options) -> UploadOperation As a property: upload.file(), upload.json(), etc. -> Builder | | [`uploadAndWait(file, options)`](#uploadAndWait) | `Promise` | Upload a file and wait for completion. Convenience method for simple use cases where controls aren't needed. | | [`waitForOperation(input, options)`](#waitForOperation) | `Promise` | Wait for an operation to complete or reach a settled state. | | [`uploadDirectory(files, options)`](#uploadDirectory) | `Promise` | Upload a directory to IPFS. | | [`uploadCar(file, options)`](#uploadCar) | `Promise` | Upload a CAR file without preprocessing. This is useful for passthrough of pre-generated CAR files. | | [`pinByHash(cid, options)`](#pinByHash) | `Promise>` | Pin existing content by CID. | | [`listPins(options)`](#listPins) | `Promise` | List pinned content. | | [`getPinStatus(cid)`](#getPinStatus) | `Promise` | Get pin status. | | [`isPinned(cid)`](#isPinned) | `Promise` | Check if content is pinned. | | [`setPinMetadata(cid, metadata)`](#setPinMetadata) | `Promise` | Update pin metadata. | | [`unpin(cid, options)`](#unpin) | `Promise` | Remove a pin. The block may be deleted when garbage collection is run. | | [`unpinByRequestId(requestId, options)`](#unpinByRequestId) | `Promise` | Remove a pin by request ID. The block may be deleted when garbage collection is run. | | [`destroy()`](#destroy) | `void` | Destroy the client and cleanup resources. | ### `pins()` ```typescript pins(): RemotePins ``` Access the remote pins interface. **Returns:** `RemotePins` ### `ipns()` ```typescript ipns(): IpnsClient ``` Access the IPNS interface for key management and publishing. **Returns:** [IpnsClient](/reference/sdk/ipns-client) ### `websites()` ```typescript websites(): WebsitesClient ``` Access the websites interface for website configuration and management. **Returns:** [WebsitesClient](/reference/sdk/websites-client) ### `upload()` ```typescript upload(): UploadMethodAndBuilder ``` Upload interface that works as both a method and a builder namespace. As a method: upload(file, options) -> UploadOperation As a property: upload.file(), upload.json(), etc. -> Builder **Returns:** `UploadMethodAndBuilder` ### `uploadAndWait(file, options?)` ```typescript uploadAndWait(file: File, options?: UploadOptions): Promise ``` Upload a file and wait for completion. Convenience method for simple use cases where controls aren't needed. **Parameters:** | Name | Type | Description | |------|------|-------------| | `file` | `File` | The file to upload | | `options` | `UploadOptions` | Upload configuration *optional* | **Returns:** `Promise` ### `waitForOperation(input, options?)` ```typescript waitForOperation(input: number | UploadResult, options?: any): Promise ``` Wait for an operation to complete or reach a settled state. **Parameters:** | Name | Type | Description | |------|------|-------------| | `input` | `number` | `UploadResult` | Either an operation ID (number) or an UploadResult | | `options` | `any` | Polling options (interval, timeout, settledStates) *optional* | **Returns:** `Promise` ### `uploadDirectory(files, options?)` ```typescript uploadDirectory(files: File[], options?: UploadOptions): Promise ``` Upload a directory to IPFS. **Parameters:** | Name | Type | Description | |------|------|-------------| | `files` | `File[]` | Array of files to upload as a directory | | `options` | `UploadOptions` | Upload configuration *optional* | **Returns:** `Promise` ### `uploadCar(file, options?)` ```typescript uploadCar(file: File | ReadableStream>, options?: UploadOptions): Promise ``` Upload a CAR file without preprocessing. This is useful for passthrough of pre-generated CAR files. **Parameters:** | Name | Type | Description | |------|------|-------------| | `file` | `File` | `ReadableStream>` | CAR file or stream to upload | | `options` | `UploadOptions` | Upload configuration *optional* | **Returns:** `Promise` ### `pinByHash(cid, options?)` ```typescript pinByHash(cid: string | CID, options?: RemoteAddOptions): Promise, void, undefined>> ``` Pin existing content by CID. **Parameters:** | Name | Type | Description | |------|------|-------------| | `cid` | `string` | `CID` | CID of content to pin (string or CID object) | | `options` | `RemoteAddOptions` | Remote add options *optional* | **Returns:** `Promise>` ### `listPins(options?)` ```typescript listPins(options?: RemoteLsOptions): Promise ``` List pinned content. **Parameters:** | Name | Type | Description | |------|------|-------------| | `options` | `RemoteLsOptions` | List filtering options *optional* | **Returns:** `Promise` ### `getPinStatus(cid)` ```typescript getPinStatus(cid: string | CID): Promise ``` Get pin status. **Parameters:** | Name | Type | Description | |------|------|-------------| | `cid` | `string` | `CID` | CID of the pinned content to check | **Returns:** `Promise` ### `isPinned(cid)` ```typescript isPinned(cid: string | CID): Promise ``` Check if content is pinned. **Parameters:** | Name | Type | Description | |------|------|-------------| | `cid` | `string` | `CID` | CID to check | **Returns:** `Promise` ### `setPinMetadata(cid, metadata)` ```typescript setPinMetadata(cid: string | CID, metadata: Record | undefined): Promise ``` Update pin metadata. **Parameters:** | Name | Type | Description | |------|------|-------------| | `cid` | `string` | `CID` | CID of the pin | | `metadata` | `Record` | `undefined` | Key-value metadata to set | **Returns:** `Promise` ### `unpin(cid, options?)` ```typescript unpin(cid: string | CID, options?: AbortOptions): Promise ``` Remove a pin. The block may be deleted when garbage collection is run. **Parameters:** | Name | Type | Description | |------|------|-------------| | `cid` | `string` | `CID` | CID to unpin | | `options` | `AbortOptions` | Abort options *optional* | **Returns:** `Promise` ### `unpinByRequestId(requestId, options?)` ```typescript unpinByRequestId(requestId: string, options?: AbortOptions): Promise ``` Remove a pin by request ID. The block may be deleted when garbage collection is run. **Parameters:** | Name | Type | Description | |------|------|-------------| | `requestId` | `string` | The request ID to remove | | `options` | `AbortOptions` | Abort options *optional* | **Returns:** `Promise` ### `destroy()` ```typescript destroy(): void ``` Destroy the client and cleanup resources. **Returns:** `void` *** Source: [pinner.ts](https://github.com/LumeWeb/web/blob/f91f8529cde13c6d53768f3fb3abf239748501d3/libs/pinner/src/pinner.ts#L23) > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. Handles file uploads via XHR or TUS protocol based on file size. ## Constructor ```typescript new UploadManager(config: PinnerConfig) ``` | Parameter | Type | Description | |-----------|------|-------------| | `config` | `PinnerConfig` | SDK configuration | ## Methods | Method | Returns | Description | |--------|---------|-------------| | [`fetchUploadLimit()`](#fetchUploadLimit) | `Promise` | Fetch the upload size limit from the API. Falls back to 100 MB on failure. | | [`getUploadLimit()`](#getUploadLimit) | `number` | Get the cached upload size limit (in bytes). Call fetchUploadLimit() first. | | [`upload(input, options)`](#upload) | `Promise` | Upload a file or stream to IPFS. | | [`uploadCar(input, options)`](#uploadCar) | `Promise` | Upload a CAR file or stream directly without preprocessing. | | [`waitForOperation(input, options)`](#waitForOperation) | `Promise` | Wait for an operation to complete or reach a settled state. Handles two scenarios: 1. If an operationId is provided (in UploadResult), uses it directly 2. If only CID is available, lists operations filtered by CID and polls the first result | | [`uploadDirectory(files, options)`](#uploadDirectory) | `Promise` | Upload an array of files as an IPFS directory. | | [`destroy()`](#destroy) | `void` | | ### `fetchUploadLimit()` ```typescript fetchUploadLimit(): Promise ``` Fetch the upload size limit from the API. Falls back to 100 MB on failure. **Returns:** `Promise` ### `getUploadLimit()` ```typescript getUploadLimit(): number ``` Get the cached upload size limit (in bytes). Call fetchUploadLimit() first. **Returns:** `number` ### `upload(input, options?)` ```typescript upload(input: UploadInput, options?: UploadOptions): Promise ``` Upload a file or stream to IPFS. **Parameters:** | Name | Type | Description | |------|------|-------------| | `input` | `UploadInput` | File, stream, or upload object | | `options` | `UploadOptions` | Upload configuration *optional* | **Returns:** `Promise` ### `uploadCar(input, options?)` ```typescript uploadCar(input: File | ReadableStream>, options?: UploadOptions): Promise ``` Upload a CAR file or stream directly without preprocessing. **Parameters:** | Name | Type | Description | |------|------|-------------| | `input` | `File` | `ReadableStream>` | CAR file or stream | | `options` | `UploadOptions` | Upload configuration *optional* | **Returns:** `Promise` ### `waitForOperation(input, options?)` ```typescript waitForOperation(input: number | UploadResult, options?: any): Promise ``` Wait for an operation to complete or reach a settled state. Handles two scenarios: 1. If an operationId is provided (in UploadResult), uses it directly 2. If only CID is available, lists operations filtered by CID and polls the first result **Parameters:** | Name | Type | Description | |------|------|-------------| | `input` | `number` | `UploadResult` | Either an operation ID (number) or an UploadResult | | `options` | `any` | Polling options (interval, timeout, settledStates) *optional* | **Returns:** `Promise` ### `uploadDirectory(files, options?)` ```typescript uploadDirectory(files: File[], options?: UploadOptions): Promise ``` Upload an array of files as an IPFS directory. **Parameters:** | Name | Type | Description | |------|------|-------------| | `files` | `File[]` | Files to upload | | `options` | `UploadOptions` | Upload configuration *optional* | **Returns:** `Promise` ### `destroy()` ```typescript destroy(): void ``` **Returns:** `void` *** Source: [upload/manager.ts](https://github.com/LumeWeb/web/blob/f91f8529cde13c6d53768f3fb3abf239748501d3/libs/pinner/src/upload/manager.ts#L42) > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. Client for managing IPNS keys and publishing content to IPNS names. ## Constructor ```typescript new IpnsClient(config: PinnerConfig) ``` | Parameter | Type | Description | |-----------|------|-------------| | `config` | `PinnerConfig` | SDK configuration | ## Methods | Method | Returns | Description | |--------|---------|-------------| | [`listKeys(options)`](#listKeys) | `Promise` | List all IPNS keys. | | [`getKey(id, options)`](#getKey) | `Promise` | Get a specific IPNS key by ID. | | [`createKey(request, options)`](#createKey) | `Promise` | Create a new IPNS key. | | [`deleteKey(id, options)`](#deleteKey) | `Promise` | Delete an IPNS key by ID. | | [`publish(request, options)`](#publish) | `Promise` | Publish content to an IPNS name. | | [`republish(id, options)`](#republish) | `Promise` | Republish (refresh) an existing IPNS record. | | [`resolve(name, options)`](#resolve) | `Promise` | Resolve an IPNS name to its content. | ### `listKeys(options?)` ```typescript listKeys(options?: IpnsClientOptions): Promise ``` List all IPNS keys. **Parameters:** | Name | Type | Description | |------|------|-------------| | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` ### `getKey(id, options?)` ```typescript getKey(id: number, options?: IpnsClientOptions): Promise ``` Get a specific IPNS key by ID. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Key ID | | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` ### `createKey(request, options?)` ```typescript createKey(request: IPNSKeyRequest, options?: IpnsClientOptions): Promise ``` Create a new IPNS key. **Parameters:** | Name | Type | Description | |------|------|-------------| | `request` | `IPNSKeyRequest` | Key creation parameters | | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` ### `deleteKey(id, options?)` ```typescript deleteKey(id: number, options?: IpnsClientOptions): Promise ``` Delete an IPNS key by ID. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Key ID to delete | | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` ### `publish(request, options?)` ```typescript publish(request: IPNSPublishRequest, options?: IpnsClientOptions): Promise ``` Publish content to an IPNS name. **Parameters:** | Name | Type | Description | |------|------|-------------| | `request` | `IPNSPublishRequest` | Publishing parameters | | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` ### `republish(id, options?)` ```typescript republish(id: number, options?: IpnsClientOptions): Promise ``` Republish (refresh) an existing IPNS record. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Key ID to republish | | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` ### `resolve(name, options?)` ```typescript resolve(name: string, options?: IpnsClientOptions): Promise ``` Resolve an IPNS name to its content. **Parameters:** | Name | Type | Description | |------|------|-------------| | `name` | `string` | IPNS name to resolve | | `options` | `IpnsClientOptions` | Request options *optional* | **Returns:** `Promise` *** Source: [api/ipns.ts](https://github.com/LumeWeb/web/blob/f91f8529cde13c6d53768f3fb3abf239748501d3/libs/pinner/src/api/ipns.ts#L27) > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. Client for managing websites hosted on IPFS with custom domains and SSL. ## Constructor ```typescript new WebsitesClient(config: PinnerConfig) ``` | Parameter | Type | Description | |-----------|------|-------------| | `config` | `PinnerConfig` | SDK configuration | ## Methods | Method | Returns | Description | |--------|---------|-------------| | [`listWebsites(options)`](#listWebsites) | `Promise` | List all websites. | | [`createWebsite(request, options)`](#createWebsite) | `Promise` | Create a new website. | | [`getWebsite(id, options)`](#getWebsite) | `Promise` | Get website details by ID. | | [`updateWebsite(id, request, options)`](#updateWebsite) | `Promise` | Update a website's configuration. | | [`deleteWebsite(id, options)`](#deleteWebsite) | `Promise` | Delete a website by ID. | | [`validateWebsite(id, options)`](#validateWebsite) | `Promise` | Validate a website's DNS and SSL configuration. | | [`getSSLStatus(domain, options)`](#getSSLStatus) | `Promise` | Check the SSL certificate status for a domain. | | [`getWebsiteConfig(options)`](#getWebsiteConfig) | `Promise` | Get the global website configuration. | | [`watchSSL(domain, options)`](#watchSSL) | `SSLWatcher` | Watch SSL status until provisioned, failed, or timed out. | ### `listWebsites(options?)` ```typescript listWebsites(options?: WebsitesClientOptions): Promise ``` List all websites. **Parameters:** | Name | Type | Description | |------|------|-------------| | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `createWebsite(request, options?)` ```typescript createWebsite(request: WebsiteRequest, options?: WebsitesClientOptions): Promise ``` Create a new website. **Parameters:** | Name | Type | Description | |------|------|-------------| | `request` | `WebsiteRequest` | Website creation parameters | | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `getWebsite(id, options?)` ```typescript getWebsite(id: number, options?: WebsitesClientOptions): Promise ``` Get website details by ID. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Website ID | | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `updateWebsite(id, request, options?)` ```typescript updateWebsite(id: number, request: WebsiteUpdateRequest, options?: WebsitesClientOptions): Promise ``` Update a website's configuration. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Website ID | | `request` | `WebsiteUpdateRequest` | Update parameters | | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `deleteWebsite(id, options?)` ```typescript deleteWebsite(id: number, options?: WebsitesClientOptions): Promise ``` Delete a website by ID. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Website ID | | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `validateWebsite(id, options?)` ```typescript validateWebsite(id: number, options?: WebsitesClientOptions): Promise ``` Validate a website's DNS and SSL configuration. **Parameters:** | Name | Type | Description | |------|------|-------------| | `id` | `number` | Website ID | | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `getSSLStatus(domain, options?)` ```typescript getSSLStatus(domain: string, options?: WebsitesClientOptions): Promise ``` Check the SSL certificate status for a domain. **Parameters:** | Name | Type | Description | |------|------|-------------| | `domain` | `string` | Domain name | | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `getWebsiteConfig(options?)` ```typescript getWebsiteConfig(options?: WebsitesClientOptions): Promise ``` Get the global website configuration. **Parameters:** | Name | Type | Description | |------|------|-------------| | `options` | `WebsitesClientOptions` | Request options *optional* | **Returns:** `Promise` ### `watchSSL(domain, options?)` ```typescript watchSSL(domain: string, options?: WatchOptions): SSLWatcher ``` Watch SSL status until provisioned, failed, or timed out. **Parameters:** | Name | Type | Description | |------|------|-------------| | `domain` | `string` | Domain name | | `options` | `WatchOptions` | Watch interval and timeout *optional* | **Returns:** `SSLWatcher` *** Source: [api/websites.ts](https://github.com/LumeWeb/web/blob/f91f8529cde13c6d53768f3fb3abf239748501d3/libs/pinner/src/api/websites.ts#L71) > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. All SDK errors extend the base [`PinnerError`](/reference/sdk/errors#pinnererror) class, which extends the native `Error`. ## Error Hierarchy ``` Error └── PinnerError └── AuthenticationError └── ConfigurationError └── EmptyFileError └── NetworkError └── NotFoundError └── PinError └── RateLimitError └── TimeoutError └── UploadError └── ValidationError ``` ### `AuthenticationError` Extends: `PinnerError` ### `ConfigurationError` Extends: `PinnerError` ### `EmptyFileError` Extends: `ValidationError` ### `NetworkError` Extends: `UploadError` ### `NotFoundError` Extends: `PinError` ### `PinError` Extends: `PinnerError` ### `PinnerError` Extends: `Error` ### `RateLimitError` Extends: `PinError` ### `TimeoutError` Extends: `UploadError` ### `UploadError` Extends: `PinnerError` ### `ValidationError` Extends: `UploadError` > **This page is auto-generated** from [TypeDoc](https://typedoc.org/) based on the [@lumeweb/pinner](https://github.com/LumeWeb/web/tree/develop/libs/pinner/src) source code. Do not edit directly. ## Interfaces ### `PinataLegacyAdapter` Pinata 1.x Legacy Adapter Interface Matches Pinata SDK 1.x API exactly | Property | Type | Description | |----------|------|-------------| | `pinFileToIPFS` | `void` | Upload a file to IPFS | | `pinJSONToIPFS` | `void` | Upload JSON data to IPFS | | `pinByHash` | `void` | Pin content by CID | | `pinList` | `void` | List pinned files | | `unpin` | `void` | Unpin content by CID | | `hashMetadata` | `void` | Update pin metadata | | `createSignedURL` | `void` | Create signed URL for private IPFS files | | `pinJobs` | `void` | Get pin jobs | | `topUsageAnalytics` | `void` | Get top usage analytics | | `dateIntervalAnalytics` | `void` | Get date interval analytics | | `swapCid` | `void` | Swap CID | | `swapHistory` | `void` | Get swap history | ### `PinataUploadBuilder` Upload builder interface | Property | Type | Description | |----------|------|-------------| | `name` | `void` | | | `keyvalues` | `void` | | | `execute` | `void` | | ### `PinataFilterFiles` Filter files builder interface | Property | Type | Description | |----------|------|-------------| | `name` | `void` | | | `group` | `void` | | | `cid` | `void` | | | `mimeType` | `void` | | | `order` | `void` | | | `limit` | `void` | | | `cidPending` | `void` | | | `keyvalues` | `void` | | | `noGroup` | `void` | | | `pageToken` | `void` | | | `then` | `void` | | | `all` | `void` | | | `[asyncIterator]` | `void` | | ### `PinataFilterQueue` Filter queue builder interface | Property | Type | Description | |----------|------|-------------| | `cid` | `void` | | | `status` | `void` | | | `pageLimit` | `void` | | | `pageToken` | `void` | | | `sort` | `void` | | | `then` | `void` | | | `all` | `void` | | | `[asyncIterator]` | `void` | | ### `PinataFilterGroups` Filter groups builder interface | Property | Type | Description | |----------|------|-------------| | `name` | `void` | | | `limit` | `void` | | | `pageToken` | `void` | | | `isPublic` | `void` | | | `then` | `void` | | | `all` | `void` | | | `[asyncIterator]` | `void` | | ### `PinataPublicUpload` Public upload interface | Property | Type | Description | |----------|------|-------------| | `file` | `void` | | | `fileArray` | `void` | | | `base64` | `void` | | | `url` | `void` | | | `json` | `void` | | | `cid` | `void` | | | `createSignedURL` | `void` | | ### `PinataPrivateUpload` Private upload interface | Property | Type | Description | |----------|------|-------------| | `file` | `void` | | | `fileArray` | `void` | | | `base64` | `void` | | | `url` | `void` | | | `json` | `void` | | | `cid` | `void` | | | `createSignedURL` | `void` | | ### `PinataPublicFiles` Public files interface | Property | Type | Description | |----------|------|-------------| | `list` | `void` | | | `get` | `void` | | | `delete` | `void` | | | `update` | `void` | | | `addSwap` | `void` | | | `getSwapHistory` | `void` | | | `deleteSwap` | `void` | | | `queue` | `void` | | | `deletePinRequest` | `void` | | ### `PinataPrivateFiles` Private files interface | Property | Type | Description | |----------|------|-------------| | `list` | `void` | | | `get` | `void` | | | `delete` | `void` | | | `update` | `void` | | | `addSwap` | `void` | | | `getSwapHistory` | `void` | | | `deleteSwap` | `void` | | | `queue` | `void` | | | `deletePinRequest` | `void` | | ### `PinataPublicGateways` Public gateways interface | Property | Type | Description | |----------|------|-------------| | `get` | `void` | | | `convert` | `void` | | ### `PinataPrivateGateways` Private gateways interface | Property | Type | Description | |----------|------|-------------| | `get` | `void` | | | `createAccessLink` | `void` | | ### `PinataPublicGroups` Public groups interface | Property | Type | Description | |----------|------|-------------| | `create` | `void` | | | `list` | `void` | | | `get` | `void` | | | `addFiles` | `void` | | | `removeFiles` | `void` | | | `update` | `void` | | | `delete` | `void` | | ### `PinataPrivateGroups` Private groups interface | Property | Type | Description | |----------|------|-------------| | `create` | `void` | | | `list` | `void` | | | `get` | `void` | | | `addFiles` | `void` | | | `removeFiles` | `void` | | | `update` | `void` | | | `delete` | `void` | | ### `PinataAnalytics` Analytics interface | Property | Type | Description | |----------|------|-------------| | `requests` | `void` | | | `bandwidth` | `void` | | ### `PinataAdapter` Pinata 2.x Adapter Interface Matches Pinata SDK 2.x API exactly | Property | Type | Description | |----------|------|-------------| | `config` | `PinataV2Config` | Configuration | | `updateConfig` | `void` | Update the adapter configuration | | `upload` | `object` | Upload with public/private separation | | `files` | `object` | Files with public/private separation | | `gateways` | `object` | Gateways with public/private separation | | `groups` | `object` | Groups with public/private separation | | `analytics` | `PinataAnalytics` | Analytics (no public/private) | ### `IpnsClientOptions` | Property | Type | Description | |----------|------|-------------| | `signal` | `AbortSignal` | *optional* | ### `WebsitesClientOptions` | Property | Type | Description | |----------|------|-------------| | `signal` | `AbortSignal` | *optional* | ### `WatchOptions` | Property | Type | Description | |----------|------|-------------| | `interval` | `number` | *optional* | | `timeout` | `number` | *optional* | ### `SSLCallbacks` | Property | Type | Description | |----------|------|-------------| | `onReady` | `object` | *optional* | | `onError` | `object` | *optional* | | `onStatus` | `object` | *optional* | ### `SSLWatcher` | Property | Type | Description | |----------|------|-------------| | `start` | `void` | | | `stop` | `void` | | ### `SSLError` | Property | Type | Description | |----------|------|-------------| | `type` | `timeout` | `error` | | | `details` | `string` | *optional* | ### `UnstorageBlockstoreOptions` | Property | Type | Description | |----------|------|-------------| | `storage` | `Storage` | Unstorage instance to use (bypasses driver creation) *optional* | | `prefix` | `string` | Key prefix for blockstore keys *optional* | | `driver` | `Driver` | Unstorage driver name or instance *optional* | | `base` | `string` | Base path for storage driver *optional* | | `datastorePrefix` | `string` | Prefix for datastore keys (defaults to prefix) *optional* | ### `PinnerConfig` | Property | Type | Description | |----------|------|-------------| | `jwt` | `string` | JWT authentication token. Required for all API operations. | | `endpoint` | `string` | API endpoint URL. Defaults to the official pinning service. *optional* | | `gateway` | `string` | IPFS gateway URL for content retrieval. *optional* | | `allowedFileTypes` | `string[]` | Allowed MIME types for upload. If undefined, all types allowed. *optional* | | `fetch` | `object` | Custom fetch implementation. *optional* | | `datastore` | `Datastore` | Custom datastore instance for Helia. If provided, this datastore will be used directly without creating one from storage. Highest priority - takes precedence over storage and datastoreName. *optional* | | `storage` | `Storage` | Custom storage instance for both Helia blockstore and datastore. If provided, this storage will be used instead of creating default storage. The storage instance must implement the unstorage Storage interface. Used when datastore is not provided. *optional* | | `datastoreName` | `string` | Custom base name for Helia storage. Passed as the base option to both blockstore and datastore storage instances. Only used when neither datastore nor storage are provided. *optional* | | `timeout` | `number` | Upload request timeout in milliseconds. Applied to XHR uploads. TUS does not expose a timeout option. *optional* | | `retries` | `number` | Number of retry attempts for failed uploads. Applied to XHR uploads (TUS uses retryDelays instead). *optional* | ### `AbortOptions` Options that can be passed to abort async operations | Property | Type | Description | |----------|------|-------------| | `signal` | `AbortSignal` | AbortSignal to cancel the operation *optional* | ### `RemoteAddOptions` Allows passing extra options accepted by the remote pinning service | Property | Type | Description | |----------|------|-------------| | `signal` | `AbortSignal` | AbortSignal to cancel the operation *optional* | | `name` | `string` | Name for the pin or filter *optional* | | `metadata` | `Record` | Key-value metadata to attach to the pin *optional* | | `origins` | `string[]` | Origin addresses for the pinned content *optional* | ### `RemoteLsOptions` Allows passing extra options accepted by the remote pinning service | Property | Type | Description | |----------|------|-------------| | `signal` | `AbortSignal` | AbortSignal to cancel the operation *optional* | | `name` | `string` | Name for the pin or filter *optional* | | `status` | `Status[]` | Current status (queued, pinning, pinned, failed) *optional* | | `limit` | `number` | Maximum number of results per page *optional* | | `cursor` | `string` | Pagination cursor for listing results *optional* | ### `RemotePin` Includes extra metadata supported by the remote pinning service | Property | Type | Description | |----------|------|-------------| | `cid` | `CID` | IPFS Content Identifier | | `name` | `string` | Name for the pin or filter *optional* | | `status` | `Status` | Current status (queued, pinning, pinned, failed) | | `created` | `Date` | ISO timestamp of creation | | `size` | `number` | Size in bytes *optional* | | `metadata` | `Record` | Key-value metadata to attach to the pin *optional* | ### `RemotePins` Extends the Pins interface with remote pinning-specific arguments and return types (e.g. metadata as `Record` and pins with an added `.status` property) | Property | Type | Description | |----------|------|-------------| | `add` | `void` | Pin a block in the blockstore. It will not be deleted when garbage collection is run. | | `ls` | `void` | List all blocks that have been pinned. | | `isPinned` | `void` | Return true if the passed CID is pinned | | `get` | `void` | Return pin details | | `setMetadata` | `void` | Update pin metadata | | `rm` | `void` | Remove a pin. The block may be deleted when garbage collection is run. | | `rmByRequestId` | `void` | Remove a pin by request ID. The block may be deleted when garbage collection is run. | ### `UploadResult` | Property | Type | Description | |----------|------|-------------| | `id` | `string` | Unique identifier for this upload operation. | | `cid` | `string` | IPFS Content Identifier for the uploaded content. Undefined when the CID is not yet available (e.g. TUS uploads where the CID is assigned asynchronously — poll GET /api/upload/result/{'{id}'}). *optional* | | `name` | `string` | User-provided or auto-generated name for the content. | | `size` | `number` | Total size in bytes. | | `mimeType` | `string` | MIME type of the content. | | `createdAt` | `Date` | ISO timestamp when content was created. | | `numberOfFiles` | `number` | Number of files (1 for single file, N for directory). | | `keyvalues` | `Record` | Custom key-value metadata. *optional* | | `isDirectory` | `boolean` | Whether this upload is a directory. *optional* | | `isCarFile` | `boolean` | Whether this upload is already a valid CAR file. If true, the upload system will skip CAR preprocessing and upload the file as-is. Useful for passthrough of pre-generated CAR files. *optional* | | `operationId` | `number` | Portal operation ID for tracking the pinning operation. Can be used with waitForOperation() to wait for the operation to complete. *optional* | | `[UploadResultSymbol]` | `true` | Symbol brand for type checking - used to identify UploadResult objects *optional* | ### `UploadOptions` | Property | Type | Description | |----------|------|-------------| | `name` | `string` | Name for the uploaded content. *optional* | | `keyvalues` | `Record` | Custom key-value metadata. *optional* | | `onProgress` | `object` | Progress callback invoked during upload. *optional* | | `onComplete` | `object` | Callback invoked when upload completes successfully. *optional* | | `onError` | `object` | Callback invoked when upload fails. *optional* | | `signal` | `AbortSignal` | AbortSignal for cancellation. *optional* | | `size` | `number` | Optional size override for the upload input. Useful for ReadableStream inputs where size detection is difficult. *optional* | | `isDirectory` | `boolean` | Whether this upload is a directory. *optional* | | `isCarFile` | `boolean` | Whether this upload is already a valid CAR file. If true, the upload system will skip CAR preprocessing and upload the file as-is. Useful for passthrough of pre-generated CAR files. *optional* | | `waitForOperation` | `boolean` | Whether to wait for the pinning operation to complete/fail. When true, the upload will block until the operation reaches a settled state (completed, failed, or error). Default is false (upload only). *optional* | | `operationPollingOptions` | `any` | Polling options for waiting on operation completion. Only used when waitForOperation is true. *optional* | ### `UploadProgress` | 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. *optional* | | `eta` | `number` | Estimated time remaining in seconds. *optional* | ### `UploadOperation` | Property | Type | Description | |----------|------|-------------| | `cancel` | `void` | Cancel the ongoing upload. | | `pause` | `void` | Pause the upload (TUS only). | | `resume` | `void` | Resume a paused upload (TUS only). | | `result` | `Promise` | Promise that resolves when upload completes. | | `progress` | `Readonly` | Current progress. | ### `PinnerUploadBuilder` Builder interface for Pinner upload API. Supports chaining name/keyvalues and returns UploadOperation with controls. | Property | Type | Description | |----------|------|-------------| | `name` | `void` | Set the name for the upload. | | `keyvalues` | `void` | Set custom key-value metadata. | | `waitForOperation` | `void` | Whether to wait for the pinning operation to complete/fail. When true, the upload will block until the operation reaches a settled state. | | `operationPollingOptions` | `void` | Set polling options for waiting on operation completion. Only used when waitForOperation is true. | | `pin` | `void` | Start the upload and return UploadOperation with controls. | ### `UploadBuilderNamespace` Pinner upload builder namespace. Provides fluent API for file, JSON, Base64, and URL uploads. | Property | Type | Description | |----------|------|-------------| | `file` | `void` | Upload a file. Returns a builder for chaining name/keyvalues. | | `json` | `void` | Upload JSON data (encoded as JSON file). Returns a builder for chaining name/keyvalues. | | `base64` | `void` | Upload Base64 encoded content (decoded and uploaded as file). Returns a builder for chaining name/keyvalues. | | `url` | `void` | Upload content from a URL (fetched and uploaded as file). Returns a builder for chaining name/keyvalues. | | `csv` | `void` | Upload CSV data (string, array of objects, or array of arrays). Returns a builder for chaining name/keyvalues. | | `raw` | `void` | Upload raw CAR data without preprocessing. Useful for passthrough of pre-generated CAR files. Returns a builder for chaining name/keyvalues. | | `text` | `void` | Upload text content (encoded as text file). Returns a builder for chaining name/keyvalues. | | `content` | `void` | | ### `CarPreprocessOptions` | Property | Type | Description | |----------|------|-------------| | `name` | `string` | Name for the pin or filter *optional* | | `onProgress` | `object` | Progress callback for preprocessing *optional* | | `signal` | `AbortSignal` | AbortSignal to cancel the operation *optional* | ### `CarPreprocessResult` | Property | Type | Description | |----------|------|-------------| | `carStream` | `ReadableStream>` | Preprocessed CAR data as a readable stream | | `rootCid` | `string` | Root CID of the CAR file | | `size` | `bigint` | Size in bytes | ## Type Aliases ### `FileManagerItemResponse` ### `BlockMetaResponse` ### `InfoResponse` ### `PinStatusResponse` ### `PinStatusResponseInfo` ### `PinRequest` ### `PinRequestMeta` ### `PinResultsResponse` ### `ErrorResponse` ### `Multiaddr` ### `PostUploadResponse` ### `UploadResultResponse` ### `Component` ### `GatewayWebsiteResponse` ### `GatewayWebsiteStatusResponse` ### `GetBlockMetaBatchRequest` ### `IPNSKeyRequest` ### `IPNSKeyListResponseResponse` ### `IPNSKeyListResponse` ### `IPNSRepublishResponse` ### `IPNSKeyResponse` ### `IPNSPublishRequest` ### `IPNSPublishResponse` ### `IPNSResolveResponse` ### `PostApiUploadBody` ### `SSLStatusUpdateRequest` ### `WebsiteItemResponse` ### `WebsiteRequest` ### `WebsiteResponse` ### `WebsiteItem` ### `WebsiteUpdateRequest` ### `WebsiteConfigResponse` ### `SSLStatusInfo` ### `WebsiteValidateResponse` # API Reference Pinner has two APIs, each with its own Swagger UI for interactive exploration. ## IPFS Plugin API The primary API for IPFS pinning, uploads, websites, DNS, and IPNS. Served at `ipfs.pinner.xyz`. [**Open in Swagger UI →**](https://ipfs.pinner.xyz/swagger/) The Pinning endpoints (`/pins`) are compatible with the [IPFS Pinning Service API specification](https://ipfs.github.io/pinning-services-api-spec/). ## Meta API Portal metadata, plugin bundles, and authentication redirect. Served at `pinner.xyz`. [**Open in Swagger UI →**](https://pinner.xyz/swagger/) ## Authentication All IPFS API endpoints require a JWT token obtained from the Portal authentication service, passed in the `Authorization` header: ``` Authorization: Bearer ``` See [API Keys](/account/api-tokens) for key management. ## Using the SDK instead of raw HTTP The [Pinner SDK](/ipfs/sdk/getting-started) wraps authentication, retries, progress events, and TUS resumable uploads. For most use cases, the SDK is the recommended way to interact with the API; no need to integrate TUS libraries or manage JWT tokens yourself. If you need a type-safe client without the SDK, generate one from the [OpenAPI spec](https://ipfs.pinner.xyz/swagger.json) using tools like [Orval](https://orval.dev/) or [openapi-typescript](https://openapi-ts.pages.dev/). # Error Reference Every API error response includes a `reason` field for programmatic handling. This page lists every code the API can return, when it happens, what to do about it, and whether it's safe to retry. CLI users see friendly messages instead of raw codes — the [CLI error table](#cli-error-messages) maps them. ## Error response format ```json { "error": { "reason": "UNAUTHORIZED", "details": "Access denied. Please check your credentials and try again." } } ``` | Field | Type | Description | |-------|------|-------------| | `reason` | `string` | Error code for programmatic handling | | `details` | `string` | Human-readable description | ## Authentication (401 / 403) | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `UNAUTHORIZED` | 401 | API key is invalid, expired, or revoked | Run `pinner auth` to re-authenticate. Check `PINNER_API_KEY` env var. | No | | `PERMISSION_DENIED` | 403 | You don't have access to this resource | Check your plan limits. Contact support if you think this is wrong. | No | ## Upload | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `UnsupportedFormat` | 422 | File format isn't supported | Use a supported format (ZIP or CAR). | No | | `CorruptedFile` | 500 | File is corrupted | Re-export or re-download the file and retry. | No | | `EmptyZIP` | 500 | ZIP archive has no files | Add content to the ZIP and retry. | No | | `PasswordProtected` | 500 | File is password-protected | Remove password protection and retry. | No | | `FileUploadFailed` | 500 | Upload failed mid-transfer | Check network and retry with backoff. | Yes | | `FILE_UPLOAD_API_FAILED` | 500 | Upload failed due to an internal error | Retry with backoff. If it persists, check status.pinner.xyz. | Yes | | `FILE_PROCESSING_FAILED` | 500 | Server couldn't process the file | Check file integrity. Retry with backoff. | Yes | | `UPLOAD_NOT_FOUND` | 404 | Upload ID doesn't exist | Check the upload ID. It may have expired or been deleted. | No | | `BLOCK_NOT_FOUND` | 404 | Requested block doesn't exist | The content may not be fully pinned yet. Wait and retry. | Yes | | `CID_PARSE_FAILED` | 400 | CID format is invalid | Check the CID string for typos or encoding issues. | No | ## Pin | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `PIN_FETCH_FAILED` | 500 | Couldn't retrieve pin info | Retry with backoff. If it persists, the pin may not exist. | Yes | | `INVALID_CID` | 422 | CID format is invalid or unsupported | Verify the CID. See [CIDs](/ipfs/concepts/cids). | No | | `INVALID_TARGET` | 422 | Target hash or peer ID is invalid | Check the target value. | No | ## Quota (429) | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `UPLOAD_QUOTA_EXCEEDED` | 429 | Too many uploads in a period | Wait and retry. Reduce upload frequency or upgrade your plan. | Yes (after cooldown) | | `STORAGE_QUOTA_EXCEEDED` | 429 | Storage limit reached | Delete unused pins or upgrade your plan. | No | | `DOWNLOAD_QUOTA_EXCEEDED` | 429 | Too many downloads in a period | Wait and retry. Reduce download frequency or upgrade your plan. | Yes (after cooldown) | ## Website | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `WEBSITE_NOT_FOUND` | 404 | Website doesn't exist | Check the website ID. See [Websites](/websites/concepts/how-hosting-works). | No | | `INVALID_WEBSITE_STATUS` | — | Website status value isn't valid | Check the status you're setting. | No | | `INVALID_TARGET_TYPE` | — | Target type isn't valid | Must be `ipfs` or `ipns`. | No | | `INVALID_TIMESTAMP` | — | Timestamp format is invalid | Use ISO 8601 format. | No | ## SSL | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `INVALID_SSL_STATUS` | — | SSL status value isn't valid | Check the status you're setting. | No | | `SSL_STATUS_UPDATE_FAILED` | — | Couldn't update SSL status | Check domain ownership. Retry with backoff. | Yes | ## DNS | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `INVALID_DOMAIN` | 400 | Domain name is invalid | Check domain format. Must be a valid FQDN. | No | | `INVALID_DOMAIN_FORMAT` | 400 | Domain format isn't supported | Use a standard domain format. | No | | `INVALID_ZONE_STATUS` | — | DNS zone status is invalid | Check the status value. | No | | `ZONE_NOT_FOUND` | 404 | DNS zone doesn't exist | Create the zone first. See [DNS and Domains](/websites/concepts/dns-and-domains). | No | | `RECORD_NOT_FOUND` | 404 | DNS record doesn't exist | Check the record ID. | No | | `INVALID_RECORD_TYPE` | — | DNS record type isn't supported | Use a supported record type (A, AAAA, ALIAS, CNAME, TXT, MX, NS, SRV). | No | | `DUPLICATE_RECORD` | 409 | DNS record already exists | Use the existing record or delete it first. | No | | `DELETE_FAILED` | 500 | Couldn't delete DNS zone | Retry with backoff. | Yes | | `UPDATE_FAILED` | 500 | Couldn't update DNS zone | Retry with backoff. | Yes | | `VALIDATION_FAILED` | 500 | DNS validation failed | Check nameserver configuration. Retry after fixing. | Yes | ## Validation | Code | HTTP | When it happens | What to do | Retry? | |------|------|----------------|------------|--------| | `INVALID_REQUEST` | 422 | Request parameter is invalid | Check the API docs for required parameters. | No | | `INVALID_IDENTIFIER` | 422 | ID format is invalid | Check the identifier format. | No | | `INVALID_UUID_FORMAT` | 400 | UUID format is invalid | Use standard UUID format (e.g. `550e8400-e29b-41d4-a716-446655440000`). | No | | `METADATA_FETCH_FAILED` | 500 | Couldn't fetch metadata | Retry with backoff. | Yes | ## CLI error messages The CLI translates API and SDK errors into friendly messages. If you see one of these, here's what it means: | CLI message | Cause | What to do | |-------------|-------|------------| | `not authenticated` | No API key configured | Run `pinner auth` | | `path is required` | No file path given | Provide a file path argument | | `CID is required` | No CID argument given | Provide a CID argument | | `invalid CID` | CID format is wrong | Check the CID string | | `no CIDs provided` | Empty CID list | Provide at least one CID | | `file not found` | File doesn't exist locally | Check the file path | | `directory not found` | Directory doesn't exist locally | Check the directory path | | `permission denied` | Can't read the file | Check file permissions | | `pin not found` | Pin doesn't exist on Pinner | Check the CID — it may not be pinned | | `pinning failed` | Pin operation failed on the server | Retry. If it persists, check status.pinner.xyz | | `upload failed` | Upload didn't complete | Check the error details. Retry with backoff. | | `upload interrupted` | You pressed Ctrl+C or the network dropped | The CLI supports resuming — just retry the same command | | `network timeout` | Server didn't respond in time | Check your connection. Retry with backoff. | | `connection failed` | Can't reach Pinner servers | Check your internet connection and firewall | | `configuration not found` | No config file | Run `pinner setup` | | `configuration invalid` | Config file is malformed | Run `pinner setup --reset` | | `service unavailable` | Server is temporarily down | Retry after a few minutes. Check status.pinner.xyz | ## Retry strategy Some errors are safe to retry. Use exponential backoff — start at 1 second, double each attempt: * **Always retry**: 429 (quota exceeded — wait for cooldown), 500 (server error — temporary) * **Never retry**: 400, 401, 403, 404, 409, 410, 422 (client error — you must fix the request) ```typescript import { 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; } await new Promise(r => setTimeout(r, 1000 * attempt)); } } throw lastError; } ``` # Private Storage > **Coming soon.** Private Storage is not yet available. [Join the waitlist](https://pinner.xyz) to be notified when it launches. This page will be updated with documentation once Private Storage is available. ## Want early access? Reach out at [support@pinner.xyz](mailto:support@pinner.xyz). # S3 Storage > **Coming soon.** S3 Storage is not yet available. [Join the waitlist](https://pinner.xyz) to be notified when it launches. This page will be updated with documentation once S3 Storage is available. ## Want early access? Reach out at [support@pinner.xyz](mailto:support@pinner.xyz). # Quickstart Deploy a static site to IPFS and make it accessible via a custom domain — all in under 5 minutes. You need a Pinner account and an API key. Don't have one? [Create an API key](/account/api-tokens). :::code-group ```bash [CLI] # 1. Authenticate (interactive — prompts for email and password) pinner auth # 2. Build your site (example with Vite) npm run build # 3. Deploy pinner website deploy ./dist # Done — your site is live on IPFS ``` ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY }); // Create a website pointing to your Content Identifier (CID) const site = await pinner.websites.createWebsite({ domain: "mysite.pinner.xyz", target_type: "ipfs", target_hash: "QmYourCIDHere", }); console.log("Site ID:", site.id); console.log("Domain:", site.domain); ``` ::: ### Non-interactive (CI/CD) If you already have an API key from another machine or for automation: ```bash pinner auth YOUR_API_KEY ``` ## What just happened The CLI uploaded your `./dist` directory to IPFS and created a website record that maps a domain to that content. The SDK does the same thing — you give it a domain and a CID, and Pinner handles the rest. ## Next steps * [Deploy a static site end-to-end](/websites/tutorials/deploy-static-site) — a full walkthrough with custom domains * [Add a custom domain](/websites/how-to/custom-domains) * [Understand how website hosting works](/websites/concepts/how-hosting-works) # Migrate from Pinata SDK Replace Pinata SDK calls with Pinner adapter calls. The adapter mirrors the Pinata SDK interface so you can migrate with minimal code changes. For CLI migration, see [Migrate from Pinata CLI](/migration/pinata). ## Install the Pinner SDK ```bash npm install @lumeweb/pinner ``` ## Replace initialization :::code-group ```typescript [Pinata SDK 2.x → Pinner] // Before (Pinata) import { PinataSDK } from "pinata-web3"; const pinata = new PinataSDK({ pinataJwt: process.env.PINATA_JWT!, }); // After (Pinner adapter) import { Pinner, pinataAdapter } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pinata = pinataAdapter(pinner); ``` ```typescript [Pinata SDK 1.x → Pinner] // Before (Pinata) const pinata = require("@pinata/sdk")(pinataJwt, pinataGateway); // After (Pinner legacy adapter) import { Pinner, pinataLegacyAdapter } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const pinata = pinataLegacyAdapter(pinner); ``` ::: ## Replace API calls The adapter methods match the Pinata SDK, so most calls work without changes: :::code-group ```typescript [Upload a file] // Pinata SDK 2.x Pinner adapter pinata.upload.public.file(file) → pinata.upload.public.file(file) pinata.upload.public.json(data) → pinata.upload.public.json(data) pinata.upload.public.cid(cid) → pinata.upload.public.cid(cid) ``` ```typescript [Manage files] // Pinata SDK 2.x Pinner adapter pinata.files.public.list() → pinata.files.public.list() pinata.files.public.get(id) → pinata.files.public.get(id) pinata.files.public.delete([cid]) → pinata.files.public.delete([cid]) ``` ::: :::tip The v2 adapter uses a builder pattern. Chain methods and call `.execute()`: ```typescript const upload = await pinata.upload.public .file(file) .name("my-file.txt") .keyvalues({ project: "demo" }) .execute(); ``` ::: ## Unsupported features Some Pinata features don't have Pinner equivalents: | Feature | Status | Alternative | |---------|--------|-------------| | Private uploads | Not supported | Use Pinner's native API | | Groups | Not supported | Use key-value metadata | | Signed URLs | Not supported | Not available | | Swap CID | Not supported | Not available | For the full adapter reference, see [Pinata Migration Examples](/migration/pinata/examples). ## Next steps * [Pinata Migration Examples](/migration/pinata/examples) — complete API mapping * [Code Examples](/migration/pinata/examples) — side-by-side comparisons * [SDK Getting Started](/ipfs/sdk/getting-started) — for when you're ready to go beyond the adapter # Your First Upload with the SDK In this tutorial you'll upload a file to IPFS using the Pinner SDK and get back its CID. Need to install the SDK first? See [Getting Started with the SDK](/ipfs/sdk/getting-started). ## Set up the client Create a Pinner instance with your API key: :::code-group ```typescript [Browser] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: "YOUR_API_KEY" }); ``` ```typescript [Node.js] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); ``` ::: ## Create or pick a file :::code-group ```typescript [Browser] // Create a file from user input or construct one const file = new File(["Hello, IPFS!"], "hello.txt", { type: "text/plain", }); ``` ```typescript [Node.js] import fs from "fs"; const buffer = fs.readFileSync("./hello.txt"); const file = new File([buffer], "hello.txt", { type: "text/plain" }); ``` ::: ## Upload and get the CID :::code-group ```typescript [Browser] const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ```typescript [Node.js] const result = await pinner.uploadAndWait(file); console.log("CID:", result.cid); ``` ::: `uploadAndWait` handles the full cycle: it uploads the file, waits for pinning to complete, and returns the result with your CID. ## That's it Your file is now pinned on IPFS. The CID is a permanent, content-addressed identifier — anyone with this CID can retrieve the file from the IPFS network. ### What's next? * [Upload a whole directory](/ipfs/tutorials/upload-directory) * [Check the status of your pin](/ipfs/how-to/check-pin-status) * [Understand what a CID is](/ipfs/concepts/cids) # Upload CAR Files CAR (Content Addressable Archive) files let you upload IPFS data with the CID already known before the upload even starts. This is useful for bulk uploads and when you need to guarantee the CID ahead of time. Not sure what CAR files are or why you'd use them? See [CAR Files](/ipfs/concepts/car-files). ## Upload a CAR file :::code-group ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const carData = fs.readFileSync("./data.car"); const carFile = new File([carData], "data.car", { type: "application/vnd.ipld.car", }); const result = await pinner.uploadCar(carFile); console.log("Root CID:", result.cid); ``` ```bash [CLI] pinner upload --car data.car ``` ::: ## Why CAR instead of a regular upload? With a CAR file the root CID is deterministic — it's computed from the IPFS block structure before the upload begins. You can verify the CID locally and know it won't change after pinning. Regular uploads also work fine. CAR is for cases where you need the CID upfront: deterministic builds, verification pipelines, or bulk transfers of pre-packaged IPFS data. ## Next steps * [Learn how CAR files work](/ipfs/concepts/car-files) * [Upload a directory](/ipfs/tutorials/upload-directory) # Upload a Directory In this tutorial you'll upload multiple files as an IPFS directory and get the root CID that addresses the whole structure. ## What you'll learn * How to group files into an IPFS directory upload * What the "root CID" is and how to find it * How to verify the upload worked ## Upload the directory :::code-group ```typescript [SDK] import { Pinner } from "@lumeweb/pinner"; const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN! }); const files = [ new File(["Hello, IPFS!"], "hello.txt", { type: "text/plain" }), new File(['{"name": "config"}'], "config.json", { type: "application/json", }), ]; const result = await pinner.uploadAndWait(files); console.log("Root CID:", result.cid); ``` ```bash [CLI] # Upload an entire directory pinner upload ./my-directory/ # The root CID is printed to the terminal ``` ::: ## What is the root CID? When you upload a directory, IPFS creates a tree of CIDs — one for each file, plus intermediate nodes. The **root CID** addresses the top of that tree. With the root CID alone you can reach every file inside the directory. ## Verify the upload You can check that the content is reachable through any public gateway: ``` https://ipfs.io/ipfs//hello.txt https://dweb.link/ipfs//config.json ``` Or check the pin status: :::code-group ```typescript [SDK] const status = await pinner.pins.getStatus(result.cid); console.log(status); ``` ```bash [CLI] pinner status ``` ::: ## Next steps * [Upload CAR files for pre-calculated CIDs](/ipfs/tutorials/upload-car) * [Understand how CIDs work](/ipfs/concepts/cids) # Code Generation from OpenAPI Generate type-safe TypeScript clients from Pinner's OpenAPI specification using [Orval](https://orval.dev/). ## Setup Install Orval: ```bash npm install -D orval ``` Create `orval.config.ts`: ```ts import { defineConfig } from 'orval' export default defineConfig({ pinner: { input: 'https://api.pinner.xyz/openapi.json', output: { target: './src/api/pinner-client.ts', client: 'axios-functions', override: { mutator: { path: './src/api/mutator.ts', name: 'customInstance', }, }, }, }, }) ``` ## Client Options Orval supports generating clients for: | Client | Use case | |--------|----------| | `axios-functions` | General-purpose HTTP client | | `react-query` | React apps with caching | | `swr` | SWR-based React apps | | `fetch` | Lightweight, no dependencies | *** *Detailed code generation examples will be added when the OpenAPI spec is available for public consumption.* # TUS (Resumable Upload) Client Libraries For files over 100MB (the POST upload limit), use resumable uploads via the TUS protocol. Pinner supports the [TUS protocol](https://tus.io/) for reliable, resumable large file transfers. ## Official Clients | Language | Library | Install | |----------|---------|---------| | JavaScript | `tus-js-client` | `npm install tus-js-client` | | Go | [tus-go-client](https://github.com/tus/tus-go-client) | `go get github.com/tus/tus-go-client` | | Python | [tuspy](https://github.com/tus/tuspy) | `pip install tuspy` | | Java | [tus-java-client](https://github.com/tus/tus-java-client) | Maven/Gradle | ## When to Use Resumable Uploads (TUS) * Files over 100MB * Unreliable network connections (resumes after interruption) * Bulk uploads that need progress tracking The Pinner SDK uses the TUS protocol internally for large uploads. You only need these clients if you're working directly with the API. *** *Detailed TUS integration examples will be added as the API reference is generated.* # DNS and Domains DNS is the bridge between your domain name and Pinner's hosting. You configure DNS records at your registrar (or let Pinner manage DNS entirely), and Pinner's gateway serves your IPFS content when visitors hit your domain. ## Two ways to handle DNS ### Bring your own DNS You keep your existing DNS provider and add records that point to Pinner: | Record | Purpose | Value | |--------|---------|-------| | TXT | Domain validation | `verify=` | | CNAME | Route traffic to Pinner | `sites.pinner.xyz` | | A | Root domain routing | IP from website config | This is the most common approach. You keep control of your DNS and just add a few records. ### Pinner-managed DNS When you enable `dns_hosting_enabled` on a website, Pinner creates a DNS zone for your domain. You then delegate your domain's nameservers to Pinner at your registrar. Pinner handles all DNS records — validation, CNAME, and any custom records you need. The tradeoff: less manual work, but you give Pinner full DNS control for that domain. To find Pinner's nameservers for delegation, retrieve the website configuration. The `nameservers` field in the configuration response lists the nameservers to set at your registrar. ## DNS records for website hosting ### Validation TXT record Before Pinner serves your domain, you must prove you own it. Pinner assigns a `validation_token` to each website. Add a TXT record: ``` _pinner-validation.yourdomain.com TXT "verify=abc123def456" ``` The exact hostname comes from the `validation_record_host` field on the website. After adding the record, trigger validation and Pinner checks that the token matches. ### CNAME record (subdomain) For `www.yourdomain.com` or other subdomains: ``` www CNAME sites.pinner.xyz ``` ### A record (root domain) Root domains (`yourdomain.com`) can't use CNAME at most registrars. Use an A record instead: ``` @ A ``` ### Nameserver delegation (Pinner-managed DNS) If you're using Pinner's DNS hosting, update your domain registrar to use Pinner's nameservers. The nameservers are available from the website configuration. Once propagated, Pinner controls all DNS for your domain. ## DNS propagation DNS changes aren't instant. Typical propagation times: | Change | Typical time | |--------|-------------| | New records | 1–5 minutes | | Modified records | 5–30 minutes | | Nameserver changes | 1–48 hours | Pinner's SSL provisioning starts only after DNS has propagated. If SSL stays in `pending`, your DNS records may not have propagated yet. ## DNS hosting setup flow 1. Enable `dns_hosting_enabled` on your website 2. Pinner creates a DNS zone with the necessary records 3. Change your domain's nameservers at your registrar to Pinner's nameservers (from website configuration) 4. Wait for nameserver propagation 5. Pinner validates the domain automatically 6. SSL provisions See [Manage DNS Zones](/dns/how-to/manage-zones) and [Manage DNS Records](/dns/how-to/manage-records) for CLI commands. # How Website Hosting Works Pinner hosts static websites on IPFS and makes them accessible through regular domain names with HTTPS. No servers to manage, no containers to configure — just upload your files and point a domain at them. ## The pieces Website hosting on Pinner involves three systems working together: 1. **IPFS storage** — Your site's files are pinned on IPFS and addressed by Content Identifier (CID) 2. **DNS** — Maps your domain name to the IPFS content 3. **SSL** — Provides HTTPS for your domain automatically ## The flow ``` Build output → Upload to IPFS → Get Content Identifier (CID) → Create website record → Configure DNS → SSL provisions → Site is live ``` Here's what happens at each step: ### Upload Your static files (HTML, CSS, JS, images) get uploaded to IPFS as a directory. The directory gets a Content Identifier (CID) — a content-addressed identifier that represents your entire site. ### Website record A website record on Pinner maps a domain to IPFS content. It stores: * The **domain** name (e.g. `mysite.example.com`) * The **target** — either a CID (direct IPFS) or an IPNS name (updatable) * The **validation token** used to prove you own the domain * The **SSL status** tracking certificate provisioning ### DNS For Pinner to serve your domain, DNS must point at Pinner's infrastructure. You can either: * **Bring your own DNS** — Add CNAME/A records at your registrar pointing to Pinner * **Use Pinner-managed DNS** — Delegate the DNS zone to Pinner's nameservers Either way, the result is the same: traffic to your domain reaches Pinner's gateway, which resolves the CID and serves your content. ### SSL Once DNS is in place, Pinner automatically provisions an SSL certificate for your domain. You don't need to buy, install, or renew certificates. Pinner handles the entire lifecycle. When a visitor hits `https://mysite.example.com`, the request flows through Pinner's HTTPS gateway, which fetches the content from IPFS using the CID in the website record and serves it to the browser. ## Content addressing vs traditional hosting | Traditional hosting | Pinner website hosting | |---------------------|----------------------| | Files live on a specific server | Files live on IPFS — a distributed network | | Server URL identifies the content | CID identifies the content | | Updating means replacing files on the server | Updating means uploading new files and pointing the domain at the new CID | | Server downtime = site downtime | No single server to go down | ## Subdomains If you just want to get something live quickly, Pinner provides `*.pinner.xyz` subdomains. No DNS configuration needed — the subdomain is set up automatically when you create the website. Custom domains require DNS validation and manual DNS configuration, but give you full branding control. ## Updating your site With direct IPFS targeting, you update content by uploading new files and updating the website's `target_hash` to the new CID. The old CID stays on IPFS until you unpin it. With IPNS targeting, you publish the new CID under the same IPNS name. The domain resolves the IPNS name, which now points at the new CID. This is more convenient for frequently updated sites. See [IPNS](/websites/concepts/ipns) for more on the IPNS approach. # IPNS IPNS (InterPlanetary Name System) is a naming system for IPFS. It lets you associate a stable name with a Content Identifier (CID), and update that association over time. For website hosting, this means you can update your site's content without changing the domain configuration. ## The problem IPNS solves IPFS uses content addressing — a CID is derived from the content itself. Changed content means a new CID. This is a strength (integrity) but a challenge for websites: every time you update your site, the CID changes. Without IPNS, updating a website looks like this: 1. Upload new content → get new CID 2. Update the website record to point at the new CID 3. Wait for DNS to propagate (if applicable) With IPNS, step 2 goes away: 1. Upload new content → get new CID 2. Republish the IPNS name to point at the new CID 3. The domain still points at the same IPNS name, which now resolves to the new CID ## How IPNS works on Pinner ### Keys An IPNS key is a cryptographic keypair. The public key becomes the IPNS name (a long string starting with `k51...`). You create keys on Pinner, and each key gets an ID you use in API calls. When you create a key, Pinner returns: * `ipns_name` — the public IPNS name (e.g. `k51qzi5...`) * `id` — the numeric ID used in subsequent API calls ### Publishing Publishing associates an IPNS name with a CID. You specify the key ID and the target CID. The IPNS name now resolves to that CID. Later, publish again with a different CID — same IPNS name, different CID. The domain follows. ### Resolving Anyone can resolve an IPNS name to find the current CID. The resolution returns the CID that the IPNS name currently points to. ### Republishing IPNS records have a time-to-live. Republish before expiry to keep the name active. ## IPNS vs direct IPFS for websites | | Direct IPFS | IPNS | |---|-----------|------| | **Update method** | Change website `target_hash` | Republish IPNS name | | **Domain config changes** | Yes, every update | No, set once | | **Resolution speed** | One hop (CID direct) | Two hops (IPNS → CID) | | **Best for** | Rarely updated sites | Frequently updated sites, CI/CD | The resolution overhead of IPNS is small — typically under a second on Pinner's infrastructure. For most sites, it's not noticeable. ## IPNS key lifecycle ``` Create key → Publish CID → (update) → Republish CID → ... → Delete key ``` * **Create**: Generate a keypair on Pinner * **Publish**: Associate the key's IPNS name with a CID * **Republish**: Update the association or extend the TTL * **Delete**: Remove the key — the IPNS name becomes unresolvable :::warning Deleting an IPNS key is permanent. Any website using that key as its target will stop resolving. Make sure no websites depend on the key before deleting it. ::: # SSL Pinner provisions and renews SSL certificates for your custom domains automatically. You don't need to buy certificates, configure web servers, or set up renewal cron jobs. ## How it works 1. You configure DNS to point your domain at Pinner 2. Pinner detects the DNS change and starts certificate provisioning 3. Pinner completes the automated certificate management challenge (domain validation via DNS or HTTP) 4. The certificate is issued and installed on Pinner's gateway 5. Your site is available over HTTPS You don't touch any of this. The entire process runs in the background. ## SSL states SSL moves through these states: ``` pending → validating → valid → ready ↘ failed ↘ error ``` | State | What's happening | |-------|-----------------| | `pending` | Waiting to start — DNS may not have propagated yet | | `validating` | Pinner is verifying domain control and requesting the certificate from the CA | | `valid` | Certificate issued and installed | | `ready` | Certificate is active and serving HTTPS traffic | | `failed` | Provisioning failed — the `error` field has details | | `error` | Unexpected error during provisioning | ## Checking status You can check SSL status through the SDK or CLI. The SDK's `getSSLStatus` method returns the current state, and the `watchSSL` method polls until the certificate is ready or an error occurs. ## Renewal Pinner renews certificates before they expire. You don't need to do anything — renewal happens automatically in the background. ## Troubleshooting ### SSL stays in `pending` DNS records haven't propagated. Verify your CNAME and validation TXT records: ```bash dig CNAME www.mysite.example.com dig TXT _pinner-validation.mysite.example.com ``` If the records are missing, add them and wait. If they're present, DNS may just need more time. ### SSL fails with `failed` or `error` Check the `error` field on the SSL status response. Common causes: * **DNS not pointing at Pinner** — The CNAME or A record is missing or incorrect * **Domain validation expired** — Re-validate the domain (see [Custom Domains](/websites/how-to/custom-domains)) * **Too many attempts** — Let's Encrypt has rate limits; wait a while and retry ### SSL works but the site isn't loading SSL is only one piece. Check that: 1. Your website's `target_hash` points at a valid, pinned CID 2. The content is actually accessible on IPFS 3. DNS is correctly pointing at Pinner's infrastructure # Check SSL Status Pinner provisions SSL certificates automatically for custom domains. You don't configure anything — just wait for DNS to propagate and Pinner handles the rest. ## Check the current status ```typescript const ssl = await pinner.websites.getSSLStatus("mysite.example.com"); console.log("Status:", ssl.status); if (ssl.error) { console.log("Error:", ssl.error); } ``` ## SSL states | Status | Meaning | |--------|---------| | `pending` | Waiting to start provisioning — DNS may not have propagated yet | | `validating` | Pinner is verifying domain control and requesting the certificate | | `valid` | Certificate issued and active | | `ready` | Certificate is active and the site is serving HTTPS | | `failed` | Provisioning failed — check `error` for details | | `error` | An unexpected error occurred during provisioning | ## Watch for SSL readiness Instead of polling manually, use the SSL watcher to get notified when the certificate is ready: ```typescript const watcher = pinner.websites.watchSSL("mysite.example.com", { interval: 5000, // check every 5 seconds timeout: 300000, // give up after 5 minutes }); await watcher.start({ onReady: (status) => { console.log("SSL is ready!"); // Safe to announce your site is live }, onStatus: (status) => { console.log("Progress:", status.status); }, onError: (error) => { console.error("SSL failed:", error.type, error.message); }, }); // Stop watching early if needed watcher.stop(); ``` ## Troubleshooting ### SSL stuck at `pending` DNS records haven't propagated yet. This is normal — it can take a few minutes to a few hours depending on your registrar. Verify your CNAME and TXT records are in place: ```bash dig CNAME www.mysite.example.com dig TXT _pinner-validation.mysite.example.com ``` ### SSL in `failed` or `error` state Check the `error` field for details. Common causes: * **DNS not pointing at Pinner** — The CNAME or A record is missing or pointing elsewhere * **Domain validation expired** — Re-validate the domain (see [Custom Domains](/websites/how-to/custom-domains)) * **Rate limit hit** — Too many SSL requests in a short time; wait and retry ### SSL watcher times out The default timeout is 5 minutes. If DNS propagation is slow, increase it: ```typescript const watcher = pinner.websites.watchSSL("mysite.example.com", { interval: 10000, // check every 10 seconds timeout: 600000, // 10 minute timeout }); ``` # Custom Domains By default, your site is available at a `*.pinner.xyz` subdomain. To use your own domain, you need to prove ownership and configure DNS. ## Subdomain vs custom domain | Type | Example | Setup | |------|---------|-------| | Subdomain | `mysite.pinner.xyz` | Automatic — no DNS needed | | Custom domain | `mysite.example.com` | Requires DNS validation and records | ## Step 1: Create the website with your domain :::code-group ```typescript [SDK] const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: "QmYourCID", }); ``` ```bash [CLI] pinner website deploy ./dist --domain mysite.example.com ``` ::: Pinner assigns a `validation_token` to the website. You'll need this in the next step. ## Step 2: Add the validation TXT record At your domain registrar, create a TXT record: | Record | Name | Value | |--------|------|-------| | TXT | `_pinner-validation.mysite.example.com` | `verify=` | The `validation_record_host` field on the website tells you the exact hostname for the TXT record. ## Step 3: Trigger validation :::code-group ```typescript [SDK] const result = await pinner.websites.validateWebsite(site.id); if (result.valid) { console.log("Domain validated!"); } else { console.log("Not yet valid:", result.reason, result.message); } ``` ```bash [CLI] pinner website validate ``` ::: DNS propagation can take a few minutes. If validation fails with `dns_missing`, wait a few minutes and retry. ## Step 4: Point DNS to Pinner Add a CNAME record to route traffic: | Record | Name | Value | |--------|------|-------| | CNAME | `www` | `sites.pinner.xyz` | For root domains (`example.com`), use an A record — check `getWebsiteConfig()` for the IP. ```typescript const config = await pinner.websites.getWebsiteConfig(); console.log("Gateway domain:", config.gateway_domain); console.log("Nameservers:", config.nameservers); ``` ## Step 5: Wait for SSL SSL provisions automatically after DNS is in place. See [SSL](/websites/concepts/ssl) for details on checking status. ## Use Pinner-managed DNS If you don't want to manage DNS at your registrar, enable Pinner's DNS hosting. This delegates DNS management to Pinner entirely. ### Enable during website creation ```typescript const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: "QmYourCID", dns_hosting_enabled: true, }); ``` ### Enable on an existing website ```typescript await pinner.websites.updateWebsite(site.id, { dns_hosting_enabled: true, }); ``` When DNS hosting is enabled, Pinner creates a DNS zone for your domain and handles the validation and CNAME records for you. Update your domain registrar's nameservers to point to Pinner's nameservers (shown in `getWebsiteConfig()`). See [Manage DNS Zones](/dns/how-to/manage-zones) and [Manage DNS Records](/dns/how-to/manage-records) for the DNS-specific commands. ## Troubleshooting ### Validation keeps failing with `dns_missing` DNS propagation can take up to 48 hours (usually a few minutes). Verify the TXT record exists: ```bash dig TXT _pinner-validation.mysite.example.com ``` ### Validation fails with `dns_mismatch` The TXT record exists but the token doesn't match. Check that you copied the `validation_token` exactly — no extra whitespace or line breaks. ### Validation fails with `token_expired` The validation token has expired. Re-create the website or update it to get a fresh token. # Deploy with GitHub Actions Deploy your static site to Pinner automatically every time you push to your repository. ## Set up secrets Add your Pinner API key as a repository secret: 1. Go to **Settings → Secrets and variables → Actions** 2. Click **New repository secret** 3. Name: `PINNER_API_KEY` 4. Value: your API key from [API Keys](/account/api-tokens) ## Create the workflow Create `.github/workflows/deploy.yml`: ```yaml name: Deploy to Pinner on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build run: npm ci && npm run build - name: Install Pinner CLI run: npm install -g @lumeweb/pinner-cli - name: Authenticate run: pinner auth ${{ secrets.PINNER_API_KEY }} - name: Deploy run: pinner website deploy ./dist ``` This workflow builds your site, authenticates with Pinner, and deploys the build output. ## Deploy with the SDK If you prefer programmatic control — for example, to update an existing website or publish via IPNS: ```yaml - name: Deploy with SDK env: PINNER_API_KEY: ${{ secrets.PINNER_API_KEY }} run: node scripts/deploy.mjs ``` ```javascript // scripts/deploy.mjs import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY }); // Upload build artifacts const files = fs.readdirSync("./dist").map((name) => { const buffer = fs.readFileSync(`./dist/${name}`); return new File([buffer], name); }); const result = await pinner.uploadAndWait(files); console.log("CID:", result.cid); // Update existing website or create a new one const siteId = Number(process.env.WEBSITE_ID); if (siteId) { await pinner.websites.updateWebsite(siteId, { target_hash: result.cid, }); console.log("Updated website", siteId); } else { const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: result.cid, }); console.log("Created website", site.id); } ``` ## Deploy with IPNS for continuous updates If your site updates frequently, use IPNS so the domain always points at the latest content: ```javascript // scripts/deploy-ipns.mjs import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY }); // Upload build artifacts const files = fs.readdirSync("./dist").map((name) => { const buffer = fs.readFileSync(`./dist/${name}`); return new File([buffer], name); }); const result = await pinner.uploadAndWait(files); // Publish to IPNS const IPNS_KEY_ID = Number(process.env.IPNS_KEY_ID); await pinner.ipns.publish({ key_id: IPNS_KEY_ID, cid: result.cid, }); console.log("Published to IPNS:", result.cid); ``` The domain resolves to the IPNS name, which always points at the latest CID. ## Next steps * [Understand IPNS publishing](/websites/concepts/ipns) * [Set up a custom domain](/websites/how-to/custom-domains) * [Monitor SSL provisioning](/websites/how-to/check-ssl) # Manage Websites List your websites, update their content, and delete sites you no longer need. ## List websites :::code-group ```bash [CLI] pinner website list ``` ```typescript [SDK] const result = await pinner.websites.listWebsites(); console.log(`You have ${result.total} website(s)`); for (const site of result.data) { console.log(`- ${site.domain} (${site.status}) → ${site.target_hash}`); } ``` ::: ## Get a single website ```typescript const site = await pinner.websites.getWebsite(42); console.log(site.domain, site.status, site.target_hash); ``` ## Update a website Point your domain at new content after a rebuild: :::code-group ```bash [CLI] # Deploy new content and update the website record pinner website deploy ./dist ``` ```typescript [SDK] // Upload new content first const result = await pinner.uploadAndWait(newFiles); // Update the website to point at the new CID const updated = await pinner.websites.updateWebsite(42, { target_hash: result.cid, }); console.log("Updated:", updated.domain, "→", updated.target_hash); ``` ::: You can also update other fields: ```typescript const updated = await pinner.websites.updateWebsite(42, { domain: "newdomain.example.com", target_hash: "QmNewCID", dns_hosting_enabled: true, }); ``` ## Delete a website :::code-group ```bash [CLI] pinner website delete ``` ```typescript [SDK] await pinner.websites.deleteWebsite(42); ``` ::: :::warning Deleting a website removes the hosting record. Your content stays on IPFS unless you also unpin it, but the domain mapping and SSL certificate are removed. ::: ## Website properties Every website has these fields: | Field | Description | |-------|-------------| | `id` | Unique identifier | | `domain` | The domain name for the website | | `target_type` | Content address type (`ipfs` or `ipns`) | | `target_hash` | The CID or IPNS name the domain points to | | `status` | Current website status | | `validation_token` | Token for proving domain ownership | | `dns_hosting_enabled` | Whether Pinner manages DNS for this domain | | `is_subdomain` | Whether this is a `*.pinner.xyz` subdomain | | `ssl` | SSL certificate status (see [SSL](/websites/concepts/ssl)) | | `expired` | Whether the website has expired | | `created` / `updated` | Timestamps | ## Validate domain ownership If your custom domain hasn't been validated yet, trigger a validation check: ```typescript const result = await pinner.websites.validateWebsite(42); console.log(result.valid, result.reason, result.message); ``` Validation reasons: | Reason | Meaning | |--------|---------| | `validated` | Domain ownership confirmed | | `token_expired` | Validation token has expired, redeploy | | `dns_missing` | The TXT record wasn't found | | `dns_mismatch` | TXT record exists but token doesn't match | | `token_missing` | No validation token on the website record | # Publish with IPNS IPNS (InterPlanetary Name System) lets you publish content under a stable name that can be updated to point at new CIDs. This is useful for websites that update frequently — instead of changing DNS records every time, you update the IPNS record. ## How IPNS fits into website hosting When you create a website, you can set `target_type` to either `ipfs` or `ipns`: * **`ipfs`** — The domain points directly at a CID. To update, change `target_hash` on the website record. * **`ipns`** — The domain points at an IPNS name. To update, republish the IPNS name with a new CID. The domain doesn't change. IPNS adds a layer of indirection. You update content by republishing the IPNS name instead of updating the website record. ## Manage IPNS keys ### List keys :::code-group ```bash [CLI] pinner ipns list ``` ```typescript [SDK] const result = await pinner.ipns.listKeys(); console.log(`You have ${result.data.length} key(s)`); for (const key of result.data) { console.log(`- ${key.id}: ${key.ipns_name}`); } ``` ::: ### Create a key ```typescript const key = await pinner.ipns.createKey({ name: "my-website-key", }); console.log("IPNS name:", key.ipns_name); ``` ### Delete a key ```typescript await pinner.ipns.deleteKey(keyId); ``` :::warning Deleting an IPNS key means the IPNS name can no longer be resolved. Any domains pointing at this name will break. ::: ## Publish content to an IPNS name Point an IPNS name at a CID: :::code-group ```bash [CLI] pinner ipns publish --key --cid QmYourCID ``` ```typescript [SDK] const result = await pinner.ipns.publish({ key_id: key.id, cid: "QmYourCID", }); console.log("Published:", result.name, "→", result.value); ``` ::: ## Create a website with an IPNS target ```typescript const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipns", target_hash: key.ipns_name, }); ``` Now when you update your site, publish a new CID to the same IPNS name — the domain follows automatically. ## Republish IPNS records have a limited lifetime. Republish to extend it: ```typescript await pinner.ipns.republish(key.id); ``` ## Resolve an IPNS name Look up what CID an IPNS name currently points to: ```typescript const resolved = await pinner.ipns.resolve("k51qzi5..."); console.log("Points to:", resolved.value); ``` ## When to use IPNS vs direct IPFS | Scenario | Use | |----------|-----| | Static content that rarely changes | Direct IPFS (`target_type: "ipfs"`) | | Frequently updated sites | IPNS (`target_type: "ipns"`) | | CI/CD automated deploys | IPNS — update once, domain follows | | One-off deployments | Direct IPFS — simpler | IPNS resolution is slightly slower than direct IPFS because it adds a lookup step. For most sites, the difference is negligible. # Deploy a Static Site This tutorial walks you through the full process: build a static site, deploy it to IPFS, and make it accessible at a custom domain. By the end, you'll understand each step of the deployment pipeline. ## Prerequisites * A Pinner account with an [API key](/account/api-tokens) * The [Pinner CLI installed and authenticated](/ipfs/cli/auth) * A static site to deploy (we'll use Vite as an example) * A domain you control (for custom domain setup) ## Step 1: Build your site Build your project the way you normally would. For a Vite project: ```bash npm run build ``` This outputs static files to `./dist`. Any static site generator works — Next.js export, Astro, Hugo, plain HTML. As long as the output is a directory of static files, Pinner can host it. ## Step 2: Deploy with the CLI ```bash pinner website deploy ./dist ``` The CLI uploads the directory to IPFS, creates a website record, and prints the site details including the domain and CID. :::info The `deploy` command handles both the upload and the website creation in one step. If you prefer to control each part separately, use the SDK directly. ::: ## Step 3: (SDK) Upload and create the website If you're using the SDK, you handle the upload and website creation separately: ```typescript import { Pinner } from "@lumeweb/pinner"; import fs from "fs"; const pinner = new Pinner({ jwt: process.env.PINNER_API_KEY! }); // Upload the build output const files = fs.readdirSync("./dist").map((name) => { const buffer = fs.readFileSync(`./dist/${name}`); return new File([buffer], name); }); const result = await pinner.uploadAndWait(files); console.log("CID:", result.cid); // Create the website const site = await pinner.websites.createWebsite({ domain: "mysite.example.com", target_type: "ipfs", target_hash: result.cid, }); console.log("Website created:", site.id, site.domain); ``` ## Step 4: Set up your custom domain If you used a custom domain (not a `*.pinner.xyz` subdomain), you need to prove you own it. The website response includes a `validation_token` and `validation_record_host`. Add a DNS TXT record at your domain registrar: | Record | Name | Value | |--------|------|-------| | TXT | `_pinner-validation` | `verify=` | Then trigger validation: ```typescript const validation = await pinner.websites.validateWebsite(site.id); console.log("Valid:", validation.valid, validation.message); ``` Or with the CLI: ```bash pinner website validate ``` ## Step 5: Add a CNAME record Point your domain to Pinner's hosting infrastructure: | Record | Name | Value | |--------|------|-------| | CNAME | `www` | `sites.pinner.xyz` | For root domains, use an A record pointing to the IP shown in your website config: ```typescript const config = await pinner.websites.getWebsiteConfig(); console.log("Gateway domain:", config.gateway_domain); console.log("Nameservers:", config.nameservers); ``` ## Step 6: Wait for SSL Pinner provisions an SSL certificate automatically after your DNS records are in place. No configuration on your part — just wait. Check the status: ```typescript const ssl = await pinner.websites.getSSLStatus("mysite.example.com"); console.log("SSL status:", ssl.status); ``` SSL moves through these states: `pending` → `validating` → `valid` (or `ready`). If something goes wrong, it lands in `failed` or `error`. You can also watch for SSL readiness: ```typescript const watcher = pinner.websites.watchSSL("mysite.example.com", { interval: 5000, // check every 5 seconds timeout: 300000, // give up after 5 minutes }); await watcher.start({ onReady: (status) => console.log("SSL ready!", status), onError: (error) => console.error("SSL error:", error.message), onStatus: (status) => console.log("SSL progress:", status.status), }); ``` ## Step 7: Visit your site Once DNS propagates and SSL is ready, your site is live at `https://mysite.example.com`. ## Recap You built a static site, uploaded it to IPFS, created a website record, proved domain ownership, pointed DNS at Pinner, and let SSL provision automatically. That's the full cycle. ## Next steps * [Update your site with a new deploy](/websites/how-to/manage-websites) * [Use IPNS for updatable content addresses](/websites/concepts/ipns) * [Automate deploys with GitHub Actions](/websites/how-to/deploy-github-actions)