# API Keys
Source: https://docs.pinata.cloud/account-management/api-keys
This page is where you can create, record, and delete API keys for the [Pinata API](/api-reference/introduction). Creating an API key is very simple! Just visit the page to start by click on the API Keys button in the left sidebar, then click "New Key" in the top right.
In the New Key modal you can choose if you want the key to be an Admin key and have full access over every endpoint, or scope the keys by selecting which endpoints you want to use. You can also give it a limited number of uses, and be sure to give it a name to keep track of it. Once you have that filled out click "Create Key" and it will show you the `pinata_api_key`, `pinata_api_secret_key`, and the `JWT`. It's best to click "Copy All" and keep the API key data safe and secure.
Once API keys have been created, you will not be able to see the secret or JWT again
Once you have created your keys you can go ahead and try testing them! You can even use them in our [API Reference section](/api-reference/endpoint/ipfs/test-authentication) :eyes: Or feel free to paste this into your terminal with your `JWT`
```bash cURL theme={null}
curl --request GET \
--url https://api.pinata.cloud/data/testAuthentication \
--header 'accept: application/json' \
--header 'authorization: Bearer YOUR_PINATA_JWT'
```
If successful you should see this!
```shell bash theme={null}
{
"message": "Congratulations! You are communicating with the Pinata API!"
}
```
## Managing Keys
From the Keys Page you can see the name of a key, the public key, when it was issues, how many max uses it has, and what permissions it was given.
At any point you can delete an API key by clicking on the Revoke button
# Billing
Source: https://docs.pinata.cloud/account-management/billing
The billing page is where you can upgrade your account, view your current usage, or make changes to your billing info.
## Usage
Heading over to the "Usage" tab, this is where you can view how much of your plan has been used in the month. Gateway Bandwidth and Requests are reset each month on your billing cycle date.
If you reach 80% percent of your usage available, then you will start to
receive emails and warnings that you are close to running out of space. If you
are on the Free plan, then your account will no longer be able to upload or
use the Dedicated Gateway once your account has gone above the limit by 25%.
## Payment Info
Clicking the 'Manage Billing' button will show you the current card in use and if it's the default. If you want to remove a card, then you will need to add a new one first and set it as default before removing the old one.
Pinata currently only accepts standard debit and credit cards
## Plan Selection
From the plan selection you can choose a plan that fits your need the most, whether that be upgrading or downgrading.
If you upgrade in the middle of a billing cycle, then you will only be charged
the prorated amount
# Limits
Source: https://docs.pinata.cloud/account-management/limits
The Private IPFS API and IPFS API have variying limits that users should be aware of.
## API Limits
API rate limits on both the Private IPFS API and IPFS API are currently determined by plan type:
| Plan | Rate Limit |
| ---------- | ----------------------- |
| Free | 60 requests per minute |
| Picnic | 250 requests per minute |
| Fiesta | 500 requests per minute |
| Enterprise | 100 requests per second |
### Exceptions
The following API calls have increased rate limits:
* Endpoints under `api.pinata.cloud/data/` have a rate limit of 30 requests per minute
* The [Pinning Services API endpoint for listing content](/api-reference/pinning-service-api) has a rate limit of 30 requests per minute
## File Restrictions
HTML files can be uploaded on any plan, but can only be retrieved through a Dedicated Gateway with a [Custom Domain](gateways/dedicated-ipfs-gateways).
Binary files are only allowed on a case by case basis, please contact [team@pinata.cloud](mailto:team@pinata.cloud) for assistance.
## Gateway Rate Limits
At this time there are currently no rate limits for users retrieving content from a dedicated gateway.
## Upload Size Limits
There differing limits on file sizes between the Private IPFS API and IPFS API
### Private IPFS API
Files that are over **100MB** will require using [resumable uploads](/files/uploading-files#resumable-uploads) to complete. If you are using the SDK and the method `upload.file()` this will be handled automatically.
Beyond 100MB the max file size is **25GB** at this time.
### IPFS API
While the upload limit is 25GB we would recommend only uploading up to 15GB per file/folder for reliability reasons. We can try to assist uploads 15GB-25GB but we cannot guarantee success at this time.
There is no aggregate limit for uploads, but each individual upload (whether it is a file or a folder) is limited to **25 GB**.
There is also a file limit size of **10MB** for the pinJSONToIPFS API endpoint.
# Webhooks
Source: https://docs.pinata.cloud/account-management/webhooks
Subscribe to Pinata API events using Webhooks
Through the Pinata App you can create Webhooks for particular events fired from the Pinata API for your account specifically, like uploading or deleting a file.
## Setup
Navigate to the [Webhooks Tab]() inside the Pinata App and click "Add Endpoint" in the top right.

On the New Endpoint page you can put in a URL for your server endpoint that will receive the events. Additionally you can give it a description, which events it will subscribe to, and more advance options like rate limiting. Once you have your options selected click "Create" at the bottom.
If you don't have an endpoint for your app or server yet, you can create a test one by clicking on the `Svix Play` link below the URL input.

Once your Webhook is created you will be taken to the dashboard where you can see incoming logs and events that are fired. Try triggering one of the events that you've selected for your webhook either from the Pinata App or from the API. Then come back to the dashboard to see the result!

## Event Catalog
Check out the link below to browse all the available webhook events you can subscribe to!
Visit our Svix page for all Pinata Webhook events!
## Signature Verification
Webhook signatures let you verify that webhook messages are actually sent by Pinata and not a malicious actor.
For a more detailed explanation, check out this article on [why you should verify webhooks](https://docs.svix.com/receiving/verifying-payloads/why).
To grab the secret for your Webhook locate it on the Webhook Dashboard on the right sidebar. Click on the reveal icon to view the secret and copy it.

Our webhook partner Svix offers a set of useful libraries that make verifying webhooks very simple. Here is a an example using Javascript:
```typescript theme={null}
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
const headers = {
"webhook-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"webhook-timestamp": "1614265330",
"webhook-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"test": 2432232314}';
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
const payload = wh.verify(payload, headers);
```
For more instructions and examples of how to verify signatures, check out their [webhook verification documentation](https://docs.svix.com/receiving/verifying-payloads/how).
# Workspaces
Source: https://docs.pinata.cloud/account-management/workspaces
Workspaces is only available on the [Picnic and Fiesta plans](https://pinata.cloud/pricing)
Workspaces is a feature that allows you to add multiple people to your account and collaborate in a natural way. With the Picnic plan, you'll get 3 seats to invite your teammates, and with Fiesta you'll get 5 seats, plus the ability to add more at an extra fee.
## Inviting Members
At this time only Workspace Owners can invite members
To get started, login with a paid account and click on the profile button in the top right, then select "Workspaces."
Once at the Workspaces screen, you can type in the email for the person you want to invite. They could already have a Pinata account or could be someone who hasn't signed up yet. Once they sign into their account, they will be prompted to accept the invite on the Workspaces page.
## Switching Workspaces
By default, when you login, you will be put in your account with your Workspace, and you can switch to another Workspace you are member of by clicking on the drop-down menu in the top left corner.
## Removing Members
At this time only Workspace Owners can remove members
If you ever need to remove someone from a Workspace, you can do so from the Workspaces page. Click on the three small dots next to the user's email and click "remove member." You can invite them back at any time!
# Agents
Source: https://docs.pinata.cloud/agents
Deploy hosted OpenClaw agents through Pinata
Pinata Agents are hosted [OpenClaw](https://openclaw.org) instances. Each agent runs in an isolated container with its own environment, skills, and secrets. Pick a personality, connect an LLM provider, deploy — your agent is live.
Agents require a paid Pinata plan. [Upgrade here](https://app.pinata.cloud/billing).
## Agents Workspace
Open **Agents** in the [Pinata App](https://app.pinata.cloud) sidebar to access the Agents workspace.
The workspace has three tabs: **My Agents**, **Skills**, and **Secrets**.
## Before You Start
Your agent needs an LLM provider API key to run. Add it as a [secret](#secrets) before creating an agent — the setup wizard looks for it during the Connect step.
Pinata supports three providers:
| Provider | Models |
| ---------- | ------------------- |
| Anthropic | Claude |
| OpenAI | GPT |
| OpenRouter | Multi-model routing |
## Creating an Agent
Click **Create Agent** to start the setup wizard. There are four steps.
### 1. Identity
Choose a personality template or create a custom one:
* **Atlas** — analytical, methodical
* **Nova** — creative, exploratory
* **Sage** — patient, accuracy-focused
* **Custom** — define your own personality and purpose
Give the agent a name and an optional personality description.
### 2. Agent Workspace
Select a workspace template. The **Pinata Optimized Agent** environment includes Node.js, Python, and common CLI tools. State persists and syncs automatically.
### 3. Connect
Choose your AI provider. If the API key is already in the Secrets vault, the provider shows "Key available."
You can also configure messaging channels on this step.
**Telegram**
Create a bot through [@BotFather](https://t.me/botfather), copy the bot token, set a DM policy, and optionally restrict access to specific user IDs.
**Slack**
Create a Slack App at [api.slack.com/apps](https://api.slack.com/apps), enable Socket Mode, and generate an App-Level Token with the `connections:write` scope. Under **OAuth & Permissions**, add these Bot Token Scopes:
| Scope | Description |
| ------------------- | --------------- |
| `chat:write` | Send messages |
| `im:write` | Open DMs |
| `im:history` | Read DM history |
| `im:read` | DM info |
| `users:read` | User lookup |
| `app_mentions:read` | Hear @mentions |
For public channel support, also add `channels:history` and `channels:read`. Install the app to your workspace, then paste the Bot Token and App Token.
### 4. Deploy
Click **Deploy Agent**. The container spins up and your agent starts running.
## Managing Agents
Click into any agent to view its detail page with the Agent ID, version, and controls for Chat, Settings, and Restart.
The detail page also includes:
* **Snapshots** — snapshots of the agent environment, recorded every minute when changes are detected
* **Channels** — Telegram and Slack configuration
* **Skills** — attached skill packages
* **Secrets** — environment variables injected at startup
### Chat
Click **Chat** to open a direct session through the OpenClaw gateway. You can send messages, paste images, and start new sessions.
The gateway dashboard also provides access to instance management, sessions, usage metrics, and logs.
### Settings
Click **Settings** to open the full OpenClaw configuration panel covering environment, updates, authentication, channels, commands, hooks, skills, tools, gateway, models, and logging.
## Skills
Skills are reusable capability packages pinned to private IPFS. You manage them at the workspace level and attach them to agents during creation.
Each skill folder needs a `SKILL.md` with YAML frontmatter (name and description) and can optionally include a `metadata` JSON file for required environment variables. Click **Upload Skill**, name it (letters, numbers, hyphens, and underscores), add an optional description, and click **Upload & Register**.
Once an agent is deployed, its skills are managed through the OpenClaw instance directly.
## Secrets
The Secrets Vault stores encrypted API keys and credentials at the workspace level. You attach secrets to agents during creation, and they're injected as environment variables at startup. Values can't be viewed after saving.
Click **Add Secret**, enter the environment variable name (e.g., `ANTHROPIC_API_KEY`) and its value, then save. Each secret shows which agents are using it.
After deployment, per-agent secrets are managed through the OpenClaw instance
# Deleting Files
Source: https://docs.pinata.cloud/files/deleting-files
Deleting files from Pinata is simple and easy!
## Deleting Programatically
The SDK has a very simple [delete](/sdk/files/public/delete) method that will allow you to delete an array of files by `id`. Alternatively you can delete a single file with the [API](/api-reference/introduction).
```typescript SDK theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const unpin = await pinata.files.public.delete([
"3c52f1b8-11b1-40d9-849d-5f05a4bbd76d",
"b72886db-9dd4-434c-a1b2-f9d36781ecee"
])
```
```typescript API {6,9,11} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function delete() {
try {
const fileId = "e0b102e9-d481-4192-ab44-b8f7ff010e9a"
const request = await fetch(
`https://api.pinata.cloud/v3/files/public/${fileId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Deleting All Files
If you find yourself in a position where you need to delete most or all of your files you can use the [Auto Paginate](/sdk/files/public/list#auto-paginate) feature on the SDK to fetch all the IDs of your files and delete them in a few lines of code!
```typescript theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "dweb.mypinata.cloud",
});
async function main() {
try {
let files = [];
for await (const item of pinata.files.public.list()) {
files.push(item.id);
}
const res = await pinata.files.public.delete(files);
} catch (error) {
console.log(error);
}
}
main();
```
## Deleting by Web App
If you are trying to delete files, then you can do so by clicking on the on the 3 dots at the end of the row and selecting "Delete"
Additionally, with our Bulk File Actions tool, you can select and manage multiple files at once - up to 100!
# Groups
Source: https://docs.pinata.cloud/files/file-groups
Private Groups allow you to organize your Pinata content through the Pinata App, SDK, or API, giving you a clearer picture of what your files are being used for.
## SDK and API
With the [SDK](/sdk/groups/public), you can create groups, add files to groups, list details about a group, and more! You can also manage groups using the [API](/api-reference/endpoint/create-group).
### Create a Group
To create a group you can use the [create](/sdk/groups/public/create) method and passing in the `name` you want to give a group.
```typescript SDK theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const group = await pinata.groups.public.create({
name: "My New Group",
});
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const payload = JSON.stringify({
name: "My New Group",
})
const request = await fetch("https://api.pinata.cloud/v3/groups/public", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${JWT}`,
},
body: payload,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
This will return the Group info
```typescript SDK theme={null}
{
id: "01919976-955f-7d06-bd59-72e80743fb95",
name: "Test Private Group",
created_at: "2024-08-28T14:49:31.246596Z"
}
```
```json API theme={null}
{
"data": {
"id": "01919976-955f-7d06-bd59-72e80743fb95",
"name": "Test Private Group",
"created_at": "2024-08-28T14:49:31.246596Z"
}
}
```
### Add or Remove Files from a Group
There are two ways you can add files to a group. The first is to add the file to a group on [upload](/sdk/upload/public/file).
```typescript SDK {3} theme={null}
const upload = await pinata.upload.public
.file(file)
.group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```
```typescript API {13} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public")
formData.append("group", "b07da1ff-efa4-49af-bdea-9d95d8881103")
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
Another option is to add files after the fact using the [addFiles](/sdk/groups/public/add-files) method.
```typescript SDK theme={null}
const upload = await pinata.groups.public.addFiles({
groupId: "b07da1ff-efa4-49af-bdea-9d95d8881103",
files: [
"0ed5738f-07e7-4587-81fb-f04f8be15d77",
"a277dc29-2ca3-4dfb-aeb9-3f2b23e956f7"
]
})
```
```typescript API {6,8,11} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const groupId = "e0b102e9-d481-4192-ab44-b8f7ff010e9a"
const fileId = "521f23f3-2749-4611-b757-3155b40ff570"
const request = await fetch(
`https://api.pinata.cloud/v3/groups/public/${groupId}/ids/${fileId}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
Removing files can be done the exact same way with the [removeFiles](/sdk/groups/public/remove-files) method.
```typescript SDK theme={null}
const upload = await pinata.groups.public.removeFiles({
groupId: "b07da1ff-efa4-49af-bdea-9d95d8881103",
files: [
"0ed5738f-07e7-4587-81fb-f04f8be15d77",
"a277dc29-2ca3-4dfb-aeb9-3f2b23e956f7"
]
})
```
```typescript API {6,8,11} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const groupId = "e0b102e9-d481-4192-ab44-b8f7ff010e9a"
const fileId = "521f23f3-2749-4611-b757-3155b40ff570"
const request = await fetch(
`https://api.pinata.cloud/v3/groups/public/${groupId}/ids/${fileId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Get a Group
To fetch details of an already existing group you can use the [get](/sdk/groups/public/get) and pass in the `groupId`.
```typescript SDK theme={null}
const groups = await pinata.groups.public.get({
groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```
```typescript API {6,8,11} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const groupId = "e0b102e9-d481-4192-ab44-b8f7ff010e9a"
const request = await fetch(
`https://api.pinata.cloud/v3/groups/public/${groupId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
This will return the same group info received upon creation.
```typescript SDK theme={null}
{
id: "0191997b-ca28-79e8-9dbc-a8044ad3e547",
name: "My New Group 5",
created_at: "2024-08-28T14:55:12.448504Z",
}
```
```json APi theme={null}
{
"data": {
"id": "0191997b-ca28-79e8-9dbc-a8044ad3e547",
"name": "My New Group 5",
"created_at": "2024-08-28T14:55:12.448504Z"
}
}
```
### List All Groups
If you want to get all Groups or filter through them, you can use the [list](/sdk/groups/public/list) method.
```typescript SDK theme={null}
const groups = await pinata.groups.public.list()
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const url = "https://api.pinata.cloud/v3/groups/public"
const request = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
Results can be filtered with the following queries.
#### name
* Type: `boolean`
Filters groups based on the group name
```typescript SDK theme={null}
const groups = await pinata.groups.public
.list()
.name("SDK")
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/groups/public?name=SDK"
```
#### limit
* Type: `number`
Limits the number of results
```typescript SDK theme={null}
const groups = await pinata.groups.public
.list()
.limit(10)
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/groups/public?limit=10"
```
This will return an array of Groups and their respective info:
```typescript SDK theme={null}
{
groups: [
{
id: "0191997b-ca28-79e8-9dbc-a8044ad3e547",
name: "My New Group 5",
created_at: "2024-08-28T14:55:12.448504Z",
}
],
next_page_token: "MDE5MWIzNGMtMWNmNy03MzExLThmMjYtZmZlZDMzYTVlY"
}
```
```json API theme={null}
{
"groups": [
{
"id": "0191997b-ca28-79e8-9dbc-a8044ad3e547",
"name": "My New Group 5",
"created_at": "2024-08-28T14:55:12.448504Z"
}
],
"next_page_token": "MDE5MWIzNGMtMWNmNy03MzExLThmMjYtZmZlZDMzYTVlY"
}
```
### Updating a Group
You can update the name of a group using either the SDK or the API.
```typescript SDK theme={null}
const groups = await pinata.groups.public.update({
groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
name: "My New Group 2",
});
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const groupId = "e0b102e9-d481-4192-ab44-b8f7ff010e9a"
const payload = JSON.stringify({
name: "My New Group 2",
})
const request = await fetch(
`https://api.pinata.cloud/v3/groups/public/${groupId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${JWT}`,
},
body: payload
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
This will return the updated Group info.
```typescript SDK theme={null}
{
id: "3778c10d-452e-4def-8299-ee6bc548bdb0",
name: "My New Group 2",
created_at: "2024-08-28T20:58:46.96779Z"
}
```
```json API theme={null}
{
"data": {
"id": "01919ac8-a6f5-7e8e-a8a2-6cfe00122b90",
"name": "Updated Name",
"created_at": "2024-08-28T20:58:46.96779Z"
}
}
```
### Delete a Group
Deleting a Group that has CIDs inside of it will not unpin/delete the files. Please use the [delete](/sdk/files/public/delete) method to actually delete a file from your account
To delete a Group you can use the [delete](/sdk/groups/public/delete) method and pass in the `groupId`.
```typescript SDK theme={null}
const groups = await pinata.groups.public.delete({
groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const groupId = "e0b102e9-d481-4192-ab44-b8f7ff010e9a"
const request = await fetch(
`https://api.pinata.cloud/v3/groups/public/${groupId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
If successful the endpoint will return an `OK` response.
# Key-Values
Source: https://docs.pinata.cloud/files/key-values
A unique and powerful feature included with the IPFS API and Private IPFS API is the key-value store. Anytime you upload or update a file you can store up to 10 key-value pairs.
```typescript SDK {3-6} theme={null}
const upload = await pinata.upload.public
.file(file)
.keyvalues({
env: "prod",
userId: "abc123"
})
```
```typescript API {13-18,20} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public")
const keyvalues = JSON.stringify({
keyvalues: {
env: "prod",
userId: "abc123"
}
})
formData.append("keyvalues", keyvalues)
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
This small yet powerful feature allows you to remove the need for an external database in most cases. We like to call this paradigm **[File-Centric Architecture](https://pinata.cloud/blog/using-file-centric-architecture-to-build-simple-and-capable-apps/)**, where apps and their structure revolves around the files themselves. This creates a molecule like structure and keeps the data related to the file close by.

## Creating
Creating a new key-value for a file can be done in two ways:
### Uploading a File
By including the key-values as part of the upload [method](/sdk/upload/public/file) or [endpoint](/api-reference/endpoint/upload-a-file) and the file and the key-values will be created at the same time.
```typescript SDK {3-8} theme={null}
const upload = await pinata.upload.public
.file(file)
.keyvalues({
env: "prod",
userId: "abc123"
})
```
```typescript API {13-18,20} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public");
const keyvalues = JSON.stringify({
keyvalues: {
env: "prod",
userId: "abc123"
}
})
formData.append("keyvalues", keyvalues)
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Updating an Existing File
If you've already uploaded a file and want to add a key-value you can do so with the update [method](/sdk/files/public/update) or [endpoint](/api-reference/endpoint/update-file).
```typescript SDK {3-7} theme={null}
const update = await pinata.files.public.update({
id: "2b4ee88d-1032-4e4e-a373-97d1ab127f16", // Target File ID
keyvalues: {
env: "prod",
userId: "abc123"
}
})
```
```typescript API {6,8-13,15-21} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function update() {
try {
const fileId = "2b4ee88d-1032-4e4e-a373-97d1ab127f16"
const data = JSON.stringify({
keyvalues: {
env: "prod",
userId: "abc123"
}
})
const request = await fetch(`https://api.pinata.cloud/v3/files/public/${fileId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: data,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Retrieving
Since key-values exist with files, you can retrieve them by listing files either through the SDK [method](/sdk/files/public/list) or API [endpoint](/api-reference/endpoint/list-files), and filtering results by key-value. The operator will always be `===`.
You can chain multiple key-value queries together and it will only return files that meet both values.
```typescript SDK {3-5} theme={null}
const files = await pinata.files.public
.list()
.keyvalues({
user: "abc123"
})
```
```typescript API {5} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function list() {
try {
const request = await fetch(`https://api.pinata.cloud/v3/files/public?metadata[user]=123`, {
method: "GET",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Updating
The key-value system will automatically detect if you are replacing an existing value for a given key. For example, if you have a key of `env` with a value of `prod`, if you make an update of `env: "dev"` it will replace the old value. If the key does not exist then it will make a new key-value entry.
```typescript SDK {4} theme={null}
const update = await pinata.files.public.update({
id: "2b4ee88d-1032-4e4e-a373-97d1ab127f16", // Target File ID
keyvalues: {
env: "dev", // Previously `prod`
}
})
```
```typescript API {10} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function update() {
try {
const fileId = "2b4ee88d-1032-4e4e-a373-97d1ab127f16"
const data = JSON.stringify({
keyvalues: {
env: "dev", // Previously `prod`
}
})
const request = await fetch(`https://api.pinata.cloud/v3/files/public/${fileId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: data,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Deleting
You can remove a key-value entry by making the value `null`.
```typescript SDK {4} theme={null}
const update = await pinata.files.public.update({
id: "2b4ee88d-1032-4e4e-a373-97d1ab127f16", // Target File ID
keyvalues: {
env: null, // Deletes the `env` key-value entry
}
})
```
```typescript API {6,8-13,15-21} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function update() {
try {
const fileId = "2b4ee88d-1032-4e4e-a373-97d1ab127f16"
const data = JSON.stringify({
keyvalues: {
env: null, // Deletes the `env` key-value entry
}
})
const request = await fetch(`https://api.pinata.cloud/v3/files/public/${fileId}`, {
method: "PUT",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: data,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Further Reading
Check out some of our reading material on some of the possibilities of key-values and file-centric architecture!
# List & Query Files
Source: https://docs.pinata.cloud/files/listing-files
Pinata gives you the ability to query uploaded files based on different filters and attributes such as name, [key-values](/files/key-values), date, and more. This is different from retrieving the actual contents of a file, which you can learn more about [here](/gateways/retrieving-files).
## Basic Usage
You can either use the [SDK](/sdk/files/public/list) or the [API](/api-reference/endpoint/list-files) as see in the examples below.
```typescript SDK theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const files = await pinata.files.public.list()
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function files() {
try {
const url = "https://api.pinata.cloud/v3/files/public",
const request = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
This will return an array of file objects
```typescript SDK theme={null}
{
files: [
{
id: "dd5f8888-bf15-4559-b8a2-6c626869507f",
name: "Hello Files API",
cid: "bafybeifq444z4b7yqzcyz4a5gspb2rpyfcdxp3mrfpigmllh52ld5tyzwm",
size: 4861678,
number_of_files: 1,
mime_type: "TODO",
group_id: null,
created_at: "2024-08-27T14:57:51.485934Z",
},
{
id: "e2057aa3-7b6c-4a45-b785-12ba297bcbd0",
name: "Quickstart.png",
cid: "bafkreiebavn2jzkqh3ehy4pkqkdi2otnho6gbcffkeqnunk2lw5nmnwaea",
size: 223548,
number_of_files: 1,
mime_type: "TODO",
group_id: "5f8adce6-7312-46e0-90f7-13896bed297d",
created_at: "2024-08-28T23:46:07.823118Z",
},
{
id: "ac5308a1-de49-40a3-9f5c-d20f1bb6206d",
name: "hello.txt",
cid: "bafkreiffsgtnic7uebaeuaixgph3pmmq2ywglpylzwrswv5so7m23hyuny",
size: 11,
number_of_files: 1,
mime_type: "TODO",
group_id: null,
created_at: "2024-08-29T02:23:02.735018Z",
}
],
next_page_token: "MDE5MWIzNGMtMWNmNy03MzExLThmMjYtZmZlZDMzYTVlY"
}
```
```json API theme={null}
{
"data": {
"files": [
{
"id": "dd5f8888-bf15-4559-b8a2-6c626869507f",
"name": "Hello Files API",
"cid": "bafybeifq444z4b7yqzcyz4a5gspb2rpyfcdxp3mrfpigmllh52ld5tyzwm",
"size": 4861678,
"number_of_files": 1,
"mime_type": "TODO",
"group_id": null,
"created_at": "2024-08-27T14:57:51.485934Z"
},
{
"id": "e2057aa3-7b6c-4a45-b785-12ba297bcbd0",
"name": "Quickstart.png",
"cid": "bafkreiebavn2jzkqh3ehy4pkqkdi2otnho6gbcffkeqnunk2lw5nmnwaea",
"size": 223548,
"number_of_files": 1,
"mime_type": "TODO",
"group_id": "5f8adce6-7312-46e0-90f7-13896bed297d",
"created_at": "2024-08-28T23:46:07.823118Z"
},
{
"id": "ac5308a1-de49-40a3-9f5c-d20f1bb6206d",
"name": "hello.txt",
"cid": "bafkreiffsgtnic7uebaeuaixgph3pmmq2ywglpylzwrswv5so7m23hyuny",
"size": 11,
"number_of_files": 1,
"mime_type": "TODO",
"group_id": null,
"created_at": "2024-08-29T02:23:02.735018Z"
}
],
"next_page_token": "MDE5MWIzNGMtMWNmNy03MzExLThmMjYtZmZlZDMzYTVlY"
}
}
```
## Filters
When listing files there a few ways you can filter the results
### name
* Type: `string`
Filter results based on name
```typescript SDK {3} theme={null}
const files = await pinata.files.public
.list()
.name("pinnie")
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files/public?name=pinnie"
```
### group
* Type: `string`
Filter results based on group ID
```typescript SDK {3} theme={null}
const files = await pinata.files.public
.list()
.group("5b56981c-7e5b-4dff-aeca-de784728dddb")
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files/public?group=5b56981c-7e5b-4dff-aeca-de784728dddb"
```
### noGroup
* Type: `boolean`
Filter results to only show files that are not part of a group
```typescript SDK {3} theme={null}
const files = await pinata.files.public
.list()
.noGroup(true)
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files/public?group=null"
```
### cid
* Type: `string`
Filter results based on CID
```typescript SDK{3} theme={null}
const files = await pinata.files.public
.list()
.cid("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files/public?cid=bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
```
### mimeType
* Type: `string`
Filter results based on mime type
```typescript SDK {3} theme={null}
const files = await pinata.files.public
.list()
.mimeType("image/png")
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files/public?mimeType=image/png"
```
### keyvalues
* Type: `Record`
Filter results based on keyvalue pairs in metadata
```typescript SDK {3} theme={null}
const files = await pinata.files.public
.list()
.keyvalues({
env: "prod"
})
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files/public?keyvalues[env]=prod"
```
### order
* Type: `"ASC" | "DESC"`
Order results either ascending or descending by created date
```typescript SDK {3} theme={null}
const files = await pinata.files
.list()
.order("ASC")
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files?order=ASC"
```
### limit
* Type: `number`
Limit the number of results
```typescript SDK {3} theme={null}
const files = await pinata.files
.list()
.limit(10)
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files?limit=10"
```
### cidPending
* Type: `boolean`
Filters results and only returns files where `cid` is still `pending`
```typescript SDK {3} theme={null}
const files = await pinata.files
.list()
.cidPending(true)
```
```typescript API theme={null}
const url = "https://api.pinata.cloud/v3/files?cidPending=true"
```
## Auto Paginate (SDK)
The `list` method has an auto pagination feature that is triggered when used inside a `for await` iterator
```typescript theme={null}
for await (const item of pinata.files.list() {
console.log(item.id);
}
```
Works like magic ✨
# Collections
Source: https://docs.pinata.cloud/files/nft-backup/collections
View your NFT collections and their IPFS CIDs
After syncing a wallet, view your NFT collections and their CIDs.
## Viewing Collections
Each collection shows:
| Field | Description |
| ---------- | --------------- |
| Name | Collection name |
| Chain | Blockchain |
| NFTs Owned | Your NFT count |
| CIDs | IPFS CIDs found |
## Collection Details
Click a collection to see:
* Collection metadata (image, description)
* List of CIDs with backup status
### CID Information
| Field | Description |
| --------- | ------------------------------ |
| CID | IPFS content identifier |
| Source | `metadata` or `image` |
| Backed Up | Whether pinned to your account |
## Filtering
Filter collections by chain using the dropdown.
## Related
* [Wallets](/files/nft-backup/wallets) - Add and sync wallets
* [Sync](/files/nft-backup/sync) - Sync CIDs
# Overview
Source: https://docs.pinata.cloud/files/nft-backup/overview
Backup your NFT collections to Pinata
Pin all IPFS content from your NFT collections to your Pinata account.
## Two Ways to Backup
Sync your wallet on-demand and choose which CIDs to backup. Best for one-time backups or when you want full control over what gets pinned.
Enable auto-backup to automatically pin new CIDs whenever you sync. Set it and forget it—your NFTs stay protected.
## How It Works
1. **Connect** - Add your wallet address
2. **Sync** - Pinata scans your wallet across supported chains and extracts IPFS CIDs
3. **Backup** - Pin CIDs to your Pinata account (manually or automatically)
Own multiple NFTs from the same collection? Shared assets are only pinned once, so you won't pay for duplicates.
## Supported Chains
| | | |
| --------- | -------- | --------- |
| Ethereum | Base | Polygon |
| Flow | Zora | Arbitrum |
| Monad | Optimism | Avalanche |
| Blast | Sei | B3 |
| Berachain | ApeChain | Ronin |
| Abstract | Shape | Unichain |
| Gunzilla | HyperEVM | Somnia |
## Next Steps
Add and sync wallets
View your collections
Sync CIDs
# Sync
Source: https://docs.pinata.cloud/files/nft-backup/sync
Pin your NFT CIDs to Pinata
After syncing a wallet, backup the IPFS CIDs to your Pinata account. Own multiple NFTs from the same collection? Shared assets are only pinned once, so you won't pay for duplicates.
## Backup All
To backup all CIDs for a wallet:
1. Go to [NFT Backup](https://app.pinata.cloud/nft-backup/wallets)
2. Select a wallet
3. Click **Backup All**
Pinata pins all CIDs in the background. This can take a few minutes.
## Auto-Backup
Enable auto-backup to automatically pin new CIDs when you sync:
1. Select a wallet
2. Toggle **Auto-Backup** on
## Related
* [Wallets](/files/nft-backup/wallets) - Add and sync wallets
* [Collections](/files/nft-backup/collections) - View collections
# Wallets
Source: https://docs.pinata.cloud/files/nft-backup/wallets
Add wallets to backup your NFT collections
Add your wallet address to Pinata, then sync to fetch your NFT collections.
## Adding a Wallet
1. Go to [NFT Backup](https://app.pinata.cloud/nft-backup/wallets)
2. Click **Add Wallet**
3. Enter your wallet address
4. Click **Register**
## Syncing a Wallet
After adding a wallet, sync it to fetch your NFT collections:
1. Select a wallet
2. Click **Sync**
Syncing scans your wallet across supported chains and extracts any IPFS CIDs from the metadata and images. This can take a few minutes depending on how many NFTs you have.
## Managing Wallets
From the wallets page you can:
* View sync status and CID counts
* Enable auto-backup
## Related
* [Collections](/files/nft-backup/collections) - View synced collections
* [Sync](/files/nft-backup/sync) - Sync CIDs
# Presigned URLs
Source: https://docs.pinata.cloud/files/presigned-urls
There are situations where you may need to upload a file client side instead of server side, but doing so might risk exposing an API key. To solve this you can create a presigned upload URL on the server and then pass it to the client for it to be consumed. Creating signed upload URLs can be done with either the [Files SDK](/sdk/upload/public/create-signed-url) or the [API](/api-reference/endpoint/create-signed-upload-url), and you can designate how long the URL is valid for, how large the file can be, the type of file allowed, or extra metadata like a name and [keyvalues](/files/key-files).
For a more robust example, check out our guides on Hono and React!
## Usage
Setting up a server side API endpoint might look something like this:
```typescript SDK theme={null}
import { type NextRequest, NextResponse } from "next/server";
import { pinata } from "@/utils/config"; // Import the Pinata SDK instance
export const dynamic = "force-dynamic";
export async function GET() {
// Handle your auth here to protect the endpoint
try {
const url = await pinata.upload.public.createSignedURL({
expires: 30, // The only required param
mimeTypes: ["text/*"], // Optional restriction for certain file types
maxFileSize: 5000000 // Optional file size limit
})
return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
} catch (error) {
console.log(error);
return NextResponse.json({ text: "Error creating signed URL:" }, { status: 500 });
}
}
```
```typescript API theme={null}
import { type NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
// Handle your auth here to protect the endpoint
try {
// Prepare payload data for request
const data = JSON.stringify({
network: "public",
expires: 30, // Number of seconds the signed url is good for
filename: "Client file", // Optional name
allow_mime_types: [ "text/*" ], // Optional array of allowed file types
max_file_size: 5000000 // optional max file size
})
// send request and parse response
const urlRequest = await fetch("https://uploads.pinata.cloud/v3/files/sign", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.PINATA_JWT}`
},
body: data
})
const urlResponse = await urlRequest.json()
return NextResponse.json({ url: urlResponse.data }, { status: 200 }); // Returns the key data
} catch (error) {
console.log(error);
return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
}
}
```
Then back on the client side code, you can upload using the signed URL instead of the regular upload endpoint.
If you're using the SDK you can use the `.url()` parameter on any of the upload methods and pass in the signed upload URL there. If you are using the API you can simply make the upload request using the signed URL as the endpoint.
```typescript SDK {3} theme={null}
const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL from your server
const urlResponse = await urlRequest.json(); // Parse response
const upload = await pinata.upload.public
.file(file)
.url(urlResponse.url); // Upload the file with the signed URL
```
```typescript API {1-2,13} theme={null}
const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
const urlResponse = await urlRequest.json(); // Parse response
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public")
const request = await fetch(urlResponse.url, {
method: "POST",
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Reference
Below are the availble parameters for presigned URLs
### expires
* Type: `number`
The number of seconds the signed URL should be valid for
```typescript {2} theme={null}
const url = await pinata.upload.public.createSignedURL({
expires: 30,
});
```
### name (Optional)
* Type: `string`
Name for the file to be uploaded
```typescript {3} theme={null}
const url = await pinata.upload.public.createSignedURL({
expires: 30,
name: "My Cool File"
});
```
### mimeTypes (Optional)
* Type: `string[]`
Specify allowed file mime types and prevent uploads from files that do not match. Accepts wildcard mime types as well.
```typescript {3-6} theme={null}
const url = await pinata.upload.public.createSignedURL({
expires: 30,
mimeTypes: [
"image/*",
"application/json"
]
});
```
### maxFileSize (Optional)
* Type: `number`
Restrict upload to a specified file size in `bytes`
```typescript {3} theme={null}
const url = await pinata.upload.public.createSignedURL({
expires: 30,
maxFileSize: 50000 // 50kb
});
```
### groupId (Optional)
* Type: `string`
The target groupId the file would be uploaded to
```typescript {3} theme={null}
const url = await pinata.upload.public.createSignedURL({
expires: 30,
groupId: "ad4bc3bf-8794-49e7-94ff-fea1ce745779"
});
```
### keyvalues (Optional)
* Type: `Record`
Keyvalue pairs for the uploaded file
```typescript {3-5} theme={null}
const url = await pinata.upload.public.createSignedURL({
expires: 30,
keyvalues: {
env: "prod"
}
});
```
### date (Optional)
* Type: `number`
A UNIX timestamp of the date a URL is signed
```typescript {1-2,6} theme={null}
const date = Math.floor(new Date().getTime() / 1000);
//date: 1724943711
const url = await pinata.upload.public.createSignedURL({
expires: 30,
date: date
});
```
# Private IPFS
Source: https://docs.pinata.cloud/files/private-ipfs
IPFS has traditionally been a fully public network, so anything you pin can be accessed by anyone if they have the CID. While this is a benefit for a majority of blockchain applications, there are still cases where true privacy is needed. This is why Pinata has built Private IPFS, a new service that allows you to keep content private and only share when authorized.
## Private vs Public Network
Everything in the SDK and API have been separated by two networks: `public` and `private`. This means files and groups will be in separate resources and will be accessed by designating the network in either the SDK method or API route.
### Uploading
```typescript SDK theme={null}
// Uploads a file to Public IPFS
const publicUpload = await pinata.upload.public.file(file)
// Uploads a file to Private IPFS
const privateUpload = await pinata.upload.private.file(file)
```
```typescript {11} API theme={null}
const JWT = "PINATA_JWT";
async function uploadFile() {
try {
const text = "Hello World!";
const blob = new Blob([text], { type: "text/plain" });
const file = new File([blob], "hello-world.txt");
const data = new FormData();
data.append("file", file);
// Upload a file to Public IPFS
data.append("network", "public")
const request = await fetch(
"https://uploads.pinata.cloud/v3/files",
{
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: data,
}
);
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Listing and Querying
```typescript SDK theme={null}
// List public files
const files = await pinata.files.public.list()
// List private files
const files = await pinata.files.private.list()
```
```typescript {6,8} API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function files() {
try {
// Designate network to list either public or private files
const network = "public"
const url = `https://api.pinata.cloud/v3/files/${network}`,
const request = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${JWT}`,
}
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Groups
```typescript SDK theme={null}
// Create a public group
const group = await pinata.groups.public.create({
name: "My Public Group",
});
// Create a private group
const group = await pinata.groups.private.create({
name: "My Private Group",
});
```
```typescript {5,7} API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function group() {
try {
const network = "public"
const endpoint = `https://api.pinata.cloud/v3/groups/${network}`
const payload = JSON.stringify({
name: "My Public Group",
})
const request = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${JWT}`,
},
body: payload,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Accessing Private Files
With Public IPFS you can simply access a file through an IPFS Gateway with a CID. Since Private IPFS does not announce to the public IPFS network the only way you can access them is with a temporary access link. This can be generated with either the SDK or the API and set to expire after a designated number of seconds.
```typescript SDK theme={null}
const url = await pinata.gateways.private.createAccessLink({
cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq", // CID of the file to access
expires: 30, // Number of seconds the link is valid for
});
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function createAccessLink() {
try {
// Endpoint for creating access links
const url = "https://api.pinata.cloud/v3/files/download_link"
// CID of the file you want to access
const cid = "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
// Current Date
const date = Math.floor(new Date().getTime() / 1000);
const data = JSON.stringify({
url: `https://example.mypinata.cloud/files/${cid}`, // Gateway URL for the file using /files/ in the path
expires: 180, // Number of seconds the link will be valid for
date: date, // Current date of request
method: "GET" // GET for accessing file
})
const request = await fetch(url, {
method: "POSt",
headers: {
Authorization: `Bearer ${JWT}`,
"Content-Type": "application/json"
},
body: data
});
const response = await request.json();
return response.data
} catch (error) {
console.log(error);
}
}
```
## Monetizing Private Files
Want to get paid for access to your private content? Check out [x402 Monetization](/files/x402), which allows you to attach payment requirements to private files using USDC on Base.
## Example Apps
Pinata has built several example apps and tutorials you can reference to see how Private IPFS enables token gated experiences in blockchain and crpto contexts.
GitHub repo for [CONCEALMINT](https://concealmint.com), an app for creating and access Private NFTs
By using Private IPFS you can gate acceess to files based on NFT ownership
Sell digital content inside Farcaster frames and keeping it secure through Private IPFS
Add simple yet private logging to your crypto app using Private IPFS
See the possibilities of file management using Groups and Private IPFS
# Signatures
Source: https://docs.pinata.cloud/files/signatures
Learn how to use Pinata to cryptographically sign CIDs
In a post-AI world it will become more and more evident that every piece of content will need a cryptographic signature to verify it's authenticity. Pinata is taking steps in this direction with the [Signatures API](/sdk/signatures/public) and the [Content Addressable Gateway Plugin](/gateways/plugins/content-addressable).
## Signature Standard
Pinata is using the EIP-712 signature standard for signing CIDs with the following domain and types.
```typescript theme={null}
export const domain = {
name: "Sign Content",
version: "1.0.0",
chainId: 1,
} as const;
export const types = {
Sign: [
{ name: "address", type: "address" },
{ name: "cid", type: "string" },
{ name: "date", type: "string" },
],
EIP712Domain: [
{
name: "name",
type: "string",
},
{
name: "version",
type: "string",
},
{
name: "chainId",
type: "uint256",
},
],
};
```
### address
* Type: `address`
The wallet address of the user singing the CID
```
0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826
```
### cid
* Type: `string`
The target CID to be signed
```
bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4
```
### date
* Type: `string`
The date the target CID was first uploaded to Pinata
```
2024-07-29T18:29:47.355Z
```
## Creating a Signature
In order to sign a CID you can use any library that support EIP-712 signing, like the example below with [viem](https://viem.sh/docs/actions/wallet/signTypedData).
```typescript example.ts theme={null}
import { account, walletClient } from './config'
import { domain, types } from './data'
const signature = await walletClient.signTypedData({
account,
domain,
types,
primaryType: 'Sign',
message: {
address: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
cid: 'bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4',
date: "2024-07-29T18:29:47.355Z"
}
})
```
```typescript data.ts theme={null}
export const domain = {
name: "Sign Content",
version: "1.0.0",
chainId: 1,
} as const;
export const types = {
Sign: [
{ name: "address", type: "address" },
{ name: "cid", type: "string" },
{ name: "date", type: "string" },
],
EIP712Domain: [
{
name: "name",
type: "string",
},
{
name: "version",
type: "string",
},
{
name: "chainId",
type: "uint256",
},
],
};
```
```typescript config.ts theme={null}
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
export const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum!),
})
export const [account] = await walletClient.getAddresses()
↑ JSON-RPC Account
// export const account = privateKeyToAccount(...)
↑ Local Account
```
## Adding Signature to CID
In order to attach a signature to a CID the following requirements must be met:
* The CID being signed is owned by the signer
* The CID being signed was first uploaded by the signer
* The CID must not already have an existing signature with Pinata
After creating the signature with the previous step you can add it to the CID with the [add](/sdk/signatures/public/add) method in the SDK.
```typescript theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const signature = await pinata.signatures.public.add({
cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
signature: "0x1b...911b",
address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```
## Getting a Signature for a CID
There are two ways you can an existing signature for a CID: the [get](/sdk/signatures/public/get) method in the SDK or the [Content Addressable Gateway Plugin](/gateways/plugins/content-addressable).
### Content Addressable Gateway Plugin
After [installing the plugin](/gateways/plugins/getting-started#installing-plugins) you can simply request a CID through the Dedicated Gateway and get the signature in the header `pinata-signauture`.
```typescript theme={null}
const signatureReq = await fetch(
`https://.mypinata.cloud/ipfs/`,
{
method: "HEAD",
}
);
const signature = signatureReq.headers.get("pinata-signature");
```
### SDK
You can also use the [get](/sdk/signatures/public/get) method to get a signature for a given CID.
This method will check all CIDs on Pinata and will return a signature if it exists
```typescript theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const signature = await pinata.signatures.public.get(
"QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU"
);
```
## Verifying a Signature
Since the signatures are using the EIP-712 standard you can use a library like [Viem](https://viem.sh/docs/utilities/verifyTypedData) to verify with the same typed data used to create it.
```typescript example.ts theme={null}
import { account, walletClient } from './config'
import { domain, types } from './data'
import { verifyTypedData } from 'viem'
const CID = "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
const signatureReq = await fetch(
`https://example-gateway.mypinata.cloud/ipfs/${CID}`,
{
method: "HEAD",
}
);
const signature = signatureReq.headers.get("pinata-signature");
const valid = await verifyTypedData({
address: account.address,
domain,
types,
primaryType: 'Sign',
message: {
address: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826',
cid: 'bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4',
date: "2024-07-29T18:29:47.355Z"
},
signature,
})
```
```typescript data.ts theme={null}
export const domain = {
name: "Sign Content",
version: "1.0.0",
chainId: 1,
} as const;
export const types = {
Sign: [
{ name: "address", type: "address" },
{ name: "cid", type: "string" },
{ name: "date", type: "string" },
],
EIP712Domain: [
{
name: "name",
type: "string",
},
{
name: "version",
type: "string",
},
{
name: "chainId",
type: "uint256",
},
],
};
```
```typescript config.ts theme={null}
import { createWalletClient, custom } from 'viem'
import { mainnet } from 'viem/chains'
export const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum!),
})
export const [account] = await walletClient.getAddresses()
↑ JSON-RPC Account
// export const account = privateKeyToAccount(...)
↑ Local Account
```
## Removing a Signature for a CID
To delete an existing signautre for a given CID you can use the [delete](/sdk/signatures/public/delete) method.
```typescript theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const signature = await pinata.signatures.public.delete(
"QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU"
);
```
# Uploading Files
Source: https://docs.pinata.cloud/files/uploading-files
At the core of Pinata's services is our IPFS APIs which allow you to upload files to either public or private IPFS. You can read more about the difference between the two [here](/files/private-ipfs).
Let's look at the multiple ways you can upload files!
## How to Upload Files
Uploading files with Pinata is simple, whether you want to use the SDK or the API. Key things to know:
* Uploads are done through `multipart/form-data` requests
* The SDK and API accept File objects per the [Web API Standard for Files]()
* You can add additional info to your upload such as a custom name for the file, keyvalue metadata, and a target group destination for organization
Here is a simple example of how you might upload a file in Typescript
```typescript SDK theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
const upload = await pinata.upload.public.file(file);
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
This will return the following response
```typescript SDK theme={null}
{
id: "349f1bb2-5d59-4cab-9966-e94c028a05b7",
name: "file.txt",
cid: "bafybeihgxdzljxb26q6nf3r3eifqeedsvt2eubqtskghpme66cgjyw4fra",
size: 4682779,
number_of_files: 1,
mime_type: "text/plain",
group_id: null
}
```
```JSON API theme={null}
{
"data": {
"id": "349f1bb2-5d59-4cab-9966-e94c028a05b7",
"name": "file.txt",
"cid": "bafybeihgxdzljxb26q6nf3r3eifqeedsvt2eubqtskghpme66cgjyw4fra",
"size": 4682779,
"number_of_files": 1,
"mime_type": "text/plain",
"group_id": null
}
}
```
* `id`: The ID of the file used for getting info, updating, or deleting
* `name`: The name of the file or the provided name in the `addMetadata` method
* `cid`: A cryptographic hash based on the contents of the file
* `size`: The size of the file in bytes
* `number_of_files`: The number of files in a reference
* `mime_type`: The mime type of the uploaded file
* `group_id`: The group the file was uploaded to if applicable
### Metadata
When uploading a file you can add additional metadata using the `name` or `keyvalues` methods after the selected upload method. This can include an optional `name` override or `keyvalue` pairs that can be used to searching the file later on
[Check out the Key-Values doc for more info](/files/key-values)
```typescript SDK {3-8} theme={null}
const upload = await pinata.upload.public
.file(file)
.name("hello.txt")
.keyvalues({
env: "prod"
})
```
```typescript API {13,15-19, 21} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public");
formData.append("name", "hello.txt");
const keyvalues = JSON.stringify({
keyvalues: {
env: "prod"
}
})
formData.append("keyvalues", keyvalues);
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Groups
Pinata offers Private IPFS Groups to organize your content. You can upload a file to a group by using the `group` method.
[Check out the Groups doc for more info](/files/file-groups)
```typescript SDK {3} theme={null}
const upload = await pinata.upload.public
.file(file)
.group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```
```typescript API {13} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public");
formData.append("group", "b07da1ff-efa4-49af-bdea-9d95d8881103");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Expiration
You can set an expiration date on files by using the `expires_at` option. The timestamp must be a Unix timestamp in the future. Once the expiration time is reached, the file will be automatically deleted.
```typescript SDK {3} theme={null}
const upload = await pinata.upload.public
.file(file)
.expires(1735689600) // Unix timestamp in the future
```
```typescript API {13} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public");
formData.append("expires_at", "1735689600"); // Unix timestamp in the future
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
You can also set or update the expiration on an existing file using the update method or endpoint:
```typescript SDK {3} theme={null}
const update = await pinata.files.public.update({
id: "2b4ee88d-1032-4e4e-a373-97d1ab127f16",
expires_at: 1735689600 // Unix timestamp in the future
})
```
```typescript API theme={null}
const JWT = "YOUR_PINATA_JWT";
async function update() {
try {
const fileId = "2b4ee88d-1032-4e4e-a373-97d1ab127f16";
const data = JSON.stringify({
expires_at: 1735689600 // Unix timestamp in the future
});
const request = await fetch(`https://api.pinata.cloud/v3/files/public/${fileId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${JWT}`,
},
body: data,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Client Side Uploads
There are situations where you may need to upload a file client side instead of server side. A great example is in Next.js where there is a 4MB file size restriction for files passed through Next's API routes. To solve this you can create a signed upload URL on the server and then pass it to the client for it to be consumed. This way your admin API key stays safe behind a server. Creating signed upload URLs can be done with either the [SDK](/sdk/upload/public/create-signed-url) or the [API](/api-reference/endpoint/create-signed-upload-url), and you can designate how long the URL is valid for or if there is other infromation you want to include such as metadata or a group ID.
Setting up a server side API endpoint might look something like this:
```typescript SDK theme={null}
import { type NextRequest, NextResponse } from "next/server";
import { pinata } from "@/utils/config"; // Import the Files SDK instance
export const dynamic = "force-dynamic";
export async function GET() {
// Handle your auth here to protect the endpoint
try {
const url = await pinata.upload.public.createSignedURL({
expires: 30, // The only required param
name: "Client File",
group: "my-group-id"
})
return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
} catch (error) {
console.log(error);
return NextResponse.json({ text: "Error creating signed URL:" }, { status: 500 });
}
}
```
```typescript API theme={null}
import { type NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
// Handle your auth here to protect the endpoint
try {
// Prepare payload data for request
const data = JSON.stringify({
network: "public",
expires: 30,
filename: "Client file",
group_id: "my-group-id"
})
// send request and parse response
const urlRequest = await fetch("https://uploads.pinata.cloud/v3/files/sign", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.PINATA_JWT}`
},
body: data
})
const urlResponse = await urlRequest.json()
return NextResponse.json({ url: urlResponse.data }, { status: 200 }); // Returns the key data
} catch (error) {
console.log(error);
return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
}
}
```
Then back on the client side code, you can upload using the signed URL instead of the regular upload endpoint.
If you're using the SDK you can use the `.url()` parameter on any of the upload methods and pass in the signed upload URL there. If you are using the API you can simply make the upload request using the signed URL as the endpoint.
```typescript SDK {3} theme={null}
const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
const urlResponse = await urlRequest.json(); // Parse response
const upload = await pinata.upload.public
.file(file)
.url(urlResponse.url); // Upload the file with the signed URL
```
```typescript API {1-2,13} theme={null}
const urlRequest = await fetch("/api/url"); // Fetches the temporary upload URL
const urlResponse = await urlRequest.json(); // Parse response
async function upload() {
try {
const formData = new FormData();
const file = new File(["hello"], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public")
const request = await fetch(urlResponse.url, {
method: "POST",
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
For more information on client side uploads check out our [Presigned URLs Guide](/files/presigned-urls)
### Upload Progress
If you happen to use the API as well as local files you can also track the progress of the upload using a library like `got`. Better support for upload progress will come in later versions of the SDK!
```typescript theme={null}
import fs from "fs";
import FormData from "form-data";
import got from "got";
async function upload() {
const url = `https://uploads.pinata.cloud/v3/files`;
try {
let data = new FormData();
data.append(`file`, fs.createReadStream("path/to/file"));
const response = await got(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PINATA_JWT}`,
},
body: data,
}).on("uploadProgress", (progress) => {
console.log(progress);
});
console.log(JSON.parse(response.body));
} catch (error) {
console.log(error);
}
}
```
If your file is larger than 100MB then a better approach is to follow the [Resumable Upload Guide](#resumable-uploads)
## Common File Recipes
Below are some common recipes for uploading a file.
### Blob
Usually you can pass a Blob directly into the request but to help guarantee success we recommend passing it into a `File` object.
```typescript SDK {8-10} theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const text = "Hello World!";
const blob = new Blob([text]);
const file = new File([blob], "hello.txt", { type: "text/plain" });
const upload = await pinata.upload.public.file(file);
```
```typescript API {7-9} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const text = "Hello World!";
const blob = new Blob([text]);
const file = new File([blob], "Testing.txt", { type: "text/plain" });
formData.append("file", file);
formData.append("network", "public");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### JSON
Pinata makes it easy to upload JSON objects using the [json](/sdk/upload/public/json) method.
```typescript SDK {8-15} theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const upload = await pinata.upload.public.json({
id: 2,
name: "Bob Smith",
email: "bob.smith@example.com",
age: 34,
isActive: false,
roles: ["user"],
});
```
```typescript API {7-16} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const json = JSON.stringify({
id: 2,
name: "Bob Smith",
email: "bob.smith@example.com",
age: 34,
isActive: false,
roles: ["user"],
})
const blob = new Blob([json]);
const file = new File([blob], "bob.json", { type: "application/json" });
formData.append("file", file);
formData.append("network", "public");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Local Files
If you need to upload files from a local file source you can use `fs` to feed a file into a `blob`, then turn that `blob` into a `File`. Due to the buffer limit in Node.js you may have issues going beyond 2GB with this approach. Using [Resumable Uploads](#resumable-uploads) with a client side file picker will help increase this.
Support for file streams will be coming in a later version of the SDK.
```typescript SDK {10-11} theme={null}
const { PinataSDK } = require("pinata");
const fs = require("fs");
const { Blob } = require("buffer");
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const blob = new Blob([fs.readFileSync("./hello-world.txt")]);
const file = new File([blob], "hello-world.txt", { type: "text/plain" });
const upload = await pinata.upload.public.file(file);
```
```typescript API {9} theme={null}
const JWT = "YOUR_PINATA_JWT";
const fs = require("fs");
const { Blob } = require("buffer");
async function upload() {
try {
const formData = new FormData();
formData.append("file", fs.createReadStream("./hello-world.txt"));
formData.append("network", "public")
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### Folders
The SDK can accept an array of files using the [fileArray](/sdk/upload/public/file-array) method. Folders can also be uploaded via the API by creating an array of files and mapping over them to add them to the form data. This is different then having a single `file` entry and having multiple files for that one entry, which does not work.
Folder uploads are currently only supported on Public IPFS
```typescript SDK theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const file1 = new File(["hello world!"], "hello.txt", { type: "text/plain" })
const file2 = new File(["hello world again!"], "hello2.txt", { type: "text/plain" })
const upload = await pinata.upload.public.fileArray([file1, file2])
```
```javascript Node.js theme={null}
import fs from "fs"
import FormData from "form-data"
import rfs from "recursive-fs"
import basePathConverter from "base-path-converter"
import got from 'got'
const pinDirectoryToPinata = async () => {
const url = `https://api.pinata.cloud/pinning/pinFileToIPFS`;
const src = "PATH_TO_FOLDER";
var status = 0;
try {
const { dirs, files } = await rfs.read(src);
let data = new FormData();
for (const file of files) {
data.append(`file`, fs.createReadStream(file), {
filepath: basePathConverter(src, file),
});
}
const response = await got(url, {
method: 'POST',
headers: {
"Authorization": "Bearer PINATA_API_JWT"
},
body: data
})
.on('uploadProgress', progress => {
console.log(progress);
});
console.log(JSON.parse(response.body));
} catch (error) {
console.log(error);
}
};
pinDirectoryToPinata()
```
```javascript React theme={null}
import { useState } from "react";
import { pinata } from "./utils/config"
function App() {
const [selectedFiles, setSelectedFiles] = useState();
const changeHandler = (event: React.ChangeEvent) => {
setSelectedFiles(event.target?.files);
};
const handleSubmission = async () => {
try {
const upload = await pinata.upload.public.fileArray(selectedFiles)
console.log(upload);
} catch (error) {
console.log(error);
}
};
return (
<>
>
);
}
export default App;
```
```javascript Javascript theme={null}
import FormData from "form-data"
const pinDirectoryToIPFS = async () => {
try {
const folder = "json";
const json1 = { hello: "world" };
const json2 = { hello: "world2" };
const blob1 = new Blob([JSON.stringify(json1, null, 2)], {
type: "application/json",
});
const blob2 = new Blob([JSON.stringify(json2, null, 2)], {
type: "application/json",
});
const files = [
new File([blob1], "hello.json", { type: "application/json" }),
new File([blob2], "hello2.json", { type: "application/json" }),
];
const data = new FormData();
Array.from(files).forEach((file) => {
// If you are not using `fs` you might need to specify the folder path along with the filename
data.append("file", file, `${folder}/${file.name}`);
});
const pinataMetadata = JSON.stringify({
name: `${folder}`,
});
data.append("pinataMetadata", pinataMetadata);
const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
headers: {
Authorization: `Bearer ${PINATA_JWT}`,
},
body: data,
});
const resData = await res.json();
console.log(resData);
} catch (error) {
console.log(error);
}
};
pinDirectoryToIPFS();
```
We also have other tools like the [Pinata IPFS CLI](/tools/ipfs-cli) which can be used to upload using [API Keys](/account-managemnet/api-keys)!
### URL
To upload a file from an external URL you can stream the contents into an `arrayBuffer`, which then gets passed into a new `Blob` that can then be uploaded to Pinata. This has been abstracted in the SDK using the [url](/sdk/upload/public/url) method.
```typescript SDK {8} theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const upload = await pinata.upload.public.url("https://i.imgur.com/u4mGk5b.gif");
```
```typescript API {7-10} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const stream = await fetch("https://i.imgur.com/u4mGk5b.gif")
const arrayBuffer = await stream.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const file = new File([blob], "name.gif");
formData.append("file", file);
formData.append("network", "public");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
### base64
To upload a file in base64 simply turn the contents into a `buffer` that is passed into a `Blob`. Alternatively you can use the SDK for this as well using the [base64](/sdk/upload/public/base64) method.
```typescript SDK {8} theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const upload = await pinata.upload.public.base64("SGVsbG8gV29ybGQh");
```
```typescript API {7-11} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const buffer = Buffer.from("SGVsbG8gV29ybGQh", "base64");
const blob = new Blob([buffer]);
const file = new File([blob], "hello.txt");
formData.append("file", file);
formData.append("network", "public");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
## Resumable Uploads
The upload endpoint `https://uploads.pinata.cloud/v3/files` is fully [TUS](https://tus.io) compatible, so it can support larger files with the ability to resume uploads. Any file upload larger than 100MB needs to be uploaded through the TUS method, or through the legacy [/pinFileToIPFS](/api-reference/endpoint/ipfs/pin-file-to-ipfs) endpoint. The [SDK](/sdk) handles this automatically when you use `pinata.upload..file()` by checking the file size before uploading.
At this time folder uploads must go through [/pinFileToIPFS](/api-reference/endpoint/ipfs/pin-file-to-ipfs). Read the [Folder Guide](#folders) for more info!
```typescript {6} theme={null}
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const upload = await pinata.upload.public.file(massiveFile);
```
If you want to take advantage of resumable uploads then we would recommend using one of the [TUS clients](https://tus.io/implementations) and taking note of the following:
* Upload chunk size must be smaller than 50MB
* Instead of using the form data fields for `group_id` or `keyvalues` these can be passed directly into the upload metadata (see example below)
* Headers must include the Authorization with your [Pinata JWT](/account-managemnet/api-keys)
Here is an example of an upload to Pinata using the `tus-js-client`
```typescript tus-js-client [expandable] theme={null}
import * as tus from "tus-js-client";
async function resumeUpload(file) {
try {
const upload = new tus.Upload(file, {
endpoint: "https://uploads.pinata.cloud/v3/files",
chunkSize: 50 * 1024 * 1024, // 50MiB chunk size
retryDelays: [0, 3000, 5000, 10000, 20000],
onUploadUrlAvailable: async function () {
if (upload.url) {
console.log("Upload URL is available! URL: ", upload.url);
}
},
metadata: {
filename: "candyroad-demo.mp4", // name
filetype: "video/mp4",
group_id: "0192868e-6144-7685-9fc5-af68a1e48f29", // group ID
network: "public",
keyvalues: JSON.stringifiy({ env: "prod" }), // keyvalues
},
headers: { Authorization: `Bearer ${process.env.PINATA_JWT}` }, // auth header
uploadSize: fileStats.size,
onError: function (error) {
console.log("Failed because: " + error);
},
onProgress: function (bytesUploaded, bytesTotal) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
console.log(percentage + "%");
},
onSuccess: function () {
console.log("Upload completed!");
},
});
upload.start();
} catch (error) {
console.log(error);
}
}
```
## Upload by CAR File
You can upload content to Pinata using a raw CAR file. A **CAR (Content Addressed Archive)** file bundles together IPFS blocks in a portable format.
To upload a CAR file, set the `car` parameter to **true** in your upload request. CAR file uploads are supported across all Pinata upload methods.
```typescript SDK {4} theme={null}
const upload = await pinata.upload.public
.file(file)
.name("test.car")
.car()
```
```typescript API {15} theme={null}
const JWT = "YOUR_PINATA_JWT";
async function upload() {
try {
const formData = new FormData();
const file = new File(["test_car"], "test.car", { type: "application/vnd.ipld.car" });
formData.append("file", file);
formData.append("network", "public");
formData.append("name", "test.car");
formData.append("car", "true");
const request = await fetch("https://uploads.pinata.cloud/v3/files", {
method: "POST",
headers: {
Authorization: `Bearer ${JWT}`,
},
body: formData,
});
const response = await request.json();
console.log(response);
} catch (error) {
console.log(error);
}
}
```
Requirements
* Paid plan required - CAR uploads are not available on free plans
* Public network only - CAR files cannot be uploaded as private content
* Valid structure - Files must be properly formatted CAR files with valid IPFS blocks
* Single Root CID - Pinata does not support CAR files with multiple root CIDs
File Availability
CAR file processing is **asynchronous**. After a successful upload, Pinata validates the CAR file structure and IPFS blocks before preparing the file for indexing. A webhook notification is then sent to report the outcome, indicating whether the CAR file was successfully imported or if the process failed.
## Pin by CID
Another way you can upload content to Pinata is by transferring content that is already on IPFS. This could be CIDs that are on your own local IPFS node or another IPFS pinning service! You can do this with the “Import from IPFS” button in the web app, like so:
Or you can pin by CID with the SDK using the [cid](/sdk/upload/public/cid) method.
```typescript theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const pin = await pinata.upload.public.cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
```
This will result in a `request_id` and Pinata will start looking for the file. Progress can be checked by using the [queue](/sdk/files/public/queue) method.
```typescript theme={null}
import { PinataSDK } from "pinata-web3";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "example-gateway.mypinata.cloud",
});
const jobs = await pinata.files.public.queue().status("prechecking")
```
All possible filters are included in the API reference below, but these are the possible "status" filters:
Filter by the status of the job in the pinning queue (see potential statuses
below)
* `prechecking` Pinata is running preliminary validations on your pin request.
* `searching` Pinata is actively searching for your content on the IPFS network. This may take some time if your content is isolated.
* `retrieving` Pinata has located your content and is now in the process of retrieving it.
* `expired` Pinata wasn't able to find your content after a day of searching the IPFS network. Please make sure your content is hosted on the IPFS network before trying to pin again.
* `backfilled` Pinata can only search 250 files at a time per account, so if you have more than 250 items in your queue then the extra items will sit in a backfilled status. Once the queue goes down it will automatically start working on the next items in the backfill queue.
* `over_free_limit` Pinning this object would put you over the free tier limit. Please add a credit card to continue pinning content.
* `over_max_size` This object is too large of an item to pin. If you're seeing this, please contact us for a more custom solution.
* `invalid_object` The object you're attempting to pin isn't readable by IPFS nodes. Please contact us if you receive this, as we'd like to better understand what you're attempting to pin.
* `bad_host_node` You provided a host node that was either invalid or unreachable. Please make sure all provided host nodes are online and reachable.
## Predetermining the CID
If you find yourself in a position where you want to pre-determine the CID before uploading you can use a combination of the `ipfs-unixfx-importer` and `blockstore-core` libraries.
```typescript theme={null}
import { importer } from "ipfs-unixfs-importer";
import { MemoryBlockstore } from "blockstore-core/memory";
export const predictCID = async (file: File, version: 0 | 1 = 1) => {
try {
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const blockstore = new MemoryBlockstore();
let rootCid: any;
for await (const result of importer([{ content: buffer }], blockstore, {
cidVersion: version,
hashAlg: "sha2-256",
rawLeaves: version === 1,
})) {
rootCid = result.cid;
}
return rootCid.toString();
} catch (err) {
return err;
}
};
```
Usage will just require passing in a file object and the version of the CID you want to predict. Here is an example using a local file.
```typescript theme={null}
import fs from "fs"
const file = new File([fs.readFileSync("path/to-file")], "filename.extension");
const cid = await predictCID(file, 1);
console.log(cid);
```
## Peering with Pinata
If you run IPFS infrastructure and would like to peer with Pinata's nodes you can do so with the [Kubo](https://docs.ipfs.tech/reference/kubo/cli/) commands listed below. Rather than using full multiaddresses for our node IDs we use a DNS setup that is more stable and allows our infrastructure to be flexible.
```bash theme={null}
ipfs swarm connect /dnsaddr/bitswap.pinata.cloud
```
```bash theme={null}
ipfs config --json Peering.Peers '[{ "ID": "Qma8ddFEQWEU8ijWvdxXm3nxU7oHsRtCykAaVz8WUYhiKn", "Addrs": ["/dnsaddr/bitswap.pinata.cloud"] }]'
```
If you have any issues feel free to [reach out](mailto:team@pinata.cloud)!
## Web App
You can also use the **[Pinata App](https://app.pinata.cloud/)** to upload files. It’s as simple as clicking the “Add” button in the top right corner of the **[files page](https://app.pinata.cloud/ipfs/files)**. Select your file, give it a name, then upload. Once it's complete you’ll see it listed in the files page.
Start uploading by [signing up for a free account](https://app.pinata.cloud/register)!
# File Vectors (Beta)
Source: https://docs.pinata.cloud/files/vectors
The file vectors feature is still in beta and is only available on Private IPFS. Please contact the team at [team@pinata.cloud](mailto:team@pinata.cloud) if you have any issues.
## Overview
A common nusance when building AI apps is context embeddings. If you use a traditional stack you generall have to store an embedding, vectorize it, store the vector, then when you query a vector you'll get another reference to the file which you then have to fetch again. Pinata's solution is much more elegant. With Pinata's file vectoring you can upload a file and vector it at the same time.
```typescript theme={null}
const upload = await pinata.upload.private
.file(file)
.group("GROUP_ID")
.vectorize()
```
When it comes time to query a vector, you have the option to either list your query results and judge the matching score, or just return the highest scoring file itself.
```typescript theme={null}
const { data, contentType } = await pinata.files.private
.queryVectors({
groupId: "GROUP_ID",
query: "Hello World!",
returnFile: true
})
```
This enables develops to build AI applications faster and with less code!
### Pinata Vector Storage: Public Beta Limits
During the public beta, Pinata Vector Storage has the following limits:
* File Vectorization Limit: You can vectorize up to 10,000 files.
* Index Limit: You can create a maximum of 5 indexes, managed using Pinata Groups.
* Results Limit: You can query a vector and get a max of 20 files returned in one request.
**What this means**
You can organize your vector embeddings into up to 5 searchable indexes using Pinata Groups. Across all these groups, you can store a total of 10,000 vector embeddings corresponding to files stored on Pinata.
## Vectorizing Files
There are two ways you can vectorize file uploads, and with both options **files must be part of a group** in order for vectors and queries to work.
### Vectorize on Upload
If you use the [Files SDK](/sdk/getting-started) you can vectorize a file on upload.
```typescript {4} theme={null}
const upload = await pinata.upload.private
.file(file)
.group("GROUP_ID")
.vectorize()
```
### Vectorize After Upload
If you already have a file that's been uploaded and it's [part of a group](/files/file-groups) then you can vectorize it.
```typescript theme={null}
const update = await pinata.files.private.vectorize("FILE_ID")
```
## Querying Vectors
After a file has been vectorized and it's part of a group, you can query vectors for a given group.
```typescript theme={null}
const results = await pinata.files.private.queryVectors({
groupId: "52681e41-86f4-407b-8f79-33a7e7e5df68",
query: "Hello World"
})
```
This will return the following type:
```typescript theme={null}
type VectorizeQueryResponse = {
count: number;
matches: VectorQueryMatch[];
};
type VectorQueryMatch = {
file_id: string;
cid: string;
score: number;
};
```
### Returning the Top Match File
A unique feature in the SDK is if you pass in the `returnFile` flag then you will get the file and it's contents rather than just the reference to the file.
```typescript {4} theme={null}
const { data, contentType } = await pinata.files.private.queryVectors({
groupId: "52681e41-86f4-407b-8f79-33a7e7e5df68",
query: "Hello World",
returnFile: true
})
```
## Deleting Vectors
If at any point you need to delete vectors for a file you can do so with the `deleteVectors` method in the SDK.
```typescript theme={null}
const update = await pinata.files.private.deleteVectors("FILE_ID")
```
# Introduction
Source: https://docs.pinata.cloud/files/x402/intro
x402 is available on paid Pinata accounts. [Upgrade your account](https://app.pinata.cloud/billing) to access x402 monetization features.
Pinata's x402 enables you to monetize your private IPFS files by receiving USDC payments directly to your wallet. You set the price, you receive the payment.
## Overview
Pinata's x402 implementation enables:
* **Monetization of private IPFS content** using Payment Instructions
* **You receive payments directly** to your wallet address. Payments go to you.
* **Flexible payment requirements** configurable per file or group of files
* **USDC payments** on Base (mainnet) and Base Sepolia (testnet). USDC is currently the only supported token.
* **Gateway-level enforcement** through the x402 protocol
## Network Configuration
| Network | USDC Token Address | Use Case |
| ---------------------- | -------------------------------------------- | ----------------------- |
| Base (Mainnet) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | Production monetization |
| Base Sepolia (Testnet) | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | Testing payment flows |
## How It Works
1. **Upload** a private file to Pinata IPFS
2. **Create** a Payment Instruction with your desired payment requirements
3. **Attach** the CID of your private file to the Payment Instruction
4. **Share** your x402 gateway URL: `https://your-gateway.mypinata.cloud/x402/cid/{cid}`
5. **Requesters** make USDC payments through your gateway to access content
You must use your own dedicated Pinata gateway domain. Replace `your-gateway.mypinata.cloud` with your actual gateway domain throughout these examples.
## Example Workflow
```typescript theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "your-gateway.mypinata.cloud",
});
// 1. Upload a private file
const file = new File(["premium content"], "content.pdf", { type: "application/pdf" });
const upload = await pinata.upload.private.file(file);
// 2. Create a payment instruction
const instruction = await pinata.x402.createPaymentInstruction({
name: "Premium Content",
payment_requirements: [
{
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
pay_to: "0xYourWalletAddress", // YOU receive payments here
network: "base",
description: "Access fee",
amount: "10000", // $0.01 in USDC
},
],
});
// 3. Attach CID to payment instruction
await pinata.x402.addCid(instruction.data.id, upload.cid);
// 4. Share the x402 gateway URL
// https://your-gateway.mypinata.cloud/x402/cid/{cid}
```
## Why Use Payment Instructions
Payment Instructions enable you to monetize your private content by setting custom payment requirements and receiving payments directly to your wallet.
Key benefits:
* **You get paid** for your content. Set your own prices and receive USDC payments directly.
* **Flexible pricing** for different files or groups of files
* **Reusable payment instructions** that can be attached to multiple CIDs
* **Full control** over payment requirements, pricing, and access
## SDK Reference
Get started with x402 monetization
Create and manage payment instructions
Attach CIDs to payment instructions
TypeScript type definitions
## API Reference
For direct API access, see the [x402 API documentation](/api-reference/endpoint/x402/payment-instructions-list).
# Accessing Paid Content
Source: https://docs.pinata.cloud/files/x402/x402-accessing-paid-content
Learn how to access x402-protected content on Pinata.
## Overview
When attempting to access x402-protected content without a payment payload, the gateway returns payment requirements. After making a payment, requesters receive a payment proof that grants access to the content.
## The Payment Flow
The content creator provides their dedicated Pinata gateway URL. Replace `your-gateway.mypinata.cloud` in examples with the actual gateway domain provided.
### Step 1: Request the Content
Make a GET request to the x402 gateway URL:
```bash theme={null}
curl https://your-gateway.mypinata.cloud/x402/cid/bafkreih...
```
### Step 2: Receive Payment Requirements (402 Response)
The gateway returns HTTP 402 with payment details:
```json theme={null}
{
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"resource": "https://your-gateway.mypinata.cloud/x402/cid/bafkreih...",
"description": "Access fee",
"mimeType": "application/json",
"payTo": "0x6135561038E7C676473431842e586C8248276AED",
"maxTimeoutSeconds": 60,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": {
"name": "USD Coin",
"version": "2"
}
}
],
"error": "Provide a valid X-Payment header to access this content"
}
```
**Key Fields:**
* `network`: Blockchain network (e.g., `base`)
* `asset`: Token contract address (USDC on Base)
* `payTo`: Recipient wallet address (content creator receives payment here)
* `maxAmountRequired`: Payment amount in smallest unit (e.g., `10000` = 0.01 USDC)
### Step 3: Access Content with Payment Proof
After successful payment, include the `X-Payment` header in the request:
```bash theme={null}
curl https://your-gateway.mypinata.cloud/x402/cid/bafkreih... \
-H "X-Payment: eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3..."
```
The gateway will:
1. Validate the `X-Payment` header
2. Verify the payment proof
3. Check that amount, recipient, and network match
4. Serve the private content
**Successful Response:**
```
HTTP/200 OK
Content-Type: application/json
{
"your": "content"
}
```
## Using x402 Libraries
The x402 protocol has client libraries that automate the payment flow. First, set up your wallet:
## Setting Up Your Wallet
The x402 libraries require a Viem account or Coinbase Developer Platform wallet:
### Option 1: Viem Local Account
```typescript theme={null}
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
```
### Option 2: Coinbase CDP Wallet
```typescript theme={null}
import { Coinbase, Wallet } from "@coinbase/coinbase-sdk";
const coinbase = new Coinbase({
apiKeyName: "YOUR_API_KEY_NAME",
privateKey: "YOUR_PRIVATE_KEY",
});
const wallet = await Wallet.create();
const account = await wallet.getDefaultAddress();
```
## Using the Libraries
Once you have your wallet set up, use the x402 libraries to access paid content:
### @x402/fetch
```typescript theme={null}
import { wrapFetchWithPayment } from "@x402/fetch";
import { privateKeyToAccount } from "viem/accounts";
// Set up your wallet
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const fetchWithPayment = wrapFetchWithPayment(fetch, account);
// Automatically handles 402 response and payment
const response = await fetchWithPayment(
"https://your-gateway.mypinata.cloud/x402/cid/bafkreih..."
);
const content = await response.json();
console.log(content);
```
### @x402/axios
```typescript theme={null}
import { wrapAxiosWithPayment } from "@x402/axios";
import axios from "axios";
import { privateKeyToAccount } from "viem/accounts";
// Set up your wallet
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const axiosWithPayment = wrapAxiosWithPayment(axios, account);
// Automatically handles 402 response and payment
const response = await axiosWithPayment.get(
"https://your-gateway.mypinata.cloud/x402/cid/bafkreih..."
);
console.log(response.data);
```
## Technical Details (Optional)
Pinata uses Coinbase Facilitator to process x402 payments, which provides access to Coinbase's Discovery Bazaar network for broader content discovery.
## Understanding Payment Amounts
USDC uses 6 decimals, so amounts use the token's smallest unit:
| USD Amount | `maxAmountRequired` | Calculation |
| ---------- | ------------------- | ------------------- |
| \$0.01 | `10000` | \$0.01 × 1,000,000 |
| \$0.10 | `100000` | \$0.10 × 1,000,000 |
| \$1.00 | `1000000` | \$1.00 × 1,000,000 |
| \$10.00 | `10000000` | \$10.00 × 1,000,000 |
**Formula:** USD Amount × 1,000,000 = token amount
## Error Handling
### 402 Payment Required
Payment has not been made yet. Follow the payment flow above.
### 403 Forbidden
Payment proof is invalid or expired. Make a new payment.
### 404 Not Found
The CID doesn't exist or isn't attached to a payment instruction.
### 500 Internal Server Error
Gateway or facilitator error. Contact the content creator.
## Network Support
**USDC is currently the only supported token.**
| Network | Status | Token | Use Case |
| ---------------------- | ----------- | --------------------------------------------------- | ---------- |
| Base (Mainnet) | ✅ Available | USDC (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) | Production |
| Base Sepolia (Testnet) | ✅ Available | USDC (`0x036CbD53842c5426634e7929541eC2318f3dCF7e`) | Testing |
## Best Practices
1. **Handle 402 responses gracefully**: Display payment requirements clearly
2. **Use the x402 libraries**: They handle the complex payment flow automatically
3. **Test on Base Sepolia first**: Verify integration before using mainnet
4. **Store payment proofs**: If accessing content multiple times (check expiry)
5. **Monitor USDC balance**: Ensure sufficient funds for payments
## Example: Complete Integration
```typescript theme={null}
import { wrapFetchWithPayment } from "@x402/fetch";
import { privateKeyToAccount } from "viem/accounts";
// Set up your wallet
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const fetchWithPayment = wrapFetchWithPayment(fetch, account);
// Access paid content
async function accessPaidContent(cid: string) {
try {
const url = `https://your-gateway.mypinata.cloud/x402/cid/${cid}`;
const response = await fetchWithPayment(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const content = await response.json();
console.log("Content accessed successfully:", content);
return content;
} catch (error) {
console.error("Failed to access content:", error);
throw error;
}
}
// Usage
accessPaidContent("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4");
```
## Resources
* [@x402/fetch NPM Package](https://www.npmjs.com/package/@x402/fetch)
* [@x402/axios NPM Package](https://www.npmjs.com/package/@x402/axios)
* [Viem Documentation](https://viem.sh)
* [Coinbase Developer Platform](https://docs.cdp.coinbase.com/)
## Support
Need help accessing paid content? Contact the content creator or reach out to [team@pinata.cloud](mailto:team@pinata.cloud).
# Payment Instructions Guide
Source: https://docs.pinata.cloud/files/x402/x402-payment-instructions
Payment Instructions are the core mechanism for monetizing private IPFS content through the x402 protocol. You define payment requirements, attach them to your private files, and receive payments directly to your wallet when requesters access your content.
This guide covers how to create, manage, and use Payment Instructions to monetize your content.
## Prerequisites
* Paid Pinata account
* [Pinata SDK](/sdk/getting-started) installed
* Private files uploaded to Pinata IPFS
## Understanding Payment Instructions
A Payment Instruction is a reusable configuration that defines:
* **Payment requirements** - The amount, token, and recipient for payments
* **Network configuration** - Which blockchain network to use
* **Metadata** - Name and description for organization
The `payment_requirements` field is an array, allowing you to define multiple payment options. Each payment instruction can have multiple requirements, giving requesters flexibility in how they pay.
### Payment Instruction Structure
```typescript theme={null}
type PaymentInstruction = {
id: string;
version: number;
payment_requirements: PaymentRequirement[];
name: string;
description?: string;
created_at: string;
};
type PaymentRequirement = {
asset: string; // USDC token contract address
pay_to: string; // Your wallet address
network: "base" | "base-sepolia" | "eip155:8453" | "eip155:84532";
amount: string; // Amount in USDC smallest units
description?: string;
};
```
The `version` field is automatically managed by Pinata and increments with each update. You don't need to set this field when creating or updating payment instructions.
## Networks and Tokens
Currently supported configurations. **USDC is the only supported token at this time.**
### Base Mainnet (Production)
* **Network**: `base` (or `eip155:8453`)
* **USDC Token Address**: `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`
* **Use for**: Production monetization
### Base Sepolia (Testing)
* **Network**: `base-sepolia` (or `eip155:84532`)
* **USDC Token Address**: `0x036CbD53842c5426634e7929541eC2318f3dCF7e`
* **Use for**: Testing payment flows
## Complete Workflow
```typescript theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "your-gateway.mypinata.cloud",
});
```
### Step 1: Upload Private Content
First, upload your file as private to Pinata:
```typescript theme={null}
const file = new File(["file contents"], "premium-content.pdf", {
type: "application/pdf",
});
const upload = await pinata.upload.private.file(file);
const cid = upload.cid;
```
**Important:** The file must be uploaded to the `private` network to be monetized with x402.
### Step 2: Create Payment Instruction
Create a payment instruction with your desired requirements:
```typescript theme={null}
const instruction = await pinata.x402.createPaymentInstruction({
name: "Premium PDF Access",
description: "One-time payment for PDF access",
payment_requirements: [
{
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
pay_to: "0x6135561038E7C676473431842e586C8248276AED", // YOU receive payments here
network: "base",
description: "Access fee",
amount: "10000", // $0.01 in USDC
},
],
});
const instructionId = instruction.data.id;
```
### Step 3: Attach CID to Payment Instruction
Link your private file to the payment instruction:
```typescript theme={null}
await pinata.x402.addCid(instructionId, cid);
```
### Step 4: Share x402 Gateway URL
Your content is now monetized and accessible at:
```
https://your-gateway.mypinata.cloud/x402/cid/{cid}
```
## Managing Payment Instructions
### Listing Instructions
View all your payment instructions with pagination:
```typescript theme={null}
const instructions = await pinata.x402.listPaymentInstructions({ limit: 20 });
```
Filter by specific criteria:
```typescript theme={null}
// Find instruction for a specific CID
const byCid = await pinata.x402.listPaymentInstructions({ cid: "bafkreih..." });
// Filter by name
const byName = await pinata.x402.listPaymentInstructions({ name: "Premium" });
// Get specific instruction by ID
const byId = await pinata.x402.listPaymentInstructions({ id: "019a2b6a..." });
```
### Getting a Single Instruction
```typescript theme={null}
const instruction = await pinata.x402.getPaymentInstruction(instructionId);
```
### Updating Instructions
Modify payment requirements for all attached CIDs:
```typescript theme={null}
const updated = await pinata.x402.updatePaymentInstruction(instructionId, {
payment_requirements: [
{
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
pay_to: "0x6135561038E7C676473431842e586C8248276AED",
network: "base",
amount: "50000", // Updated to $0.05
},
],
});
```
### Deleting Instructions
Before deleting, remove all CID attachments:
```typescript theme={null}
// 1. List attached CIDs
const { data: cidData } = await pinata.x402.listCids(instructionId);
// 2. Remove each CID
for (const cid of cidData.cids) {
await pinata.x402.removeCid(instructionId, cid);
}
// 3. Delete the instruction
await pinata.x402.deletePaymentInstruction(instructionId);
```
## CID Management
### CID and Payment Instruction Relationship
* **One Payment Instruction → Multiple CIDs**: A single payment instruction can be attached to multiple CIDs
* **One CID → One Payment Instruction**: Each CID can only have one payment instruction at a time
* **Multiple Requirements**: A payment instruction can have multiple payment requirements in its `payment_requirements` array
* Updating the instruction affects all attached CIDs
### Managing CID Attachments
```typescript theme={null}
// List all CIDs for an instruction
const cids = await pinata.x402.listCids(instructionId);
// Add a CID
await pinata.x402.addCid(instructionId, cid);
// Remove a CID
await pinata.x402.removeCid(instructionId, cid);
```
## Gateway Behavior
When a requester accesses your x402 gateway URL without payment, the gateway returns payment requirements:
### Without Payment (402 Response)
```json theme={null}
{
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"resource": "https://gateway/x402/cid/bafkreig...",
"payTo": "0x6135561038E7C676473431842e586C8248276AED",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": {
"name": "USD Coin",
"version": "2"
}
}
],
"error": "Provide a valid X-Payment header to access this content"
}
```
### With Valid Payment
The gateway:
1. Validates the `X-Payment` header
2. Verifies the payment proof
3. Checks amount, recipient, and network match requirements
4. Serves the private content
## Best Practices
### Payment Amounts
The `amount` uses USDC's smallest unit. USDC has 6 decimals, so to convert USD to the token amount, multiply by 1,000,000:
| USD Amount | `amount` | Calculation |
| ---------- | ------------ | ------------------- |
| \$0.01 | `"10000"` | \$0.01 × 1,000,000 |
| \$0.10 | `"100000"` | \$0.10 × 1,000,000 |
| \$1.00 | `"1000000"` | \$1.00 × 1,000,000 |
| \$5.00 | `"5000000"` | \$5.00 × 1,000,000 |
| \$10.00 | `"10000000"` | \$10.00 × 1,000,000 |
**Formula:** USD Amount × 1,000,000 = token amount
**Tips:**
* Consider transaction costs when setting minimum amounts
* Test on Base Sepolia before deploying to mainnet
### Instruction Organization
* Use descriptive names for easy management
* Group similar content under one instruction
* Document your payment requirements clearly
### Security Considerations
* Only private files can be monetized
* Ensure your `pay_to` address is correct. **You receive payments directly to this address.**
* Test payment flows on testnet first
* Monitor your gateway analytics
## Troubleshooting
### Common Issues
**409 Conflict when deleting instruction**
* Solution: Remove all CID attachments first
**400 Bad Request when creating instruction**
* Check `asset` and `pay_to` addresses start with `0x`
* Verify `network` is `base`, `base-sepolia`, `eip155:8453`, or `eip155:84532`
* Ensure `amount` is provided
**404 Not Found when accessing CID**
* Verify the CID exists and is private
* Check the CID is attached to a payment instruction
* Ensure the gateway URL format is correct
## SDK Reference
* [List Payment Instructions](/sdk/x402/payment-instructions/list)
* [Create Payment Instruction](/sdk/x402/payment-instructions/create)
* [Get Payment Instruction](/sdk/x402/payment-instructions/get)
* [Update Payment Instruction](/sdk/x402/payment-instructions/update)
* [Delete Payment Instruction](/sdk/x402/payment-instructions/delete)
* [List CIDs](/sdk/x402/cids/list)
* [Add CID](/sdk/x402/cids/add)
* [Remove CID](/sdk/x402/cids/remove)
## API Reference
For direct API access, see the [x402 API documentation](/api-reference/endpoint/x402/payment-instructions-list).
# Quick Start
Source: https://docs.pinata.cloud/files/x402/x402-quick-start
Get started monetizing your private IPFS files with x402 in just a few minutes.
With x402, you monetize your private files by setting payment requirements. Payments go directly to your wallet. You control the price and receive the funds.
x402 is available on paid Pinata accounts. [Upgrade your account](https://app.pinata.cloud/billing) to access x402 monetization features.
## Prerequisites
* Paid Pinata account
* [Pinata SDK](/sdk/getting-started) installed
* Ethereum wallet address to receive payments
## Step 1: Upload a Private File
First, upload a file to Private IPFS:
```typescript theme={null}
import { PinataSDK } from "pinata";
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT!,
pinataGateway: "your-gateway.mypinata.cloud",
});
const file = new File(["file contents"], "your-file.pdf", {
type: "application/pdf",
});
const upload = await pinata.upload.private.file(file);
const cid = upload.cid;
```
Save the returned `cid` for the next step.
## Step 2: Create a Payment Instruction
Define your payment requirements:
```typescript theme={null}
const instruction = await pinata.x402.createPaymentInstruction({
name: "Premium Content Access",
description: "One-time payment for file access",
payment_requirements: [
{
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
pay_to: "YOUR_WALLET_ADDRESS", // YOU receive payments here
network: "base",
description: "Access fee",
amount: "10000", // $0.01 in USDC
},
],
});
const instructionId = instruction.data.id;
```
**Understanding Payment Amounts:** The `amount` uses USDC's smallest unit. USDC has 6 decimals, so to convert USD to the token amount, multiply by 1,000,000:
| USD Amount | `amount` | Calculation |
| ---------- | ------------ | ------------------- |
| \$0.01 | `"10000"` | \$0.01 × 1,000,000 |
| \$0.10 | `"100000"` | \$0.10 × 1,000,000 |
| \$1.00 | `"1000000"` | \$1.00 × 1,000,000 |
| \$5.00 | `"5000000"` | \$5.00 × 1,000,000 |
| \$10.00 | `"10000000"` | \$10.00 × 1,000,000 |
**Formula:** USD Amount × 1,000,000 = token amount
**Networks:**
* Production: Use `"base"` (or `"eip155:8453"`) with `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`
* Testing: Use `"base-sepolia"` (or `"eip155:84532"`) with `0x036CbD53842c5426634e7929541eC2318f3dCF7e`
Save the returned `id` for the next step.
## Step 3: Attach CID to Payment Instruction
Link your private file to the payment instruction:
```typescript theme={null}
await pinata.x402.addCid(instructionId, cid);
```
## Step 4: Share Your Monetized Content
Your file is now monetized! Share this URL with requesters:
```
https://your-gateway.mypinata.cloud/x402/cid/{cid}
```
Replace `your-gateway.mypinata.cloud` with your actual dedicated Pinata gateway domain (e.g., `my-gateway.mypinata.cloud`), and `{cid}` with your file's CID.
## Step 5: Test the Payment Flow
When a requester accesses your URL without payment, the gateway returns a 402 response with payment requirements:
```typescript theme={null}
const response = await fetch(
`https://your-gateway.mypinata.cloud/x402/cid/${cid}`
);
const paymentRequirements = await response.json();
console.log(paymentRequirements);
```
Response:
```json theme={null}
{
"x402Version": 1,
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "10000",
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"payTo": "YOUR_WALLET_ADDRESS",
"resource": "https://your-gateway.mypinata.cloud/x402/cid/{cid}"
}],
"error": "Provide a valid X-Payment header to access this content"
}
```
After payment is made, requesters can access the file by including the payment proof in the `X-Payment` header.
## Next Steps
* **Manage Multiple Files**: Attach multiple CIDs to the same payment instruction
* **Update Pricing**: Use [`updatePaymentInstruction`](/sdk/x402/payment-instructions/update)
* **Monitor Access**: Check your gateway analytics to track paid downloads
* **Requester Guide**: Learn how requesters access paid content in the [Accessing Paid Content](/files/x402/x402-accessing-paid-content) guide
## Common Issues
**404 Not Found**: Verify the CID is private and correctly attached to a payment instruction
**403 Forbidden**: Check that your API key has the required permissions
**409 Conflict**: If deleting a payment instruction fails, remove all CID attachments first
Need help? Contact [team@pinata.cloud](mailto:team@pinata.cloud)
## API Reference
For direct API access, see the [x402 API documentation](/api-reference/endpoint/x402/payment-instructions-list).
# x402 Services
Source: https://docs.pinata.cloud/files/x402/x402-services
## Overview
The x402 Services provide streamlined endpoints for crypto-based content monetization using the @x402/axios or @x402/fetch libraries.
## Endpoints
For detailed API specifications, see the API Reference:
Upload files with payment
Access private files with payment
## See Also
For more granular control over payment configurations, see the [Payment Instructions API](/files/x402/x402-payment-instructions), which offers:
* **Flexible pricing** - Define custom payment requirements
* **Separate management** - Configure payments independently from files
* **One-to-many** - Attach one payment instruction to multiple files
# Astro
Source: https://docs.pinata.cloud/frameworks/astro
Get started using Pinata with Astro
This guide will walk you through setting up Pinata with Astro
## Create an API Key and get Gateway URL
To create an API key, visit the [Keys Page](https://app.pinata.cloud/developers/keys) and click the "New Key" button in the top right. Once you do that you can select if you want your key to be admin or if you want to scope the privileges of the keys to certain endpoints or limit the number of uses. Make those selections, then give the key a name at the bottom, and click create key.
If you are just getting started we recommend using Admin privileges, then move
to scope keys as you better understand your needs
Once you have created the keys you will be shown your API Key Info. This will contain your **Api Key**, **API Secret**, and your **JWT**. Click "Copy All" and save them somewhere safe!
The API keys are only shown once, be sure to copy them somewhere safe!
After you have your API key, you will want to get your Gateway domain. When you create a Pinata account, you'll automatically have a Gateway created for you! To see it, simply visit the [Gateways Page](https://app.pinata.cloud/gateway) see it listed there.
The gateway domains are randomly generated and might look something like this:
```
aquamarine-casual-tarantula-177.mypinata.cloud
```
## Setup Astro
To create a new Astro project go ahead and run this command in the terminal:
```bash theme={null}
npm create astro@latest pinata-astro
```
You can select which options you prefer, but for this exmaple we'll use the following:
```
tmpl How would you like to start your new project?
Empty
ts Do you plan to write TypeScript?
Yes
use How strict should TypeScript be?
Strict
deps Install dependencies?
Yes
git Initialize a new git repository?
Yes
```
After completing the project setup we can go ahead and `cd` into the repo and install `pinata`.
```bash theme={null}
cd pinata-astro && npm i pinata
```
Since we want to keep our API key private we will need to make sure our code is deployed server side, and we can use several different adapter options which you can view [here](https://docs.astro.build/en/guides/server-side-rendering/). We also need a UI framework to handle our upload form, and there are many to choose from [here](https://docs.astro.build/en/guides/framework-components/). We'll use `vercel` and `svelte` for this tutorial, and you can install them like so.
```bash theme={null}
npx astro add vercel svelte
```
This should install the dependencies and alter the `astro.config.mjs` for us.
## Setup Pinata
In the `src` folder make a new folder called `utils`, and inside there make a file called `pinata.ts` with the following contents:
```typescript src/utils/pinata.ts theme={null}
import { PinataSDK } from "pinata";
export const pinata = new PinataSDK({
pinataJwt: import.meta.env.PINATA_JWT,
pinataGateway: import.meta.env.GATEWAY_URL,
});
```
This will create and export an instance of the Files SDK using environment variables, and to set those up make a new file at the root of the project called `.env` with the following values:
```
PINATA_JWT= # The Pinata JWT API key we got earlier
GATEWAY_URL= # The Gateway domain we grabbed earlier, formatting just as we copied it from the app
```
## Create Client Side Form
In the `src` folder make another folder called `components`, then inside there make file called `UploadForm.svelte`.
```html src/components/UploadForm.svelte theme={null}
```
This component creates a `