# 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.

<img />

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.

<Warning>Once API keys have been created, you will not be able to see the secret or JWT again</Warning>

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.

<img />

At any point you can delete an API key by clicking on the Revoke button

<img />


# 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.

<img />

## 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.

<Warning>
  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%.
</Warning>

<img />

## 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.

<Note>Pinata currently only accepts standard debit and credit cards</Note>

<img />

## Plan Selection

From the plan selection you can choose a plan that fits your need the most, whether that be upgrading or downgrading.

<Note>
  If you upgrade in the middle of a billing cycle, then you will only be charged
  the prorated amount
</Note>

<img />


# 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

<Warning>
  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.
</Warning>

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.

![webhooks create](https://docs.mypinata.cloud/ipfs/bafkreig535s7dg4gokb37cl3khns7uaq5sqauyzdur2picd76xfzowd5m4)

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.

<Tip>
  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.
</Tip>

![webhooks form](https://docs.mypinata.cloud/ipfs/bafkreicze5g4n2z7mr4r6o4p6o25lqbswsiytodqe6lwblzk2qmir6bpei)

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!

![webhooks dashboard](https://docs.mypinata.cloud/ipfs/bafybeicvtsffigooqn26s5zpgligxwjodlespt6a4lyfpzljn4h2ypu2jy)

## Event Catalog

Check out the link below to browse all the available webhook events you can subscribe to!

<Card title="Webhook Event Catalog" href="https://event-types.pinata.cloud/" icon="link">
  Visit our Svix page for all Pinata Webhook events!
</Card>

## 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.

![webhook secret](https://docs.mypinata.cloud/ipfs/bafybeid6jzbykbdoo742rzyq2l6bswahdxbnl7fmdgcraanjeagtokoeb4)

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



<Note>Workspaces is only available on the [Picnic and Fiesta plans](https://pinata.cloud/pricing)</Note>

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.

<iframe title="YouTube video player" />

## Inviting Members

<Note>
  At this time only Workspace Owners can invite members
</Note>

To get started, login with a paid account and click on the profile button in the top right, then select "Workspaces."

<img />

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.

<img />

## 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.

<img />

## Removing Members

<Note>
  At this time only Workspace Owners can remove members
</Note>

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!

<img />


# HTTP API
Source: https://docs.pinata.cloud/agents/api

Authenticate, manage agents, and talk to gateways over HTTP

The Agents API is the same API the dashboard uses. Everything in the UI is exposed at `agents.pinata.cloud` - including agent management, secrets, skills, channels, snapshots, custom domains, devices, and engine runtime config.

Full OpenAPI reference: [agents.pinata.cloud/openapi](https://agents.pinata.cloud/openapi) (also linked from the Support → OpenAPI Docs item in the sidebar).

## Base URLs

There are two surfaces, on purpose:

| Surface        | Host                            | Auth          | What it's for                                              |
| -------------- | ------------------------------- | ------------- | ---------------------------------------------------------- |
| **Management** | `agents.pinata.cloud`           | Pinata JWT    | Creating agents, managing secrets, browsing templates      |
| **Per-agent**  | `{agentId}.agents.pinata.cloud` | Gateway token | Talking to a specific agent (chat, routes, files, devices) |

The same agent sub-routes (`/v0/agents/{agentId}/...`) are mounted on both. Use the management host when you have the workspace owner's JWT; use the per-agent host when you only have the gateway token.

## Authentication

Three credentials, used in different contexts:

### Pinata JWT

Standard Pinata API key (`bearerAuth` in the OpenAPI spec). Used for all management routes (`/v0/agents`, `/v0/secrets`, `/v0/skills`, `/v0/templates`, etc.).

```http theme={null}
Authorization: Bearer <PINATA_JWT>
```

Create one in your [Account → API Keys](/account-management/api-keys).

### Gateway token

Per-agent token (`gatewayToken` in the OpenAPI spec) used for the agent's own subdomain. Read it from the agent's **Danger** page or `GET /v0/agents/{agentId}/gateway-token`. Rotate it from the same page or `POST /v0/agents/{agentId}/gateway-token/rotate`.

Passing the gateway token:

```http theme={null}
Authorization: Bearer <GATEWAY_TOKEN>
```

```http theme={null}
?token=<GATEWAY_TOKEN>
```

```http theme={null}
Cookie: gw_token=<GATEWAY_TOKEN>
```

<Warning>
  The gateway token grants full access to the agent's container - console, files, routes, everything. Treat it like a server credential.
</Warning>

### Git Basic auth

Used only by the Git Smart HTTP endpoints (`/v0/agents/{agentId}/git/...`). Sent as HTTP Basic auth - username is ignored, password is the gateway token. The **Copy with Token** button on the Files tab embeds this for you.

### Platform JWT (for skills)

Inside an agent, the `@pinata/platform` skill can exchange the gateway token for a short-lived (1 hour) platform JWT. That JWT then unlocks the management-domain API (create secrets, install skills, etc.) on the agent's behalf - so an agent can self-modify without ever seeing the user's Pinata JWT.

```bash theme={null}
# From inside the agent container:
curl -X POST \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  https://{agentId}.agents.pinata.cloud/v0/platform/token
```

## Quick Examples

All examples below use `PINATA_JWT` (from `https://app.pinata.cloud`) or `GATEWAY_TOKEN` (from the agent's Danger page).

### List your agents

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents
```

### Create an agent

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Agent",
    "description": "Personal assistant",
    "emoji": "🤖",
    "engine": "openclaw",
    "skillCids": ["@pinata/api"],
    "secretIds": ["secret-id-1"]
  }' \
  https://agents.pinata.cloud/v0/agents
```

`engine` is optional and defaults to `openclaw`. Pass `hermes` for the opinionated engine — see [Concepts → Engine](/agents/concepts#engine). For chat-oriented engines, the dashboard exposes channel setup during create, and engines that support create-time channel bootstrap can accept a `channels` object on this request so Telegram, Slack, or Discord come up with the agent without a post-create gateway restart. Each channel object accepts:

| Field       | Type             |      Required      | Notes                                                                       |
| ----------- | ---------------- | :----------------: | --------------------------------------------------------------------------- |
| `botToken`  | string           | yes (all channels) | Bot token from the platform. Slack uses the `xoxb-` bot token.              |
| `appToken`  | string           |  yes (Slack only)  | Slack `xapp-` app-level token for Socket Mode.                              |
| `dmPolicy`  | string           |      optional      | `open` (anyone can DM) or `pairing` (must be approved). Defaults to `open`. |
| `allowFrom` | array of strings |      optional      | Allow-list of platform user IDs that can DM the bot.                        |

These are the same fields accepted by the per-channel `POST /v0/agents/{agentId}/channels/{channel}` endpoint and by `channels` in [`manifest.json`](/agents/manifest#channels).

To list engines enabled for the current deployment:

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/engines
```

Returns `{ "engines": [ { "id": "openclaw", "label": "OpenClaw" }, { "id": "hermes", "label": "Hermes" } ] }`, depending on which engines are enabled for the deployment.

Returns `201` with the created agent. Returns `403` if you're at the agent limit.

### Get agent details

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID
```

Response includes `agent`, `processStatus`, `skills`, `secrets`, `snapshots`, and `portForwarding`.

### Restart the gateway

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/restart
```

### Run a shell command

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{"command":"ls workspace","cwd":"/home/node/clawd"}' \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/console/exec
```

Returns `{ stdout, stderr, exitCode, command, timestamp }`.

### Read a file

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  "https://agents.pinata.cloud/v0/agents/$AGENT_ID/files?path=workspace/IDENTITY.md"
```

### Upload a file

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d "{\"filename\":\"report.pdf\",\"contentBase64\":\"$(base64 -w0 < report.pdf)\"}" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/files/upload
```

Returns `{ path, filename, size }`. File limit matches the read endpoint (a few MB).

### Snapshot now

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/snapshots/sync
```

### Reset to a snapshot

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{"snapshotCid":"QmXyz..."}' \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/snapshots/reset
```

### Read the latest logs

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/logs
```

Returns the last 100 lines as a single string.

### Validate manifest / config

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d "{\"config\":$(jq -Rs . < manifest.json)}" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/config/validate
```

## Files

| Endpoint                                 | Method         | Notes                                                                               |
| ---------------------------------------- | -------------- | ----------------------------------------------------------------------------------- |
| `/v0/agents/{agentId}/files?path=<path>` | `GET`          | Read any file from the container                                                    |
| `/v0/agents/{agentId}/files/upload`      | `POST`         | Upload a base64 file into `<workspace>/uploads/` (max \~few MB, `413` if too large) |
| `/v0/agents/{agentId}/snapshots`         | `GET`          | Latest 10 snapshots                                                                 |
| `/v0/agents/{agentId}/snapshots/sync`    | `GET` / `POST` | Get sync status / take a snapshot now                                               |
| `/v0/agents/{agentId}/snapshots/reset`   | `POST`         | Reset to a specific snapshot CID                                                    |

## Custom Domains

| Endpoint                                         | Method           | Notes                                              |
| ------------------------------------------------ | ---------------- | -------------------------------------------------- |
| `/v0/agents/{agentId}/domains`                   | `GET`            | List subdomains and custom domains                 |
| `/v0/agents/{agentId}/domains`                   | `POST`           | Register a subdomain or custom domain              |
| `/v0/agents/{agentId}/domains/challenge`         | `POST`           | Get the TXT verification value for a custom domain |
| `/v0/agents/{agentId}/domains/{domainId}`        | `PUT` / `DELETE` | Update port/protected or remove                    |
| `/v0/agents/{agentId}/domains/{domainId}/verify` | `POST`           | Verify ownership and provision SSL                 |

See [Routes & Domains](/agents/routes) for the full workflow.

## Devices

| Endpoint                                           | Method | Notes                             |
| -------------------------------------------------- | ------ | --------------------------------- |
| `/v0/agents/{agentId}/devices`                     | `GET`  | List pending and paired devices   |
| `/v0/agents/{agentId}/devices/{requestId}/approve` | `POST` | Approve a specific device         |
| `/v0/agents/{agentId}/devices/approve-all`         | `POST` | Bulk-approve every pending device |

## Streaming logs

The Logs tab streams over WebSocket on the agent's subdomain:

```
wss://{agentId}.agents.pinata.cloud/v0/logs?token=<GATEWAY_TOKEN>
```

Each message is a JSON object: `{ timestamp, level, source, message }`. Filter on the client by `level` (`TRACE` ... `FATAL`) and free-text on `message`.

## Error Format

Errors return JSON with a single `error` field:

```json theme={null}
{ "error": "Validation failed: skills exceeds maximum of 10" }
```

Status codes follow HTTP semantics - `400` for validation, `401` for missing auth, `403` for plan/permission issues, `404` for missing resources, `409` for conflicts (duplicate secret name, both subdomain and customDomain provided, etc.), `413` for upload size, `500` for internal failures, `503` when an upstream (Convex, Pinata storage) is unreachable.

See [Errors](/agents/errors) for the full code-by-code reference.

## Endpoint Quick Reference

Compact index of every endpoint, grouped by purpose. `JWT` = Pinata JWT, `GW` = gateway token (per-agent host). All paths under `agents.pinata.cloud`.

### Agents

| Method   | Path                                            |   Auth   | Description                                         |
| -------- | ----------------------------------------------- | :------: | --------------------------------------------------- |
| `GET`    | `/v0/agents`                                    |    JWT   | List your agents                                    |
| `POST`   | `/v0/agents`                                    |    JWT   | Create an agent                                     |
| `GET`    | `/v0/agents/{agentId}`                          | JWT / GW | Agent detail (skills, secrets, snapshots, routes)   |
| `DELETE` | `/v0/agents/{agentId}`                          |    JWT   | Delete the agent                                    |
| `POST`   | `/v0/agents/{agentId}/restart`                  | JWT / GW | Restart the gateway                                 |
| `GET`    | `/v0/agents/{agentId}/logs`                     | JWT / GW | Last 100 log lines                                  |
| `GET`    | `/v0/agents/engines`                            |    JWT   | List available engines (`openclaw`, `hermes`, etc.) |
| `GET`    | `/v0/agents/{agentId}/available-agent-versions` | JWT / GW | Versions you can upgrade to                         |
| `POST`   | `/v0/agents/{agentId}/scripts/retry`            | JWT / GW | Re-run `build` then `start`                         |
| `GET`    | `/v0/agents/{agentId}/update`                   | JWT / GW | Check for OpenClaw updates                          |
| `POST`   | `/v0/agents/{agentId}/update`                   | JWT / GW | Apply an OpenClaw update                            |

### Auth

| Method | Path                                        | Auth | Description                                  |
| ------ | ------------------------------------------- | :--: | -------------------------------------------- |
| `GET`  | `/v0/agents/{agentId}/gateway-token`        |  JWT | Get the current gateway token                |
| `POST` | `/v0/agents/{agentId}/gateway-token/rotate` |  JWT | Rotate it                                    |
| `POST` | `/v0/agents/{agentId}/platform/token`       |  GW  | Exchange gateway token for a 1h platform JWT |
| `GET`  | `/v0/agents/{agentId}/platform/whoami`      |  GW  | Agent's own identity                         |

### Secrets

| Method   | Path                                      |   Auth   | Description                         |
| -------- | ----------------------------------------- | :------: | ----------------------------------- |
| `GET`    | `/v0/secrets`                             |    JWT   | List your secrets                   |
| `POST`   | `/v0/secrets`                             |    JWT   | Create a secret                     |
| `PUT`    | `/v0/secrets/{id}`                        |    JWT   | Update value                        |
| `DELETE` | `/v0/secrets/{id}`                        |    JWT   | Delete                              |
| `POST`   | `/v0/agents/{agentId}/secrets`            | JWT / GW | Attach existing secrets to an agent |
| `DELETE` | `/v0/agents/{agentId}/secrets/{secretId}` | JWT / GW | Detach a secret                     |

### Skills

| Method   | Path                                           |   Auth   | Description                        |
| -------- | ---------------------------------------------- | :------: | ---------------------------------- |
| `GET`    | `/v0/skills`                                   |    JWT   | List your installed skills         |
| `POST`   | `/v0/skills`                                   |    JWT   | Register a new skill from a folder |
| `GET`    | `/v0/skills/{skillId}/versions`                |    JWT   | List versions                      |
| `POST`   | `/v0/skills/{skillId}/versions`                |    JWT   | Publish a new version              |
| `DELETE` | `/v0/skills/{skillCid}`                        |    JWT   | Remove from library                |
| `GET`    | `/v0/clawhub`                                  |    JWT   | Browse the community hub           |
| `GET`    | `/v0/clawhub/{slug}`                           |    JWT   | Hub skill detail                   |
| `POST`   | `/v0/clawhub/{hubSkillId}/install`             |    JWT   | Install a hub skill                |
| `GET`    | `/v0/agents/{agentId}/skills`                  | JWT / GW | Skills attached to this agent      |
| `POST`   | `/v0/agents/{agentId}/skills`                  | JWT / GW | Attach                             |
| `DELETE` | `/v0/agents/{agentId}/skills/{skillId}`        | JWT / GW | Detach                             |
| `GET`    | `/v0/agents/{agentId}/skills/updates`          | JWT / GW | Check for skill updates            |
| `POST`   | `/v0/agents/{agentId}/skills/{skillId}/update` | JWT / GW | Bump a skill on this agent         |

### Channels

| Method   | Path                                      |   Auth   | Description                                         |
| -------- | ----------------------------------------- | :------: | --------------------------------------------------- |
| `GET`    | `/v0/agents/{agentId}/channels`           | JWT / GW | Status of all channels                              |
| `POST`   | `/v0/agents/{agentId}/channels/{channel}` | JWT / GW | Configure (`telegram`/`slack`/`discord`/`whatsapp`) |
| `DELETE` | `/v0/agents/{agentId}/channels/{channel}` | JWT / GW | Remove                                              |

### Routes & domains

| Method   | Path                                             |   Auth   | Description                           |
| -------- | ------------------------------------------------ | :------: | ------------------------------------- |
| `GET`    | `/v0/agents/{agentId}/port-forwarding`           | JWT / GW | List path routes                      |
| `PUT`    | `/v0/agents/{agentId}/port-forwarding`           | JWT / GW | Replace path routes                   |
| `GET`    | `/v0/agents/{agentId}/domains`                   | JWT / GW | List domains and subdomains           |
| `POST`   | `/v0/agents/{agentId}/domains`                   | JWT / GW | Register a subdomain or custom domain |
| `POST`   | `/v0/agents/{agentId}/domains/challenge`         | JWT / GW | Get TXT challenge for a custom domain |
| `PUT`    | `/v0/agents/{agentId}/domains/{domainId}`        | JWT / GW | Update port/protected                 |
| `DELETE` | `/v0/agents/{agentId}/domains/{domainId}`        | JWT / GW | Remove                                |
| `POST`   | `/v0/agents/{agentId}/domains/{domainId}/verify` | JWT / GW | Verify ownership + provision SSL      |

### Tasks

| Method   | Path                                        |   Auth   | Description      |
| -------- | ------------------------------------------- | :------: | ---------------- |
| `GET`    | `/v0/agents/{agentId}/tasks`                | JWT / GW | List cron jobs   |
| `POST`   | `/v0/agents/{agentId}/tasks`                | JWT / GW | Create           |
| `GET`    | `/v0/agents/{agentId}/tasks/{jobId}`        | JWT / GW | Detail           |
| `PUT`    | `/v0/agents/{agentId}/tasks/{jobId}`        | JWT / GW | Update           |
| `DELETE` | `/v0/agents/{agentId}/tasks/{jobId}`        | JWT / GW | Delete           |
| `POST`   | `/v0/agents/{agentId}/tasks/{jobId}/run`    | JWT / GW | Run now          |
| `POST`   | `/v0/agents/{agentId}/tasks/{jobId}/toggle` | JWT / GW | Enable / disable |
| `GET`    | `/v0/agents/{agentId}/tasks/{jobId}/runs`   | JWT / GW | Run history      |

### Files & snapshots

| Method | Path                                     |   Auth   | Description                                  |
| ------ | ---------------------------------------- | :------: | -------------------------------------------- |
| `GET`  | `/v0/agents/{agentId}/files?path=<path>` | JWT / GW | Read a file from inside the container        |
| `POST` | `/v0/agents/{agentId}/files/upload`      | JWT / GW | Upload base64 file to `<workspace>/uploads/` |
| `GET`  | `/v0/agents/{agentId}/snapshots`         | JWT / GW | Last 10 snapshots                            |
| `GET`  | `/v0/agents/{agentId}/snapshots/sync`    | JWT / GW | Sync status                                  |
| `POST` | `/v0/agents/{agentId}/snapshots/sync`    | JWT / GW | Trigger a snapshot now                       |
| `POST` | `/v0/agents/{agentId}/snapshots/reset`   | JWT / GW | Reset to a snapshot CID                      |

### Console

| Method | Path                                |   Auth   | Description                                               |
| ------ | ----------------------------------- | :------: | --------------------------------------------------------- |
| `POST` | `/v0/agents/{agentId}/console/exec` | JWT / GW | Run a shell command, returns `{stdout, stderr, exitCode}` |

### Devices

| Method | Path                                               |   Auth   | Description           |
| ------ | -------------------------------------------------- | :------: | --------------------- |
| `GET`  | `/v0/agents/{agentId}/devices`                     | JWT / GW | List pending + paired |
| `POST` | `/v0/agents/{agentId}/devices/{requestId}/approve` | JWT / GW | Approve one           |
| `POST` | `/v0/agents/{agentId}/devices/approve-all`         | JWT / GW | Approve every pending |

### Config (engine runtime)

| Method | Path                                   |   Auth   | Description                                            |
| ------ | -------------------------------------- | :------: | ------------------------------------------------------ |
| `GET`  | `/v0/agents/{agentId}/config`          | JWT / GW | Read the engine runtime config shown in the Danger tab |
| `PUT`  | `/v0/agents/{agentId}/config`          | JWT / GW | Write runtime config (validated server-side)           |
| `POST` | `/v0/agents/{agentId}/config/validate` | JWT / GW | Validate runtime config without applying               |

<Note>
  Note `config/validate` validates runtime config, **not** `manifest.json`. OpenClaw agents use `/home/node/.openclaw/openclaw.json`; Hermes agents use `/home/hermes/data/config.yaml`. To validate a manifest, use `POST /v0/templates/validate` or run `pinata agents templates validate`.
</Note>

### Templates

| Method   | Path                            | Auth | Description                                 |
| -------- | ------------------------------- | :--: | ------------------------------------------- |
| `GET`    | `/v0/templates`                 |  JWT | List your templates                         |
| `POST`   | `/v0/templates`                 |  JWT | Submit a template from a git repo           |
| `GET`    | `/v0/templates/{slug}`          |  JWT | Get by slug                                 |
| `GET`    | `/v0/templates/id/{templateId}` |  JWT | Get by ID                                   |
| `PUT`    | `/v0/templates/{templateId}`    |  JWT | Update / resubmit                           |
| `DELETE` | `/v0/templates/{templateId}`    |  JWT | Archive                                     |
| `POST`   | `/v0/templates/validate`        |  JWT | Validate a git repo for template submission |
| `POST`   | `/v0/templates/branches`        |  JWT | List branches on a public repo              |
| `POST`   | `/v0/templates/refs`            |  JWT | List branches + tags                        |
| `GET`    | `/v0/public-templates`          | none | Public marketplace listing                  |

### Git Smart HTTP

| Method | Path                                        |            Auth            | Description              |
| ------ | ------------------------------------------- | :------------------------: | ------------------------ |
| `GET`  | `/v0/agents/{agentId}/git/info/refs`        | Git Basic (GW as password) | Ref advertisement        |
| `POST` | `/v0/agents/{agentId}/git/git-upload-pack`  | Git Basic (GW as password) | `git clone` / `git pull` |
| `POST` | `/v0/agents/{agentId}/git/git-receive-pack` | Git Basic (GW as password) | `git push`               |

## OpenAPI Spec

For everything not covered here:

```
https://agents.pinata.cloud/openapi
```

The OpenAPI spec is the authoritative source - schemas, query parameters, response codes, and per-endpoint descriptions.


# Channels
Source: https://docs.pinata.cloud/agents/channels

Let people talk to your agent on Telegram, Slack, or Discord

By default, the only way to talk to your agent is through the Pinata dashboard. Channels change that — connect Telegram, Slack, or Discord, and your agent shows up wherever you and your team already chat.

WhatsApp is on the roadmap; you'll see its card grayed out as **Coming Soon**.

<Frame>
  <img alt="Image" />
</Frame>

## How it works

Open your agent → **Channels**. You'll see one card per platform.

* A card with **+ ADD** isn't connected yet. Click it to open the setup dialog.
* A card with **ENABLED** is already connected. The card shows a summary (the DM policy and a masked bot token), and a **RECONFIGURE** button reopens the dialog so you can update settings without losing the connection.

Setup is the same shape for every platform: grab a bot token from the platform, paste it into the dialog, save. Some platforms need an extra token or an OAuth invite to a workspace/server.

Once a channel is enabled, anyone who messages your bot on that platform is talking to your agent. Responses come back through the same channel.

## Telegram

This is the quickest. You need a bot token from Telegram's BotFather.

1. In Telegram, message [@BotFather](https://t.me/botfather)
2. Send `/newbot` and follow the prompts
3. Copy the bot token BotFather gives you
4. In Pinata: agent → **Channels** → **+ ADD** on the Telegram card
5. Paste the token and save

### Controlling who can DM your bot

The Telegram dialog has two access controls:

* **DM policy** — `open` (anyone can message) or `pairing` (users must be approved first)
* **Allow list** — a list of Telegram user IDs that are allowed to message

You can use either or both. Leave both at their defaults if you want anyone to be able to chat.

## Slack

Slack needs a custom app with the right permissions. It's a few more steps but everything happens once.

1. Go to [api.slack.com/apps](https://api.slack.com/apps) and create a new app
2. Enable **Socket Mode**
3. Create an **App-Level Token** with the `connections:write` scope
4. Under **OAuth & Permissions**, add these bot token scopes: `chat:write`, `im:write`, `im:history`, `im:read`, `users:read`, `app_mentions:read`
5. Install the app to your workspace
6. In Pinata: agent → **Channels** → **+ ADD** on the Slack card
7. Paste both tokens — bot token starts with `xoxb-`, app token starts with `xapp-`

Once connected, the bot responds to DMs and to `@mentions` in any channel it's been invited to.

## Discord

Same shape as Telegram — make a bot in Discord's developer portal, copy the token, paste it in.

1. Go to [discord.com/developers](https://discord.com/developers/applications) and create an application
2. Under **Bot**, click **Add Bot** and copy the token (you may need to reset it once to reveal it)
3. Under **OAuth2 → URL Generator**, check the `bot` scope and the permissions your bot needs
4. Open the generated URL to invite the bot to your server
5. In Pinata: agent → **Channels** → **+ ADD** on the Discord card
6. Paste the bot token and save

## Updating or removing a channel

* **Reconfigure** — click **RECONFIGURE** on an enabled channel. You can leave the token fields blank to keep the existing token while changing other settings.
* **Remove** — same dialog, **Remove** action. This deletes the channel config and restarts the agent's gateway.

<Note>
  Channels are available for chat-oriented engines such as OpenClaw and Hermes. Channel changes take effect after the gateway restarts; Configure and Remove handle that automatically. The create wizard can collect channel setup before deployment. If you're creating an agent via the API or CLI, pass a `channels` object on `POST /v0/agents` for engines that support create-time channel bootstrap — see [API → Create an agent](/agents/api#create-an-agent).
</Note>

<Warning>
  Bot tokens are sensitive. Keep them out of source control. They're stored encrypted on Pinata's side; treat them the same way locally.
</Warning>

## When it doesn't work

| Symptom                             | What's going on                                    | What to do                                                                       |
| ----------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------------- |
| Bot doesn't respond                 | Gateway didn't restart after configure             | Open **Danger** → **Restart Gateway**                                            |
| Slack works in DMs but not channels | Missing `app_mentions:read` scope                  | Reinstall the Slack app with the scopes above                                    |
| Telegram users get no reply         | `dmPolicy` is `pairing` and they aren't paired yet | Switch to `open`, or approve them via [Devices](/agents/devtools#device-pairing) |
| `403` when adding a channel         | Plan limit or workspace permissions                | Check your plan and workspace role                                               |

If none of these match, check [Logs](/agents/logs) for entries from `gateway/reload` and the channel name, or walk through [Troubleshooting](/agents/troubleshooting).


# Chat
Source: https://docs.pinata.cloud/agents/chat

Talk to your agent through the web interface

The Chat tab is your main conversation with the agent. You see your prompts, the agent's responses, any tools it called along the way, and — if you enable it — its thinking.

Open your agent → **Chat**.

<Frame>
  <img alt="Image" />
</Frame>

## Sending a message

The composer is at the bottom of the page.

* **+** opens an attachment picker. Files you attach land in `<workspace>/uploads/` inside the container, and the agent gets the path in its context.
* The text input is the message itself. Press Enter to send, Shift+Enter for a newline.
* **MODEL** picks which model handles this turn. It defaults to whatever you set as the agent's default — see [Models](/agents/models).
* The send arrow submits.

## Slash commands

Type `/` at the start of a message to run a command instead of a regular prompt:

| Command   | Effect                                                            |
| --------- | ----------------------------------------------------------------- |
| `/new`    | Starts a fresh session — the agent forgets the prior conversation |
| `/status` | Prints the current model, session info, and key flags             |
| `/help`   | Shows the commands your agent supports                            |

The list depends on your agent engine and any skills you've attached, so `/help` is the best way to see what's actually available.

## Reading agent responses

Each turn the agent sends back has a few elements you'll see in the chat:

* **Thinking block** — collapsed by default, labeled `Thinking ▾`. Click it to expand. Only appears for models that expose reasoning.
* **Message text** — the actual reply. Renders markdown, including code blocks and tables.
* **Tool calls** — if the agent called a tool (a shell command, a web search, a skill), you'll see a labeled block with the arguments and result. This is your single best window into *why* the agent answered the way it did.
* **Model badge** — after the response, a small badge shows which model produced it. Useful when you've been switching models mid-conversation.

When you want to debug a bad answer, expand the thinking block and the tool calls before anything else. That's almost always where the answer is.

Response delivery is engine-specific. OpenClaw agents usually stream assistant text token-by-token; Hermes agents can deliver a complete assistant message in one event. The transcript renders the same either way, but the reply may appear to type in for OpenClaw and appear all at once for Hermes.

## Switching models mid-conversation

Use the **MODEL** dropdown in the composer. You can switch between any models you've enabled for this agent — start a turn with a cheap, fast model, then bump up for a hard question. Each turn is tagged with the model that produced it, so the conversation stays readable.

If you want to confirm what the agent is currently using without scrolling, send `/status`.

## Chatting while the agent is stopped or restarting

You don't have to wait for a stopped or restarting agent. The composer stays open ("Agent is starting — type a message and it'll send when ready…") and anything you send is queued:

* Each queued message shows a **QUEUED** clock chip, with an **×** to cancel it before it sends
* A status line tracks the backlog — "3 messages queued · sending when the agent finishes starting…"
* Queued messages **survive a page reload** — refresh or navigate away mid-restart and they're still waiting when you come back
* The moment the agent is back online, messages deliver automatically, **in order, one at a time** — no duplicates, and the agent answers each one individually

## More room for the conversation

Click **Hide navbar** in the top-right corner to collapse the left sidebar — the conversation gets the full width of the screen. Click it again to bring the sidebar back.

## Tips

* Long-running tool calls show a progress indicator. Wait for it before assuming the agent is stuck.
* If the chat looks disconnected and stays that way, the gateway probably needs a kick. Open **Danger** → **Restart Gateway**. Messages you send during the restart queue up and deliver when it's back.
* If you change a secret, attach a new skill, or add a channel, restart the gateway so the agent picks up the change. Attaching a new LLM provider restarts the agent with a fresh environment automatically.

For deeper debugging, the [Logs](/agents/logs) tab streams what the agent engine is doing under the hood. See [Troubleshooting](/agents/troubleshooting) for common failure modes.


# Concepts
Source: https://docs.pinata.cloud/agents/concepts

Terms, file layout, and how the pieces fit together

The agent platform has a handful of moving parts, and they don't always have obvious names. This page is your reference: every term, every standard file path, every reserved port. Skim it now to get the lay of the land; come back later when something in the docs reads like jargon.

## The shape of things

```text theme={null}
Workspace (your team)  ──►  Agent (container)  ──►  Channels / Routes / Tasks
   │                            │
   │                            ├─ Skills (capabilities)
   │                            ├─ Secrets (env vars)
   │                            ├─ Models (LLMs it can call)
   │                            └─ Workspace files + snapshots
   ▼
Pinata IPFS + R2 (storage)
```

A **workspace** is a Pinata team. Inside it you create one or more **agents** — each is an isolated container running an agent engine, with its own files, gateway, and per-agent subdomain.

## Glossary

### Agent

A single isolated container with a persistent workspace, gateway, and `{agentId}.agents.pinata.cloud` subdomain. Created via `POST /v0/agents` or the Create Agent button. Identified by `agentId` (a short slug like `x0i33jye`).

### Engine

The runtime that powers the agent. Engines are enabled per deployment:

| Engine                 | Tagline                                                     | When to pick it                                                                                                                                                                     |
| ---------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `openclaw` (default)   | Configurable agent for custom workflows and power users     | You want full control over manifest fields, scripts, routes, and skills                                                                                                             |
| `hermes`               | Opinionated agent with a smoother out-of-the-box experience | You want fewer knobs and first-run channel setup to feel more direct                                                                                                                |
| `super-builder` (beta) | Headless coding agent optimized for issue-board work        | You're assigning work through [Issues & Boards](/agents/issues) and want longer runs (200-turn default cap) over chat features. Chat-first surfaces like Channels aren't available. |

Pick the engine when creating the agent — either in the **Agent Engine** card on the Create Agent wizard, or by passing `engine` in `POST /v0/agents`, or by setting `engine` in [`manifest.json`](/agents/manifest) for a template. Available engines per deployment are listed at `GET /v0/agents/engines`.

### Gateway

The long-running process inside each agent container that:

* Terminates WebSocket connections from the dashboard, channels, and CLI
* Routes path/domain traffic to user-defined ports
* Brokers HTTP API calls for the per-agent subdomain
* Restarts on **Restart Gateway** in the [Danger](/agents/devtools) tab

Listens on the reserved port `18789`.

### Gateway token

Per-agent credential used to authenticate against the agent's own subdomain (`{agentId}.agents.pinata.cloud/...`). See [API → Gateway token](/agents/api#gateway-token). Distinct from the **Pinata JWT** (workspace-wide API key) and from the **platform JWT** (1h token an agent gets from itself via `POST /v0/platform/token`).

| Token         | Where it works                  | What it's for                                                                                     |
| ------------- | ------------------------------- | ------------------------------------------------------------------------------------------------- |
| Pinata JWT    | `agents.pinata.cloud`           | Workspace-wide management - create/list/delete agents, manage secrets, browse templates           |
| Gateway token | `{agentId}.agents.pinata.cloud` | Talk to one specific agent (chat, files, routes, console, git)                                    |
| Platform JWT  | `agents.pinata.cloud`           | A short-lived JWT the agent gets *for itself* - lets the agent self-modify via `@pinata/platform` |

### Workspace

Two meanings - context disambiguates:

1. **Team workspace** - your Pinata account or shared team. Switch under **Account → Workspaces**.
2. **Agent workspace** - the per-agent file tree inside the container. OpenClaw agents use `/home/node/clawd/workspace/`; Hermes agents use `/home/hermes/data/workspace/`. The workspace includes [`manifest.json`](/agents/manifest), `SOUL.md`, attached skills under `skills/`, file uploads under `uploads/`, and anything else your agent has written.

### Workspace anatomy

The default file layout inside `workspace/`:

| File             | Purpose                                                                                                    |
| ---------------- | ---------------------------------------------------------------------------------------------------------- |
| `manifest.json`  | Agent configuration - identity, secrets, skills, scripts, routes, channels, tasks. Single source of truth. |
| `SOUL.md`        | Agent personality and principles - customize freely                                                        |
| `AGENTS.md`      | Conventions the agent follows when working in the repo (memory system, safety rules, file layout)          |
| `IDENTITY.md`    | Name, vibe, emoji, owner identity - written at deploy time                                                 |
| `USER.md`        | Notes about the human user, learned over time                                                              |
| `TOOLS.md`       | Environment notes (what tools exist in the container)                                                      |
| `BOOTSTRAP.md`   | First-run conversation guide. Self-deletes after setup.                                                    |
| `HEARTBEAT.md`   | Periodic-task notes (empty by default)                                                                     |
| `skills/<name>/` | Files for each attached skill                                                                              |
| `uploads/`       | Files uploaded via the chat attachment button or `POST /v0/agents/{agentId}/files/upload`                  |

You can commit and push anything else (`projects/`, `scripts/`, etc.) - the agent treats the whole tree as fair game.

<a />

### `manifest.json` vs runtime config

These are different files. They live in different places and have different schemas.

| File                    | Path                                 | Schema                                 | Edited by                                                                       |
| ----------------------- | ------------------------------------ | -------------------------------------- | ------------------------------------------------------------------------------- |
| `manifest.json`         | `<workspace>/manifest.json`          | [`manifest.v1.json`](/agents/manifest) | You (committed to git, validated via `POST /v0/templates/validate`)             |
| OpenClaw runtime config | `/home/node/.openclaw/openclaw.json` | OpenClaw runtime schema                | OpenClaw at runtime (validated via `POST /v0/agents/{agentId}/config/validate`) |
| Hermes runtime config   | `/home/hermes/data/config.yaml`      | Hermes runtime schema                  | Hermes at runtime (edited from the Danger tab's Config row)                     |

Day-to-day, edit `manifest.json`. Runtime config is the engine's own view of that configuration and is usually only touched from the [Danger](/agents/devtools) tab when you're debugging the engine itself.

### Skill

A reusable package of files and instructions that extends an agent's capabilities. Pinned to IPFS, addressed by **slug** (`@pinata/api`) or **CID**. Installed into your Skills Library, then attached per-agent. Up to 10 attached per agent. See [Skills](/agents/skills).

The platform ships with `@pinata/platform` bundled - it lets the agent perform self-service operations (install skills, set secrets, manage tasks).

### Secret vs variable

Both are environment variables injected at container start. The difference:

| Kind         | Storage                               | Visible in API?      | Use for                     |
| ------------ | ------------------------------------- | -------------------- | --------------------------- |
| **Secret**   | AES-GCM encrypted with a per-user key | Never returned       | API keys, tokens, passwords |
| **Variable** | Plaintext                             | Returned in listings | Public URLs, feature flags  |

See [Secrets](/agents/secrets).

### Channel

A messaging surface the agent can be reached on - Telegram, Slack, Discord (WhatsApp coming). Configured per-agent. See [Channels](/agents/channels).

### Route

A mapping from an external URL prefix or domain to a container port. Lets you expose web apps and APIs running inside the agent. See [Routes & Domains](/agents/routes).

### Task

A scheduled prompt or system event. Three schedule kinds: `at` (one-shot), `every` (interval), `cron`. See [Tasks](/agents/tasks).

### Lifecycle script

Shell commands run by the agent runner at well-defined points:

| Script  | When                                                  | Working dir        | Log file              | Timeout         |
| ------- | ----------------------------------------------------- | ------------------ | --------------------- | --------------- |
| `build` | After deploy, after each `git push`, on Retry Scripts | `/home/node/clawd` | `/tmp/user-build.log` | 5 min           |
| `start` | After successful `build`, on agent boot               | `/home/node/clawd` | `/tmp/user-start.log` | None (detached) |

Defined in `scripts` in [`manifest.json`](/agents/manifest#scripts).

### Snapshot

A capture of the agent's workspace pinned to IPFS as a single CID. Created automatically every minute when changes are detected, plus on demand via `POST /v0/agents/{agentId}/snapshots/sync`. Reset to any historical snapshot via `POST /v0/agents/{agentId}/snapshots/reset`. See [Files & Snapshots](/agents/snapshots).

### Device

A client paired with an agent (CLI, mobile, browser session) - approval-gated for security. Listed at `GET /v0/agents/{agentId}/devices`. See [Danger → Devices](/agents/devtools#device-pairing).

### Template

A pre-configured agent (manifest + workspace files + skill list) packaged for one-click deployment. Published on the [Marketplace](https://agents.pinata.cloud/marketplace) or kept private in [My Templates](https://agents.pinata.cloud/templates). See [Templates](/agents/templates/overview).

### Issue

Kanban-style work item assigned to an agent - title + prompt + optional repo. The agent runs it in an isolated `<workspace>/runs/<runId>/` directory (its own git history; ignored by snapshots), you review the diff, then accept, revise, or open a PR. Boards carry per-board instructions, model overrides, and turn/cost caps. See [Issues & Boards](/agents/issues).

## Filesystem reference

Paths agents and developers reach for most often inside the container:

| Path                                    | What's there                                                                                    |
| --------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `/home/node/clawd/`                     | OpenClaw runner root - where `build`/`start` execute                                            |
| `/home/node/clawd/workspace/`           | OpenClaw workspace                                                                              |
| `/home/hermes/data/workspace/`          | Hermes workspace                                                                                |
| `<workspace>/skills/<name>/`            | Attached skill files                                                                            |
| `<workspace>/uploads/`                  | Uploaded files                                                                                  |
| `<workspace>/runs/<runId>/`             | Isolated working dir for each [issue](/agents/issues) run (gitignored, excluded from snapshots) |
| `/home/node/.openclaw/openclaw.json`    | OpenClaw runtime config                                                                         |
| `/home/hermes/data/config.yaml`         | Hermes runtime config                                                                           |
| `/tmp/openclaw/openclaw-YYYY-MM-DD.log` | Daily OpenClaw log file                                                                         |
| `/tmp/user-build.log`                   | `build` script output                                                                           |
| `/tmp/user-start.log`                   | `start` script output                                                                           |

## Reserved ports & names

| Reserved                          | Why                          |
| --------------------------------- | ---------------------------- |
| Port `18789`                      | Gateway listens here         |
| Subdomains containing `pinata`    | Reserved for first-party use |
| Specific reserved domain suffixes | Reserved for the platform    |

Path routes must use ports between `1025` and `65535`, excluding `18789`. See [Routes](/agents/routes).

## Identifier formats

Quick reference for the IDs you'll see in URLs and API responses:

| Identifier       | Format                                 | Example                                |
| ---------------- | -------------------------------------- | -------------------------------------- |
| Agent ID         | Random short slug                      | `x0i33jye`                             |
| Snapshot CID     | IPFS CIDv1, `bafy...` or `Qm...`       | `QmUMfo19uXMdBSXLiZAz7w...`            |
| Skill CID        | IPFS CIDv1, `bafy...`                  | `bafybeicglyjdb6w...`                  |
| Skill slug       | `@<owner>/<name>` (lowercase, hyphens) | `@pinata/api`                          |
| Custom domain ID | UUID                                   | `2fcd2a0b-aa70-432d-a310-678e01570e65` |
| Secret ID        | Random opaque string                   | (server-assigned)                      |

## Where to go next

<CardGroup>
  <Card title="Manifest reference" icon="file-code" href="/agents/manifest">
    Every field in `manifest.json`
  </Card>

  <Card title="HTTP API" icon="plug" href="/agents/api">
    Auth, endpoints, and examples
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/agents/troubleshooting">
    Debug a stuck agent
  </Card>

  <Card title="Error reference" icon="circle-exclamation" href="/agents/errors">
    Look up an API error
  </Card>
</CardGroup>


# Console
Source: https://docs.pinata.cloud/agents/console

A real terminal inside your agent's container

The Console tab is a shell. You get an interactive prompt inside your agent's container, with the same workspace your agent reads and writes.

<Frame>
  <img alt="Image" />
</Frame>

```text theme={null}
--- Agent Console ---
Working directory: /home/node/clawd/workspace

~/clawd/workspace $
```

That example is an OpenClaw agent. Hermes agents use `/home/hermes/data/workspace`. The current path is also shown on the agent's [Danger](/agents/devtools) tab.

Anything you can do with a shell, you can do here:

* Run `bash`, `python`, `node`, `git`, `curl`, `jq`
* Tail log files: `tail -f /tmp/user-build.log`
* Check what the agent sees: `env | sort`
* Confirm a service is listening: `ss -tlnp`
* Edit a file with `vi` or `nano`

The session starts in the engine's workspace directory — the same workspace shown on the Files and Danger tabs. Skills live under `skills/`, uploaded files under `uploads/`.

<Tip>
  The button in the top-right of the panel switches to fullscreen mode, which is much more pleasant for anything beyond a one-liner.
</Tip>

## When to reach for the Console

The Console is the second-best debugging tool, after Logs. Use it when:

* Logs show an error and you need to look at a file to understand it
* A lifecycle script failed and you want to read `/tmp/user-build.log` or `/tmp/user-start.log`
* You want to confirm a process is actually running (`ps aux | grep <name>`)
* You want to reproduce a one-off command the agent ran

Quick checks worth knowing:

```bash theme={null}
# What did the build script say?
tail -n 200 /tmp/user-build.log

# What did the start script say?
tail -n 200 /tmp/user-start.log

# Latest OpenClaw log (OpenClaw agents)
ls -la /tmp/openclaw/

# OpenClaw runtime config
cat /home/node/.openclaw/openclaw.json

# Hermes runtime config
cat /home/hermes/data/config.yaml

# Manifest the agent is using
cat manifest.json

# Is anything listening on common dev ports?
ss -tlnp
```

For workflows by symptom, see [Troubleshooting](/agents/troubleshooting).

## Running a command from outside the UI

If you're scripting against an agent, hit the API:

Set `cwd` to the workspace path for that engine. For OpenClaw that's `/home/node/clawd/workspace`; for Hermes that's `/home/hermes/data/workspace`.

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{"command":"ls -la","cwd":"/home/node/clawd/workspace"}' \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/console/exec
```

The response is JSON: `{ stdout, stderr, exitCode, command, timestamp }`.

<Warning>
  Console access is full shell access. Anyone with the gateway token has it. Treat the token like a server credential.
</Warning>


# Danger
Source: https://docs.pinata.cloud/agents/devtools

Full agent inventory, plus restart and delete

The Danger tab does two things. First, it's the single place where you can see *everything* about an agent — every secret attached, every skill installed, every channel configured, every snapshot CID. It's also where you'll find restart, runtime config edit, and delete.

The name is mostly about the buttons at the bottom — Restart Gateway is disruptive, Delete is permanent. But it's also the page you'll open most often when you're answering "how is this agent actually configured?"

## Layout

Two big sections.

* **General** — agent details, workspace state, lifecycle scripts, channels where supported, skills, devices, secrets. One row at a time.
* **Actions** — engine-specific controls such as Restart Gateway, OpenClaw Settings UI, and Delete This Agent.

## Agent details

The first block in **General** is the agent itself:

| Field             | What it is                                                                                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Agent ID**      | The unique slug used in URLs and API calls (e.g. `x0i33jye`)                                                                                                  |
| **Status**        | `starting`, `running`, or `not_running`                                                                                                                       |
| **Name**          | What you named it                                                                                                                                             |
| **Engine**        | The container engine — `openclaw` (default), `hermes`, or another enabled engine                                                                              |
| **Version**       | Engine version, when the engine exposes one. OpenClaw agents show **Change** for version updates.                                                             |
| **Config**        | Path to the engine runtime config. OpenClaw uses `/home/node/.openclaw/openclaw.json`; Hermes uses `/home/hermes/data/config.yaml`. **Edit** opens an editor. |
| **Gateway Token** | The credential used to authenticate against the agent's own subdomain. See [API → Gateway token](/agents/api#gateway-token).                                  |
| **Created**       | When the agent was created                                                                                                                                    |
| **Base URL**      | `https://{agentId}.agents.pinata.cloud`                                                                                                                       |

The **manifest.json schema** link at the top of this section jumps to the [full field reference](/agents/manifest).

While a new agent is still `starting`, Chat and most settings tabs are disabled. The Danger tab stays available so you can check the current status, engine, config path, workspace path, and delete the agent if provisioning went wrong.

## Workspace

| Field            | What it shows                                                                                                                        |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Path**         | The workspace directory inside the container. OpenClaw uses `/home/node/clawd/workspace`; Hermes uses `/home/hermes/data/workspace`. |
| **Snapshot CID** | IPFS CID of the most recent synced snapshot                                                                                          |
| **Last Sync**    | How recently the workspace was captured                                                                                              |

For diffs and restore, use the [Files](/agents/snapshots) tab.

## Lifecycle Scripts

Whether your `build` and `start` scripts are configured, and their current status. The states are self-explanatory: `Not configured`, `pending`, `running`, `success`, `failed`.

If something failed, use `POST /v0/agents/{agentId}/scripts/retry` to re-run the lifecycle. OpenClaw agents also expose the **OpenClaw Settings UI** action for low-level runtime debugging. Logs end up in `/tmp/user-build.log` and `/tmp/user-start.log` — see [Manifest → Scripts](/agents/manifest#scripts) for the details.

## Scheduled Tasks

Just a status summary. Manage them on the [Tasks](/agents/tasks) tab.

## Channels

A quick rollup for engines that support messaging channels. For each of Telegram, Slack, Discord, and WhatsApp, you'll see either `Enabled` or `Not configured`.

## Skills

Every attached skill, its installed version, and its IPFS CID. The same info as the [Skills](/agents/skills) tab, in a flat-list format.

## Devices

Clients paired with the agent — mobile, the CLI, browser sessions. Each is listed with its status (`paired`, `pending`) and last activity.

<a />

### Device pairing

Some clients (the CLI in particular) need to be approved before they can talk to the agent. The flow:

1. The client asks to pair. A pending entry appears in this list.
2. You approve it — either click **Approve** here or hit `POST /v0/agents/{agentId}/devices/{requestId}/approve`.
3. To approve everything pending at once: `POST /v0/agents/{agentId}/devices/approve-all`.

## Secrets

Every attached secret, with a **Synced** indicator that tells you whether the *running* gateway has picked up the latest value. If you updated a secret and the indicator says out of sync, restart the gateway.

## Actions

### Restart Gateway

Restarts the gateway process inside the container. Use it when:

* You changed a secret, skill, or channel and want the agent to actually pick it up
* The agent is misbehaving
* WebSocket connections look stuck

<Warning>
  Restarting disconnects all clients (chat, channel bots, custom domain traffic) for a minute or two.
</Warning>

From the API:

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/restart
```

### OpenClaw Settings UI

OpenClaw agents show an **Open Settings UI** action. It opens OpenClaw's internal settings panel, a low-level config editor for the engine itself — useful for advanced debugging.

<Warning>
  Misconfiguration here can break the agent. For day-to-day config, edit `manifest.json` instead. See the [manifest reference](/agents/manifest).
</Warning>

Hermes agents do not show this action. Use the **Config** row's **Edit** button for Hermes runtime config.

### Edit config

The **Edit** button next to the **Config** row opens the engine runtime config directly. For OpenClaw this is `openclaw.json`; for Hermes it is `config.yaml`. Changes are validated server-side before they're written.

You can also validate any config string without applying it:

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{"config":"{...}"}' \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/config/validate
```

This validates the runtime config, not `manifest.json`. For manifest validation, see [Manifest → Validation](/agents/manifest#validation).

### Update OpenClaw

OpenClaw agents show a **Change** button next to the Version field. It checks for updates and applies them. Equivalent CLI calls:

```bash theme={null}
# Check
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/update

# Apply
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{"tag":"latest"}' \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/update
```

### Delete Agent

Permanent. Removes:

* The container
* All workspace files and snapshots
* All R2 storage tied to the agent
* Channel configurations
* Custom domains

<Warning>
  There's no undo. The agent ID is gone — it won't be reissued.
</Warning>


# Error Reference
Source: https://docs.pinata.cloud/agents/errors

Look up an API error code and how to resolve it

Every API error comes back as JSON with a single `error` field and an HTTP status code:

```json theme={null}
{ "error": "Validation failed: skills exceeds maximum of 10" }
```

This page maps the codes you'll actually see in the wild to a cause and a fix. For step-by-step debugging, see [Troubleshooting](/agents/troubleshooting).

## By HTTP status

### `400 Bad Request`

The request shape or content is invalid.

| When you'll see it                       | Likely cause                                                                | Fix                                                          |
| ---------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `POST /v0/agents`                        | `name` missing, too long, or contains banned characters                     | Trim or rename. Max 100 chars.                               |
| `POST /v0/secrets`                       | `name` or `value` empty                                                     | Provide both                                                 |
| `POST /v0/agents/.../skills`             | More than 10 skills attached                                                | Detach skills first                                          |
| `POST /v0/agents/.../console/exec`       | Empty `command`                                                             | Provide the command string                                   |
| `POST /v0/agents/.../snapshots/sync`     | Storage not configured for this workspace                                   | Pinata storage isn't provisioned - contact support           |
| `POST /v0/agents/.../config/validate`    | `config` field isn't valid JSON                                             | Fix syntax                                                   |
| `POST /v0/agents/.../domains`            | Both `subdomain` and `customDomain` provided, banned name, or limit reached | Pick one. Avoid names containing "pinata". Max 5 domains.    |
| `POST /v0/agents/.../snapshots/reset`    | `snapshotCid` missing or malformed                                          | Use a CID from the snapshot list                             |
| `POST /v0/agents/.../channels/{channel}` | Token missing on initial setup, or unsupported channel name                 | Provide `botToken` (and `appToken` for Slack)                |
| `POST /v0/agents/.../files/upload`       | `filename` empty or `contentBase64` malformed                               | Sanitize filename, base64-encode without the data URL prefix |

### `401 Unauthorized`

No credential, or the credential was rejected before reaching authorization.

| Cause                            | Fix                                                                                 |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| Missing `Authorization` header   | Add `Authorization: Bearer <token>`                                                 |
| Pinata JWT expired or revoked    | Generate a new key in [Account → API Keys](/account-management/api-keys)            |
| Gateway token rotated            | Copy the current value from Danger → Agent → Gateway Token                          |
| Token from a different workspace | Switch workspaces in **Account → Workspaces** or use a key from the right workspace |

### `403 Forbidden`

Authenticated, but the action isn't allowed.

| Cause                                                                         | Fix                                           |
| ----------------------------------------------------------------------------- | --------------------------------------------- |
| Agent limit reached on `POST /v0/agents`                                      | Upgrade your plan or delete an unused agent   |
| Free plan trying to use a paid feature (channels, custom domains)             | Upgrade plan                                  |
| Workspace permission denied                                                   | Ask the workspace admin for access            |
| `404` returned where you expected `403` (Issues API on non-internal accounts) | Issues is closed beta to `@pinata.cloud` only |

### `404 Not Found`

The resource doesn't exist (or you're not allowed to know it exists).

| Cause                                          | Fix                                                             |
| ---------------------------------------------- | --------------------------------------------------------------- |
| Wrong `agentId`                                | Check the ID from the Danger page or `GET /v0/agents`           |
| `snapshotCid` not in this agent's history      | Use one from `GET /v0/agents/{agentId}/snapshots`               |
| Skill CID/slug not installed in your library   | Install via Skills Library or `POST /v0/clawhub/{slug}/install` |
| Template slug/ID doesn't exist or was archived | List `GET /v0/templates`                                        |
| Custom domain ID not on this agent             | List `GET /v0/agents/{agentId}/domains`                         |
| Issues endpoint hit by a non-internal account  | Closed beta - returns `404` by design                           |

### `409 Conflict`

Resource state is incompatible with the request.

| Cause                                                                                       | Fix                                                             |
| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Duplicate secret name on `POST /v0/secrets`                                                 | Use a different name or update the existing one                 |
| Two credentials for the same provider attached (e.g. `OPENAI_API_KEY` + Codex subscription) | Detach one                                                      |
| Custom domain already registered (verified)                                                 | Pick a different name or delete the existing registration       |
| Subdomain collides with another tenant                                                      | Generate a new one or pick a different name                     |
| Trying to delete a secret still attached to an agent                                        | Detach from agents first                                        |
| Skill version conflict during update                                                        | Resolve via `POST /v0/agents/{agentId}/skills/{skillId}/update` |

### `413 Payload Too Large`

File upload exceeded the per-request limit.

| Cause                                                 | Fix                                                                                |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `POST /v0/agents/{agentId}/files/upload` body too big | Chunk the upload, or push the file via git, or stream it from inside the container |

### `500 Internal Server Error`

Something blew up on the server. The `error` field usually has the underlying message.

Common variants:

| Message hint                                      | Cause                                           | Fix                                                       |
| ------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------- |
| `Gateway failed to start` after agent create      | Provisioning succeeded but the gateway crashed  | Wait 30s and retry, or check Logs                         |
| `Sync failed`                                     | Workspace snapshot upload to Pinata IPFS failed | Retry; if persistent, check Pinata status                 |
| `CLI error` on `GET /v0/agents/{agentId}/devices` | Underlying engine CLI returned non-zero         | Check Logs for engine errors; restart gateway             |
| `Reset failed`                                    | Git reset to the snapshot commit failed         | The commit hash may be missing - try a different snapshot |
| `Configuration failed` on channel configure       | The engine rejected the channel config          | Validate token format (e.g. Slack `xoxb-` / `xapp-`)      |
| `Execution failed` on console exec                | Command shell terminated unexpectedly           | Inspect the command; if hung, restart gateway             |

### `503 Service Unavailable`

A dependency the server needs is down.

| Cause                                                        | Fix                                                          |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| `POST /v0/secrets` returns 503 with "Secrets not configured" | Server-side encryption key not provisioned. Contact support. |
| Snapshot/IPFS backend unreachable                            | Retry; check Pinata status page                              |

## By symptom

### "I just created the agent and the chat shows offline"

The container takes \~30 seconds to provision. A `500` with the agent body in the response means provisioning succeeded but the gateway didn't start cleanly - the agent will appear in your list and you can restart it.

### "My push to the workspace returns auth errors"

Re-grab the URL with **Copy with Token** on the Files tab. The gateway token may have been rotated.

### "Tasks return 500 from `GET /v0/agents/.../tasks`"

This usually means the engine CLI inside the container failed. Restart the gateway; if it keeps failing, check the Logs filtered to `cron/runner`.

### "Channel configure returns 500"

The engine could not write the channel config or restart. Almost always a malformed token or a channel that's already enabled. Re-check the token, then **Reconfigure**.

### "Domain stuck `pending_ownership`"

Your `_pinata-verify.<domain>` TXT record isn't visible. Confirm with `dig TXT _pinata-verify.<domain> +short` and wait for propagation.

### "Issues endpoints all return 404"

Issues is closed beta and restricted to `@pinata.cloud` accounts today.

## Common error responses

Errors documented in the API spec, grouped by where they appear. Use this as a starting point when you're trying to interpret an error response — the exact string returned by the API is the source of truth.

| Response                        | Where it appears                                         | What it means                                                           |
| ------------------------------- | -------------------------------------------------------- | ----------------------------------------------------------------------- |
| `Missing userId`                | Several JWT-protected endpoints                          | Pinata JWT couldn't be resolved to a user — regenerate the key          |
| `Agent limit reached`           | `POST /v0/agents` (`403`)                                | Workspace at its agent quota — upgrade or delete an unused agent        |
| `Storage not configured`        | `POST /v0/agents/{agentId}/snapshots/sync` (`400`)       | Workspace has no backing storage; contact support                       |
| `Sync failed`                   | Snapshot sync (`500`)                                    | IPFS pin or workspace serialization failed — retry                      |
| `Snapshot not found`            | `POST /v0/agents/{agentId}/snapshots/reset` (`404`)      | Snapshot CID isn't in this agent's history                              |
| `Invalid snapshot CID`          | Snapshot reset (`400`)                                   | The provided CID didn't parse                                           |
| `Unsupported channel`           | `DELETE /v0/agents/{agentId}/channels/{channel}` (`400`) | Channel must be `telegram` / `slack` / `discord` / `whatsapp`           |
| `Configuration failed`          | Channel configure (`500`)                                | Engine rejected the token/config                                        |
| `Removal failed`                | Channel delete (`500`)                                   | Engine failed to remove and restart                                     |
| `Invalid command`               | Console exec (`400`)                                     | `command` field empty or invalid `cwd`                                  |
| `Execution failed`              | Console exec (`500`)                                     | Shell exited unexpectedly                                               |
| `Invalid JSON` / invalid config | Config validate / write (`400`)                          | The `config` field isn't valid for this engine                          |
| `Validation command failed`     | `POST /v0/agents/{agentId}/config/validate` (`500`)      | Engine validator returned non-zero                                      |
| `Failed to read config`         | `GET /v0/agents/{agentId}/config` (`500`)                | Couldn't read runtime config                                            |
| `Failed to write config`        | `PUT /v0/agents/{agentId}/config` (`500`)                | Couldn't write runtime config                                           |
| `Invalid version`               | `POST /v0/agents/{agentId}/restart` (`400`)              | Requested engine version doesn't exist                                  |
| `Restart failed`                | Restart (`500`)                                          | Gateway didn't come back up — check Logs                                |
| `Script launch failed`          | `POST /v0/agents/{agentId}/scripts/retry` (`500`)        | Build or start couldn't be launched                                     |
| `File too large`                | `POST /v0/agents/{agentId}/files/upload` (`413`)         | Body over the upload limit — chunk or push via git                      |
| `CLI error`                     | Several engine-backed endpoints (`500`)                  | Underlying engine CLI returned non-zero — check Logs                    |
| `Token generation failed`       | `POST /v0/agents/{agentId}/platform/token` (`500`)       | Platform JWT couldn't be minted — rotate gateway token                  |
| `Secrets not configured`        | `POST /v0/secrets` (`503`)                               | Server-side encryption key not provisioned — contact support            |
| `Duplicate secret name`         | `POST /v0/secrets` (`409`)                               | Secret name is already taken                                            |
| `Conflicting secrets`           | `POST /v0/agents/{agentId}/secrets` (`409`)              | Attaching two creds for the same provider (e.g. API key + subscription) |
| `Invalid parameters`            | Tasks create (`400`)                                     | One of `name`, `schedule`, or `payload` is missing or malformed         |
| `Failed to create cron job`     | Tasks create (`500`)                                     | Engine rejected the cron config                                         |
| `Failed to list cron jobs`      | Tasks list (`500`)                                       | Engine CLI failed                                                       |

## See also

* [Concepts](/agents/concepts) - terminology and how the pieces fit
* [Troubleshooting](/agents/troubleshooting) - step-by-step debugging by symptom
* [HTTP API](/agents/api) - auth and base URLs
* [Manifest reference](/agents/manifest) - manifest schema details


# Issues & Boards
Source: https://docs.pinata.cloud/agents/issues

Assign work to agents on a kanban board, review the diff, ship it

**My Issues** is a kanban board for handing work to your agents. You file an issue — a title plus a prompt — assign it to an agent, and the agent runs it in an isolated copy of its workspace. When it finishes, the card lands in Review with a full diff for you to accept, revise, or turn into a pull request.

Open **My Issues** in the sidebar. Each workspace can have multiple boards — the **+** next to the board name creates another one.

## The board

Four columns plus an archive:

| Column          | What's in it                                                       |
| --------------- | ------------------------------------------------------------------ |
| **Backlog**     | Drafted or unassigned issues. This is where new issues start.      |
| **In Progress** | Queued or running. Dragging a backlog card here kicks off the run. |
| **Review**      | Finished runs waiting on you. Check the diff and decide.           |
| **Done**        | Accepted work.                                                     |

The **Archive** strip at the bottom takes backlog or review cards you're finished with. Cards can also be reordered within a column — ordering is scoped per board.

Above the columns: search, sort, and filters for priority, agent, owner, and tags.

## File an issue

Click **New Issue**. The composer asks for:

* **Who does the work** — **An Agent** (hand the prompt to an agent and review the result) or **I'll do it myself** (no agent; the card just tracks work as you drag it across the board).
* **Title and prompt** — the prompt is what the agent actually receives. Markdown supported, attachments and checklists too.
* **Assign** — the agent, an optional **repository**, and tags. Owner, reviewer, priority, and due date live on the card after creation.

Issues start in Backlog. **Start issue** (or dragging the card to In Progress) queues it; the agent picks it up as soon as a slot frees up.

## How a run works

Each run executes in an isolated directory inside the agent's workspace — `<workspace>/runs/<runId>/` — with its own git history, so concurrent runs don't trample each other and the agent's main workspace stays clean. A `.gitignore` entry for `/runs` is added automatically so run directories stay out of workspace snapshots.

While the run is going, the card and the issue drawer both update live:

* The drawer hero shows elapsed time and a **live token count**
* The conversation pane streams the agent's tool calls and output token count as they happen
* **Files Changed** renders the diff in real time — you can watch the work land

When the agent finishes it marks the issue **Ready for Review** and the card moves to the Review column on its own.

## Reviewing

Open a Review card. The drawer splits into **Conversation** (the full exchange, including tool calls and the commit the agent made) and **Files Changed** (per-file diffs, unified or split view, expandable to full screen).

From here you can:

* **Accept & Done** — approve the work and mark the issue done. The run's changes stay as-is.
* **Send revision** — reply with feedback; the agent picks the issue back up and continues in the same run.
* **Create a PR** — open a pull request from the drawer with a custom title and description (requires a connected repository — see below).
* **Add comment** — discussion without triggering a run.

## Working against a GitHub repo

Connect GitHub under **Account → Integrations**, then save repositories from the issue composer's repository picker (**Manage repositories**). With a repo attached to an issue:

* **Self-driving issues** spin up a branch and a **draft PR** automatically when the run starts, and the agent continues its work on that branch.
* **Create PR from the drawer** turns a finished run into a pull request with the title and description you give it.
* The board tracks PR state — when a PR merges, the card reflects it. GitHub syncing is rate-limit-aware, so large orgs don't get throttled mid-sweep.

## Board settings

Board options (the **⋯** next to the board name) → **Board settings**:

| Setting            | What it does                                                                                                                            | Default       |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| **Instructions**   | Markdown prepended to *every* run on this board — coding conventions, commit-message rules, test requirements. Up to 20,000 characters. | empty         |
| **Model override** | Force a specific model for every run on the board. Blank = each agent's own default.                                                    | agent default |
| **Max turns**      | Per-run conversation-turn cap (1–1000). The run stops when it hits the limit.                                                           | 200           |
| **Max cost (USD)** | Per-run spend ceiling — the run stops when estimated spend exceeds it.                                                                  | \$25          |

Board instructions are genuinely injected into runs — if your instructions say "prefix commits with `[bot]`", the commits come back prefixed.

## Cost & token tracking

Spend is tracked at three levels:

* **Cards** show the run's cost next to the diffstat (e.g. `$0.13 +5 -0`).
* **The drawer hero** shows duration and total tokens; hovering it reveals the full **Agent Usage** breakdown — total spend, input/output tokens, and **cached tokens** accounted separately.
* **The board-level chip** (top right) opens a rollup panel with a **Cost | Tokens** toggle: totals by agent, by model, and a token table splitting input, output, cache reads, and cache writes.

<Note>
  Live cost during a run is an estimate and can settle to a lower final number once cache pricing is reconciled. Usage reporting depends on the engine — Super Builder runs report the richest data.
</Note>

## Which engine should issue agents use?

Any agent can take issues, but the **Super Builder** engine exists for exactly this: a headless coding agent optimized for issue-board work, with a higher default turn cap (200) for longer-running jobs. It skips chat-first surfaces like Channels in favor of doing the work and reporting back. See [Concepts → Engine](/agents/concepts#engine).

## Tips

* Use board instructions for anything you'd otherwise paste into every prompt — conventions compound.
* Set a reviewer on cards that need a second pair of eyes; filter the board by owner when triaging.
* Run history on a card is ordered by actual run time, so the latest attempt is always what you're reviewing.


# Logs
Source: https://docs.pinata.cloud/agents/logs

Stream your agent engine's output in real time

The Logs tab is a live stream of what the agent engine is doing inside your agent. New entries appear without refreshing — the tab uses a WebSocket under the hood. If the small status badge in the header reads **Connected**, you're streaming. If it reads **WebSocket not connected**, either the agent isn't running, the connection dropped, or there are no gateway log lines yet — hit the refresh button or check the agent's status.

<Frame>
  <img alt="Image" />
</Frame>

## Reading the stream

Each log line has four parts:

* **Timestamp** — when it happened
* **Level** — severity (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`)
* **Source** — which component generated the line (e.g. `gateway/reload`, `cron/runner`)
* **Message** — the content

Click the **+** on the left of any row to expand it. Useful for stack traces and structured fields that don't fit on a single line.

## Filtering

The six colored checkboxes — TRACE, DEBUG, INFO, WARN, ERROR, FATAL — toggle log levels on and off. When you're hunting a problem, uncheck everything except WARN and above to cut the noise.

The **Search logs** box does a substring match against the message. Combine with level filters: "show me ERROR lines mentioning `telegram`."

The **AUTO-FOLLOW** checkbox keeps the view pinned to the latest entry. Uncheck it when you want to scroll back without the view jumping.

## Exporting

**EXPORT VISIBLE** downloads whatever the current filters are showing — useful for sharing with support or filing an issue. It's only what you see; the export respects your filters.

## Where the logs live on disk

Inside an OpenClaw container, daily log files live at `/tmp/openclaw/openclaw-{date}.log`. From the [Console](/agents/console):

```bash theme={null}
ls /tmp/openclaw/
tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log
```

For other engines, use the Logs tab or the logs API as the stable interface; on-disk log paths are engine-specific. The API keeps the latest 100 lines when the gateway restarts. If you need long-term retention, export periodically.

## Reading logs over the API

For scripts and incident response:

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/logs
```

Returns the latest 100 lines as a single string. For full streaming, the UI uses a WebSocket on the agent's subdomain — see [API → Streaming logs](/agents/api#streaming-logs).

## Common sources, and what they tell you

| Source                         | What it means                                                                           |
| ------------------------------ | --------------------------------------------------------------------------------------- |
| `gateway/reload`               | Gateway reloaded its configuration — usually after a secret, skill, or channel change   |
| `gateway/ws`                   | WebSocket lifecycle (connect, disconnect, reconnect)                                    |
| `cron/runner`                  | A scheduled task fired — which one, and what happened                                   |
| `agent/turn`                   | One conversation turn — request in, response out                                        |
| `skill/<name>`                 | Output from a specific skill                                                            |
| `script/build`, `script/start` | Lifecycle script output (also tee'd to `/tmp/user-build.log` and `/tmp/user-start.log`) |

If something's broken, start at ERROR. If you see ERROR lines and can't make sense of them, [Troubleshooting](/agents/troubleshooting) maps symptoms to fixes, and the [Error reference](/agents/errors) covers common API error responses.


# Manifest Reference
Source: https://docs.pinata.cloud/agents/manifest

Every field in manifest.json, the source of truth for an agent's configuration

`manifest.json` lives at the root of every agent's workspace. It controls identity, lifecycle scripts, secrets, skills, routes, channels, scheduled tasks, and (for templates) marketplace metadata. This page documents every field at the version 1 schema.

The schema is published at:

```
https://agents.pinata.cloud/schemas/manifest.v1.json
```

Add a `$schema` line so editors can autocomplete and validate:

```json theme={null}
{
  "$schema": "https://agents.pinata.cloud/schemas/manifest.v1.json",
  "version": 1,
  "engine": "openclaw",
  "agent": { "name": "My Agent" }
}
```

## Top-level fields

| Field      | Type    | Required | Description                                         |
| ---------- | ------- | :------: | --------------------------------------------------- |
| `$schema`  | string  | optional | Schema URL for editor support                       |
| `version`  | integer |    yes   | Manifest version - currently `1`                    |
| `engine`   | string  | optional | Container engine - `openclaw` (default) or `hermes` |
| `agent`    | object  |    yes   | Agent identity (see below)                          |
| `model`    | string  | optional | Default model in `<provider>/<model>` form          |
| `secrets`  | array   | optional | Encrypted credentials this agent requires           |
| `skills`   | array   | optional | Up to 10 skill slugs or CIDs                        |
| `tasks`    | array   | optional | Up to 20 scheduled tasks                            |
| `scripts`  | object  | optional | Lifecycle hooks (`build`, `start`)                  |
| `routes`   | array   | optional | Up to 10 port-forwarding rules                      |
| `channels` | object  | optional | Messaging channel configuration                     |
| `template` | object  | optional | Marketplace listing metadata (only for templates)   |

## `agent`

Defines the agent's identity. All fields are written into the workspace (mostly `IDENTITY.md`) at deploy time.

```json theme={null}
"agent": {
  "name": "Social Bot",
  "description": "You manage Socials",
  "vibe": "Resourceful and opinionated",
  "emoji": "🤖"
}
```

| Field         | Type   | Limit                     | Notes                        |
| ------------- | ------ | ------------------------- | ---------------------------- |
| `name`        | string | 1-100 chars, **required** | Display name                 |
| `description` | string | ≤ 10000 chars             | Personality / role           |
| `vibe`        | string | ≤ 200 chars               | One-line tagline             |
| `emoji`       | string | ≤ 32 chars                | Visible in lists and avatars |

## `model`

A single default model identifier in `<provider>/<model>` form:

```json theme={null}
"model": "anthropic/claude-sonnet-4-6"
```

Provider must match a connected provider in the [Secrets Vault](/agents/secrets). If a task or session specifies a model that isn't enabled on the agent, the request falls back to this default.

## `secrets`

Declares the secrets this agent needs. These are env-var names - the actual values come from the [Secrets Vault](/agents/secrets) at attach time.

```json theme={null}
"secrets": [
  { "name": "OPENROUTER_API_KEY" },
  { "name": "TELEGRAM_BOT_TOKEN", "description": "BotFather token", "required": false }
]
```

| Field         | Type    | Notes                                                                 |
| ------------- | ------- | --------------------------------------------------------------------- |
| `name`        | string  | Environment variable name (e.g. `ANTHROPIC_API_KEY`)                  |
| `description` | string  | Shown on the deploy form                                              |
| `required`    | boolean | Default `true`. `false` makes the field optional in the deploy wizard |

## `skills`

Up to 10 skills. Each entry is an object with a `name` plus either a `clawhub_slug` (resolves to the latest published version) or a `cid` (byte-stable, pinned to a specific upload).

```json theme={null}
"skills": [
  { "clawhub_slug": "@pinata/api", "name": "Pinata API" },
  { "clawhub_slug": "@pinata/memory-salience", "name": "Memory Salience" },
  { "cid": "bafybeicglyjdb6wrrcbfyu6i2fe4lpxdxgvfvlht7yzdim7cvwt656whue", "name": "My Custom Skill" }
]
```

| Field          | Type   | Notes                                                                  |
| -------------- | ------ | ---------------------------------------------------------------------- |
| `name`         | string | Display name shown in the UI. Required.                                |
| `clawhub_slug` | string | ClawHub slug like `@pinata/api`. Mutually exclusive with `cid`.        |
| `cid`          | string | IPFS CID of an uploaded skill. Mutually exclusive with `clawhub_slug`. |

Each attached skill is unpacked under `workspace/skills/<skill-name>/`. See [Skills](/agents/skills).

## `tasks`

Scheduled prompts. Up to 20 per agent. Full reference in [Tasks](/agents/tasks).

```json theme={null}
"tasks": [
  {
    "name": "daily-check-in",
    "schedule": { "kind": "cron", "expr": "0 9 * * *", "tz": "America/Los_Angeles" },
    "payload": { "kind": "agentTurn", "text": "Summarize yesterday." },
    "delivery": { "mode": "announce", "channel": "telegram", "to": "123" }
  }
]
```

Schedule kinds: `at` (one-shot ISO timestamp), `every` (`everyMs` interval, optional `staggerMs`), `cron` (`expr` with optional `tz`).

Payload kinds: `agentTurn` (chat message; supports `model`, `thinking`, `timeoutSeconds`) or `systemEvent` (system trigger; provide `message`).

Delivery modes: `none` (default), `announce` (to a channel - set `channel` and `to`), or `webhook` (set `to` to a URL). `bestEffort: true` swallows delivery failures.

`sessionTarget` is `main` or `isolated`. `wakeMode` is `now` or `next-heartbeat`.

## `scripts`

Lifecycle hooks. Both fields are shell commands run by the agent's runner.

```json theme={null}
"scripts": {
  "build": "cd workspace/projects/app && npm install --include=dev",
  "start": "cd workspace/projects/app && npx vite --host 0.0.0.0"
}
```

| Hook    | When it runs                                              | Working directory  | Output log            | Timeout              |
| ------- | --------------------------------------------------------- | ------------------ | --------------------- | -------------------- |
| `build` | After deploy, after each `git push`, on **Retry Scripts** | `/home/node/clawd` | `/tmp/user-build.log` | 5 min                |
| `start` | After a successful `build`, on agent boot                 | `/home/node/clawd` | `/tmp/user-start.log` | None - runs detached |

If `build` fails, `start` does not run. If `start` crashes, the agent is still healthy - only the user-defined process exits.

**Retry the lifecycle:** `POST /v0/agents/{agentId}/scripts/retry` (or the action on the Danger tab).

**Bind servers to `0.0.0.0`**, not `localhost`, so the gateway can reach them. See [Routes](/agents/routes).

## `routes`

Port-forwarding rules. Up to 10. Each entry maps an external path prefix to a container port.

```json theme={null}
"routes": [
  { "port": 5173, "path": "/app", "protected": false },
  { "port": 3000, "path": "/api", "protected": true }
]
```

| Field       | Type    | Notes                                                                    |
| ----------- | ------- | ------------------------------------------------------------------------ |
| `port`      | integer | Container port. 1025-65535. `18789` is reserved.                         |
| `path`      | string  | URL path prefix. Stripped from the request before reaching your service. |
| `protected` | boolean | `true` requires a gateway token. Defaults to `false`.                    |

For custom domains, register them through the UI or the [Domains API](/agents/api#custom-domains) - they aren't declared in `manifest.json`.

## `channels`

Configures the messaging channels for this agent. Tokens are usually injected from secrets to avoid leaking into the manifest.

```json theme={null}
"channels": {
  "telegram": {
    "botToken": "env:TELEGRAM_BOT_TOKEN",
    "dmPolicy": "pairing",
    "allowFrom": ["123456789"]
  },
  "slack": {
    "botToken": "env:SLACK_BOT_TOKEN",
    "appToken": "env:SLACK_APP_TOKEN"
  },
  "discord": {
    "botToken": "env:DISCORD_BOT_TOKEN"
  }
}
```

| Channel    | Required fields         | Notes                                                       |
| ---------- | ----------------------- | ----------------------------------------------------------- |
| `telegram` | `botToken`              | `dmPolicy` (`open` / `pairing`), `allowFrom` (user IDs)     |
| `slack`    | `botToken`, `appToken`  | Socket Mode + scopes per [Channels](/agents/channels#slack) |
| `discord`  | `botToken`              |                                                             |
| `whatsapp` | `dmPolicy`, `allowFrom` | Linking happens on the agent. Coming soon.                  |

Use `env:VAR_NAME` to read from an environment variable instead of embedding a literal token.

## `template`

Only used when the manifest is the source for a marketplace template. Ignored for direct deploys.

```json theme={null}
"template": {
  "slug": "useful-assistant",
  "category": "general",
  "partnerName": "Pinata",
  "tags": ["assistant", "personal", "productivity"]
}
```

| Field         | Notes                                                                            |
| ------------- | -------------------------------------------------------------------------------- |
| `slug`        | Marketplace URL slug. Lowercase, hyphens.                                        |
| `category`    | One of the marketplace categories (e.g. `general`, `defi`, `social`)             |
| `partnerName` | Display name shown on the template card                                          |
| `tags`        | Array of free-text tags for filtering                                            |
| `paid`        | Optional. Set `{ "amount": "1.00", "currency": "USDC" }` for x402 paid templates |

## Validation

<Note>
  `manifest.json` is different from engine runtime config - see [Concepts → manifest vs runtime config](/agents/concepts#manifest-json-vs-openclaw-json). The `/v0/agents/{agentId}/config/validate` endpoint validates runtime config, not `manifest.json`.
</Note>

To validate `manifest.json` ahead of committing, run it through the template validator (which checks `manifest.json` + `README.md` + `workspace/`):

```bash theme={null}
# CLI
pinata agents templates validate https://github.com/user/my-template
```

```bash theme={null}
# Or hit the API directly with the repo URL + ref
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{"gitUrl":"https://github.com/user/my-template","ref":"refs/heads/main"}' \
  https://agents.pinata.cloud/v0/templates/validate
```

The response is `{ valid: boolean, errors: string[], manifest, readme, files }` — any problems with the manifest show up in the `errors` array.

Schema constraints to keep in mind while authoring:

| Field                 | Constraint                                                 |
| --------------------- | ---------------------------------------------------------- |
| `version`             | Must be `1`                                                |
| `agent`               | Required; `agent.name` is required and ≤ 100 chars         |
| `agent.description`   | ≤ 10000 chars                                              |
| `agent.vibe`          | ≤ 200 chars                                                |
| `agent.emoji`         | ≤ 32 chars                                                 |
| `skills`              | ≤ 10 entries                                               |
| `routes`              | ≤ 10 entries; ports `1025`–`65535`, excluding `18789`      |
| `tasks`               | ≤ 20 entries; minimum interval 1 minute                    |
| `channels.*.botToken` | Required on initial setup (Slack also requires `appToken`) |

See [Errors](/agents/errors) for the full HTTP status code reference.

## Minimal Example

The smallest valid manifest:

```json theme={null}
{
  "version": 1,
  "agent": { "name": "Hello" }
}
```

This boots the default `openclaw` engine with no skills, no scripts, and no routes - just a workspace and a chat interface.


# Models
Source: https://docs.pinata.cloud/agents/models

Pick which AI models this agent can use

Each agent has its own list of allowed models, grouped by provider. The default model is what the agent reaches for when nothing else says otherwise; you can also swap models in the middle of a conversation if you want a faster (or smarter) one for a specific turn.

<Frame>
  <img alt="Image" />
</Frame>

## What you see

Open your agent → **Models**. The page lists each connected provider as a separate section. Only providers you've connected in the [Secrets Vault](/agents/secrets) show up here — if you don't see OpenAI, for example, it's because you haven't connected an OpenAI key.

### Free-tier agents

If the agent was created on the **Free tier** (no provider key), the Models page shows a single "Running on Pinata's free tier" state instead of provider sections. The agent uses a free, rate-limited model from OpenRouter's free pool (`openrouter/free`); there's nothing to configure. Add your own provider key in Secrets — the **Add a provider key →** link takes you there — and the full provider/model UI appears for this agent.

Inside each provider section:

* One row per model that's currently enabled
* A **DEFAULT** badge marks the agent's default model
* An **×** on a row removes that model from this agent
* **+ ADD** at the top right of each section lets you enable more models from that provider

## Enable a model

Click **+ ADD** in the provider section and pick a model from the list. It's now available to this agent — you can call it from chat or set it as default. Removing one with **×** doesn't disconnect the provider, it just narrows the menu.

## Set the default

The default is what gets used when you don't specify a model. Click a model row to set it as default; the **DEFAULT** badge moves.

## Switch models mid-conversation

On the [Chat](/agents/chat) tab, the **MODEL** dropdown next to the message input lets you pick a model for the next turn. You'd typically reach for this to:

* Use a fast/cheap model for routine questions
* Bump up to a more capable model for a hard one
* Compare answers from two models on the same prompt

Each response is tagged with the model that produced it, so the conversation stays readable when you switch.

## What models look like in configs

When you reference a model in code, in `manifest.json`, or in a task payload, the convention is `provider/model`:

```text theme={null}
anthropic/claude-sonnet-4-6
openrouter/tencent/hy3-preview
openai/gpt-5.4
```

The provider prefix tells the agent engine which connected provider to use. If you give it a model that isn't enabled on this agent, the request falls back to the default.

Provider availability and default enabled models can vary by engine. For example, fresh OpenClaw and Hermes agents can start with different Anthropic defaults.

## What's available today

The exact list updates as providers ship. A current snapshot:

* **Anthropic** — `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-haiku-4-5`
* **OpenAI Codex** (subscription) — `gpt-5.4`
* **OpenRouter** — `auto` (lets OpenRouter pick), plus any specific model on the platform (e.g. `tencent/hy3-preview`)
* **Free tier** — `openrouter/free` (shared pool, rate-limited, no key needed)
* **Venice / Pinata / Custom** — whatever the provider exposes

Provider availability also varies by engine — for example, Super Builder agents offer a narrower provider list at creation than OpenClaw agents.

<Tip>
  When you select `auto` on OpenRouter, OpenRouter picks the underlying model for each request. Handy if you don't want to micromanage which model handles what.
</Tip>


# Overview
Source: https://docs.pinata.cloud/agents/overview

Hosted AI agents that can run code, manage files, and connect to other services

Pinata Agents gives you a hosted AI agent in its own sandboxed container. The agent has a workspace it can read and write to, a terminal it can run commands in, and connectors for the outside world — chat apps, web servers, scheduled jobs. You talk to it through the dashboard, the CLI, or whichever messaging platform you connect it to.

Each agent runs on an **Agent Engine** — `openclaw` (default) for full configurability, `hermes` for an opinionated smoother out-of-the-box experience, or `super-builder` (beta), a headless coding agent built for [issue-board work](/agents/issues). You pick one when you create the agent. The default OpenClaw engine is documented at [openclaw.org](https://openclaw.org); for the differences see [Concepts → Engine](/agents/concepts#engine).

<Note>
  New accounts get a **10-hour free trial** of agents — no payment and no LLM key required, thanks to the free-tier model available throughout your account. After the trial, agents require a paid plan. [Upgrade here](https://app.pinata.cloud/billing).
</Note>

## Get your first agent running

It takes a couple of minutes. You'll pick an LLM provider, create the agent, and start chatting.

### 1. Pick an LLM provider

Your agent needs an LLM to think. You have three options:

* **Free tier — no key, zero config.** Select the **Free tier** card in the Create Agent wizard and skip provider setup entirely. Your agent runs on a shared, rate-limited model (OpenRouter's free pool, e.g. `openrouter/free`) that's available throughout your account. Expect lower quality, smaller context, and occasional rate limits compared with a paid provider — but it's a real agent, free, today.
* **Pinata-hosted models — no key, pay as you go.** Connect the **Pinata** card in the [Secrets Vault](https://agents.pinata.cloud/secrets). It's first-party, so there's no API key to paste — connecting just turns on [Pinata Inference](/inference/overview) for your account. Your agent gets full-quality hosted models, billed per token from your [credit balance](/inference/credits). The same models are also callable directly over an OpenAI-compatible [API](/inference/chat-completions).
* **Bring your own key.** Go to the [Secrets Vault](https://agents.pinata.cloud/secrets). At the top of that page you'll see a row of provider cards: **Anthropic**, **OpenAI**, **OpenRouter**, **Venice**, and **Custom**. Click **Connect** on whichever one you use, paste your API key (or run through OAuth if you're using an OpenAI Codex subscription), and save.

<Tip>
  Start free, upgrade whenever — adding a provider key in Secrets later unlocks full model capabilities for the same agent, no re-create needed.
</Tip>

You can connect more than one provider — useful so you have a fallback if one is down.

### 2. Create an agent

Head to [My Agents](https://agents.pinata.cloud/agents) and click **Create Agent**.

You have two paths:

* **Start from a template** — fastest. Templates come pre-wired with skills, settings, and a personality. Pick one from the [Marketplace](https://agents.pinata.cloud/marketplace) (others' templates) or [My Templates](https://agents.pinata.cloud/templates) (your own). Add the secrets it needs, deploy.
* **Build from scratch** — full control. The wizard walks you through naming the agent, choosing an **Agent Engine** (OpenClaw, Hermes, or Super Builder), picking an AI provider (including the free tier), a personality preset, skills, and optionally channels before deploy.

Either way, the agent takes about 30 seconds to provision. When it's ready you land on its Chat tab.

<Note>
  Plans cap how many agents you can run at once. If a deploy fails with "Agent limit reached", delete an agent you're not using or upgrade your plan.
</Note>

### 3. Start a conversation

Say hello. Ask it to write some code, summarize a webpage, or set up a workflow. It can use a terminal, edit files, search the web, and call any skills you've attached.

<Frame>
  <img alt="Image" />
</Frame>

## Finding your way around

The sidebar on the left is the same on every page. Top half is the workspace itself; bottom half is account and support.

| Section            | What's in it                                                                                                                                                                                                                                                               |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **My Agents**      | Every agent you've created. Click one to expand its tabs (Chat, Channels, Files, Skills, etc.). The **ⓘ** on each agent card opens a floating info peek — live status, the default-model picker, skill/secret/task counts, and live CPU, memory, disk, and network meters. |
| **My Issues**      | Kanban boards for assigning work to agents — file an issue, the agent runs it, you review the diff. See [Issues & Boards](/agents/issues).                                                                                                                                 |
| **Skills Library** | Skills you've installed, plus a community catalog under **Browse ClawHub**.                                                                                                                                                                                                |
| **Secrets Vault**  | All your encrypted credentials, in one place, shared across agents.                                                                                                                                                                                                        |
| **My Templates**   | Templates you've created or imported.                                                                                                                                                                                                                                      |
| **Marketplace**    | Published templates from Pinata and other builders, ready to deploy in one click.                                                                                                                                                                                          |
| **Account**        | **Workspaces** (switch teams), **Integrations** (e.g. GitHub OAuth), **Activity** (audit log).                                                                                                                                                                             |
| **Support**        | Docs, the OpenAPI reference, changelog, and a way to chat with the Pinata team.                                                                                                                                                                                            |

<Tip>
  **⌘K** (or **Ctrl+K**) opens a command palette for jumping to any agent, page, or action.
</Tip>

## The agent dashboard

Click any agent and you'll see tabs across the top of the page. Each tab is a slice of how the agent works:

* **Chat** — your conversation with the agent
* **Channels** — wire it up to Telegram, Slack, or Discord
* **Files** — a file browser for the agent's container, workspace snapshot history, content diffs, git URL
* **Skills** — capabilities attached to this specific agent (pulled from your Skills Library)
* **Secrets** — provider connections and env vars this agent can see
* **Models** — which models from your providers it's allowed to use, and which is the default
* **Routes** — path routes and custom domains for any web service it runs
* **Tasks** — cron jobs and one-off scheduled prompts
* **Console** — a terminal inside the container
* **Logs** — real-time log stream
* **Danger** — full configuration overview, plus restart and delete

That last one is named "Danger" deliberately — restart and delete live there, but so does most of the inventory data, so it's also the page you'll open when you need to know exactly how an agent is configured.

## Editing the workspace locally

Your agent's workspace is a git repo you can clone and push to.

```bash theme={null}
git clone https://agents.pinata.cloud/v0/agents/{agentId}/git my-agent
```

On the Files tab, **Copy with Token** gives you a URL with authentication built in so you don't have to manage credentials. Edit locally, push, and the agent picks up your changes — the build script (if you've defined one) runs automatically.

See [Files & Snapshots](/agents/snapshots) for the full flow.

## Working from the terminal

Everything in the dashboard is in the CLI too:

```bash theme={null}
pinata agents list              # See your agents
pinata agents create            # Create a new agent
pinata agents get <agent-id>    # Get agent details
pinata agents chat <agent-id>   # Talk to it from your terminal
```

Full reference: [Agents CLI](/tools/cli/agents).

## Going deeper

<CardGroup>
  <Card title="Concepts" icon="compass" href="/agents/concepts">
    Glossary, file layout, and how the pieces relate
  </Card>

  <Card title="Manifest reference" icon="file-code" href="/agents/manifest">
    Every field in `manifest.json`, the source of truth for an agent's config
  </Card>

  <Card title="HTTP API" icon="plug" href="/agents/api">
    Auth model and endpoint reference for building against the platform
  </Card>

  <Card title="Troubleshooting" icon="wrench" href="/agents/troubleshooting">
    What to do when something doesn't work
  </Card>
</CardGroup>

<CardGroup>
  <Card title="Issues & Boards" icon="table-columns" href="/agents/issues">
    Assign work on a kanban board and review the diff
  </Card>

  <Card title="Templates" icon="grid-2" href="/agents/templates/overview">
    Deploy a pre-built agent in one click
  </Card>

  <Card title="Channels" icon="comments" href="/agents/channels">
    Let people talk to your agent on Telegram, Slack, or Discord
  </Card>

  <Card title="Skills" icon="wand-magic-sparkles" href="/agents/skills">
    Add capabilities — IPFS storage, memory, on-chain identity, more
  </Card>

  <Card title="Routes" icon="network-wired" href="/agents/routes">
    Expose a web app or API from inside the agent
  </Card>
</CardGroup>


# Domains & Routes
Source: https://docs.pinata.cloud/agents/routes

Expose web services from inside your agent to the internet

By default, anything your agent runs on a port stays inside the container — nobody on the outside can reach it. Routes punch a hole through so the outside can.

You'd want this if your agent is hosting a web app, an API, or a dev server you want to view in a browser. If your agent just chats and runs scripts, you don't need any of this.

<Frame>
  <img alt="Image" />
</Frame>

## Two kinds of route

Open your agent → **Routes**. There are two sections:

* **Path routes** — your service is reachable on the agent's own subdomain, under a path prefix you choose. No DNS work; works immediately.
* **Custom domains** — your service is reachable on a Pinata-provided subdomain (`name-name-NN.apps.pinata.cloud`) or on your own domain.

Click **+ ADD** to add either.

## Path routes

This is the simplest option. Pick a port and a path; the route becomes available at:

```text theme={null}
https://{agentId}.agents.pinata.cloud/<path>/...
```

Click **+ ADD** → **Path Route**, then fill in:

1. **Container port** — the port your service is listening on inside the agent (e.g. `5173`)
2. **Path prefix** — the URL path (e.g. `/app`, or `/` for everything)
3. **Public** or **Protected** access — see below

One thing to know: the path prefix is **stripped** before the request hits your service. If someone visits `/app/users/123`, your service sees `/users/123`. Most frameworks let you configure a base path to match — for Vite that's `base: "/app"`.

## Custom domains

<Note>
  Custom domains are currently in beta.
</Note>

There are two flavors.

### Pinata-provided subdomain

Easiest. You get a random `name-name-NN.apps.pinata.cloud` subdomain that points at your agent.

1. **+ ADD** → **Subdomain** tab
2. Optionally rename the auto-generated subdomain (you can also accept the default)
3. Set the container port and Public/Protected
4. **Register** — it's live immediately

### Bring your own domain

1. **+ ADD** → **Custom Domain** tab
2. Type your domain (e.g. `app.example.com`)
3. Pinata gives you a TXT record value — add it as `_pinata-verify.app.example.com` at your DNS provider
4. Submit the registration
5. Pinata gives you a CNAME target — add a CNAME from `app.example.com` to that target
6. Click **Verify**

After Verify, Pinata checks your TXT record, then provisions an SSL certificate through Cloudflare. The domain shows up in the list with a status; once it reads **active**, traffic is flowing.

DNS propagation and SSL provisioning typically take a few minutes. If your domain stays in a pending state, see [Troubleshooting → Custom domain stuck pending](/agents/troubleshooting#custom-domain-stuck-pending).

### Editing a custom domain

Once registered, you can change the **target port** and the **Protected** flag. You can't rename the domain — to switch, delete and recreate.

## Public vs Protected

Every route, path or domain, is one or the other.

* **Public** — anyone on the internet can reach it. Use this if your service handles its own auth, or you genuinely want it open.
* **Protected** — requests must include the agent's gateway token. The token can be in the `Authorization: Bearer` header, a `?token=...` query string, or a `gw_token` cookie. Use this when you want the route locked down with no extra work.

<Warning>
  Public means *public*. Think before flipping that switch.
</Warning>

## Building a web app that works behind a route

Two things have to be true or it won't work, and the fix is almost always one of them:

1. **Your server has to bind to `0.0.0.0`**, not `localhost`. The gateway can't reach `localhost` from outside the process.
2. **Your dev server has to allow the agent's host**. Vite, Next, etc. check `Host` and reject unknowns by default.

Use the placeholder `__AGENT_HOST__` in config files — it's replaced at runtime with the agent's public hostname. Example for Vite:

```ts theme={null}
export default defineConfig({
  base: "/app",
  server: {
    host: "0.0.0.0",
    allowedHosts: ["__AGENT_HOST__"],
    hmr: {
      host: "__AGENT_HOST__",
      protocol: "wss",
      clientPort: 443,
    },
  },
});
```

If a route 404s or 502s after you've set everything up, [Troubleshooting → Routes not working](/agents/troubleshooting#routes) covers the common causes.

## Limits and reserved values

* Up to **10 path routes** per agent
* Up to **5 custom domains** per agent
* Container ports must be between `1025` and `65535`. Port `18789` is reserved for the gateway.
* Subdomains containing the word "pinata" or using reserved suffixes are rejected.


# Secrets
Source: https://docs.pinata.cloud/agents/secrets

Where API keys and credentials live

Your agent needs credentials — an LLM key, sometimes a bot token or two, maybe a database password. The Secrets Vault is where those live. They're encrypted at rest, never returned by the API after you save them, and injected into the agent as environment variables when its container starts.

<Warning>
  You can't read a secret's value back once it's saved. Make sure you have it stored somewhere safe before you paste it in here.
</Warning>

<Frame>
  <img alt="Image" />
</Frame>

## Connect a provider

You don't strictly need one — the Create Agent wizard offers a **Free tier** option that runs your agent on a shared, rate-limited model with no key at all. But connecting your own provider unlocks full model quality, context, and rate limits, and it's how a free-tier agent upgrades later.

Open the [Secrets Vault](https://agents.pinata.cloud/secrets). The row at the top — **AI PROVIDERS** — has a card for each supported provider:

| Provider       | How it connects                                                |
| -------------- | -------------------------------------------------------------- |
| **Anthropic**  | API key                                                        |
| **OpenAI**     | API key, or OAuth into a Codex subscription                    |
| **OpenRouter** | API key                                                        |
| **Pinata**     | [Pinata-hosted inference](/inference/overview) — no key needed |
| **Venice**     | API key (privacy-focused)                                      |
| **Custom**     | Any OpenAI-compatible endpoint                                 |
| **Free tier**  | Nothing — selected in the Create Agent wizard, no key needed   |

Cards that aren't connected show a **CONNECT** button. Click it, follow the prompts, save. The card flips to **Connected**.

You can connect more than one provider — useful for fallbacks, or for routing different agents to different LLMs.

<Note>
  The **Pinata** provider is first-party — connecting it enables [Pinata-hosted inference](/inference/overview) for your account, no API key required. You can also toggle it programmatically with the enable/disable endpoints (see [Inference → Enable](/inference/overview#enable-inference) and [Disable](/inference/overview#disable-inference)).
</Note>

## Add other secrets

For everything that isn't a provider key — bot tokens, third-party API keys, database URLs — use **New Secret** at the top right of the Vault.

The dialog asks for:

1. A **name** (this is the environment variable, like `TELEGRAM_BOT_TOKEN`)
2. A **value**
3. A **type** — `secret` or `variable`

Save. Your agent will see it as `process.env.TELEGRAM_BOT_TOKEN` (or the equivalent in Python, Bash, etc.) on next restart.

### Secret vs variable

Both become env vars. The difference is encryption:

* **Secret** — encrypted at rest, never returned in API responses. Use for anything sensitive: keys, tokens, passwords.
* **Variable** — stored as plaintext, value returned in listings. Use for non-sensitive config — public URLs, feature flags.

If you're unsure, choose **secret**. There's no downside.

### Importing a `.env`

Have a `.env` file already? The New Secret menu has an **Import .env** option that adds every line in one go.

## Make a secret available to an agent

Saving a secret in the Vault doesn't automatically give it to every agent. You attach secrets per-agent — that way you can give different agents different credentials.

* **When creating an agent**: step 3 of the wizard ("Connect") lets you pick which secrets to attach.
* **For an existing agent**: open the agent → **Secrets** → **+ ADD** and pick from your Vault.

On the agent's Secrets tab you'll see two sections:

1. **AI Providers** — the providers you've connected. Each card shows whether it's an `API KEY` connection or a `CODEX SUBSCRIPTION`.
2. **Variables and Secrets** — the non-provider secrets you've attached.

At the bottom of the same page is the **Gateway Token** — the credential other tools use to talk to *this specific agent*. See [HTTP API → Gateway token](/agents/api#gateway-token) for what to do with it.

<Note>
  When you add or update a secret on a running agent, restart the gateway so the agent picks up the new value. The Danger tab shows a per-secret **Synced** indicator so you can tell whether you've already done that.
</Note>

## Updating, removing, and edge cases

* Click the **⋯** menu on any secret to update its value or delete it.
* You can't delete a secret that's still attached to an agent — detach it first, or you'll get a `409 Conflict`.
* Updating a value doesn't roll out automatically — restart the gateway.
* Attaching two credentials for the same provider (say, an OpenAI API key *and* a Codex subscription) returns a `409 Conflict`. Pick one.

## From the CLI

```bash theme={null}
pinata agents secrets list                  # See your secrets
pinata agents secrets add <name> <value>    # Add one
pinata agents secrets delete <name>         # Remove one
```

## How secrets are protected

* Encrypted with AES-GCM using a key derived per user
* Values are never returned by the API after creation
* Injected as environment variables at container start, never written to disk
* The per-agent gateway token is generated automatically and can be rotated from the Danger tab


# Skills
Source: https://docs.pinata.cloud/agents/skills

Reusable capability packages you can attach to an agent

Skills are how you teach an agent new tricks without rewriting it. A skill is a folder of files — code, instructions, sometimes a `.env.example` listing what credentials it needs. When you attach a skill to an agent, those files land in the agent's workspace and the agent can use them.

There are two views to keep separate in your head:

* The **Skills Library** in the sidebar — the catalog of skills installed in your account. This includes the ones Pinata publishes and any you've added from the community or your own folder.
* A specific agent's **Skills tab** — which subset of the library is actually attached to *this* agent. Installing a skill in the Library doesn't attach it; attachment happens per-agent.

<Frame>
  <img alt="Image" />
</Frame>

## Find a skill

Open the **Skills Library** in the sidebar.

Two tabs at the top:

* **All | Installed** — skills already in your library
* **Browse ClawHub** — the community catalog

Search by name in either tab. Each card shows the skill name, author, install count, version, and a CID prefix. Pinata-built skills (like `@pinata/api`) carry a verified badge and show up in both tabs.

### Skills Pinata maintains

* **`@pinata/api`** — store and retrieve files on IPFS, use gateways, run vector searches, sign x402 payments
* **`@pinata/erc-8004`** — register your agent on-chain and verify other agents (ERC-8004)
* **`@pinata/memory-salience`** — let your agent remember what matters across conversations
* **`@pinata/paraspace`** — organize the workspace with the PARA method
* **`@pinata/sqlite-sync`** — a SQLite database with automatic Pinata backup and CID-chained history
* **`@pinata/platform`** — the agent can call back into the platform (install skills, set secrets, manage tasks) without ever seeing your Pinata JWT. Bundled by default.

## Install a skill

Click a card to see its description, files, and required env vars. **Install** adds it to your library. From there, you can attach it to as many agents as you like.

Installing alone doesn't change anything about your running agents — you still need to attach it.

## Attach a skill to an agent

* **When creating a new agent**: step 2 of the wizard lets you pick skills from your library.
* **For an existing agent**: open the agent → **Skills** → **+ ADD** and pick from your library.

Attaching copies the skill's files into the agent's workspace under `workspace/skills/<skill-name>/`. The agent reads from there.

If the skill declares required env vars in a `.env.example`, the Skills tab flags any that aren't set on the agent. Add the missing values in the [Secrets Vault](/agents/secrets) and attach them, then restart the gateway.

<Tip>
  Detaching a skill removes its files from the agent's workspace but keeps the skill installed in your library. You can re-attach later.
</Tip>

## Slugs vs CIDs

Anywhere you reference a skill — in the CLI, in [`manifest.json`](/agents/manifest), in API calls — you have two ways to identify it:

* **ClawHub slug** (`@pinata/api`) — always resolves to the latest published version. Easiest for templates and shared configs.
* **IPFS CID** (`bafybeicglyjdb6w...`) — pins one specific version. Use when you want byte-stable behavior or you're using a self-hosted skill not on ClawHub.

In `manifest.json` each skill is an object with `name` plus either `clawhub_slug` or `cid`:

```json theme={null}
"skills": [
  { "clawhub_slug": "@pinata/api", "name": "Pinata API" },
  { "clawhub_slug": "@pinata/memory-salience", "name": "Memory Salience" }
]
```

See the [manifest reference](/agents/manifest#skills) for the full schema.

## Versions and updates

Skills follow semver. Each card shows the current version (`v1.0.0`, etc.).

* **Update** — if a newer version exists, the Skills tab will show an update available. Update from there. For ClawHub skills, this pulls the latest published version.
* **Roll back** — open a skill's version history and pick an earlier version. Internally that re-pins to that version's CID.

If you're building automation, you can also check for updates via the API: `GET /v0/agents/{agentId}/skills/updates`.

## Publishing your own

Have something useful? Package it as a skill.

1. Put your files in a folder
2. Add a `SKILL.md` with YAML frontmatter — at minimum, `name` and `description`
3. Optional: a `.env.example` declaring required env vars (the UI will read this and flag missing values when someone attaches it)

```markdown theme={null}
---
name: my-skill
description: What this skill does
---

# My Skill

Instructions the agent should follow when using this skill.
```

In the Skills Library, click **Upload Skill**, choose your folder, give it a name and description, then **Upload & Register**.

<Note>
  Skill folders are pinned to public IPFS. Don't include secrets or anything you wouldn't paste into a Gist.
</Note>

## From the CLI

```bash theme={null}
pinata agents skills list              # See your library
pinata agents skills create <path>     # Upload a new skill
pinata agents clawhub list             # Browse the community catalog
pinata agents clawhub install <slug>   # Install a community skill
```

## Limits

* Up to **10 skills** attached per agent
* Skill names: letters, numbers, hyphens, underscores
* Folder size limits follow Pinata's IPFS upload limits


# Files & Snapshots
Source: https://docs.pinata.cloud/agents/snapshots

Browse, version, and edit your agent's workspace

The Files tab gives you four things: a snapshot history of the agent's workspace, content diffs so you can see what changed between snapshots, a file browser for the agent's container, and a git URL so you can clone the workspace locally and edit it like any other repo.

<Frame>
  <img alt="Image" />
</Frame>

## Workspace Snapshots

Every minute or so, when the workspace has changed, Pinata captures a snapshot. Each snapshot is the entire workspace pinned to IPFS as a single CID — your full file tree at that point in time, immutable.

The summary card at the top shows:

* **Status** — `Live`, plus how long ago the workspace last synced
* **Lifecycle Scripts** — whether `build` and `start` are configured (and their run state if they are)
* **Scheduled Tasks** — whether any tasks are set up
* **Files** — total file count and the current snapshot CID

Below the summary is the snapshot list — newest at the top, labeled `LATEST`, `V9`, `V8`, and so on. Click any version to expand its **Content Diff** — a file-by-file view of what changed since the previous snapshot, rendered as either a unified or side-by-side diff.

Common file you'll see diffs for: `manifest.json`. See the [manifest reference](/agents/manifest) for what every field means.

## Restore an older snapshot

If a recent change broke something, you can roll back.

1. Find the snapshot you want in the list
2. Open it
3. **Restore**

Under the hood this runs a `git --hard` reset to the commit associated with that snapshot. The current workspace is replaced — anything that hadn't been captured into a snapshot yet is lost.

<Warning>
  Restore is destructive. Snapshot the current state first if you might want to come back to it.
</Warning>

## Force a snapshot now

Snapshots run on their own, but you can trigger one immediately:

```bash theme={null}
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/snapshots/sync
```

The same endpoint with `GET` returns sync status — whether storage is configured, when it last synced, and a human-readable message.

## Workspace Files — browse the container

Below the snapshot list, the **Workspace Files** panel is a live file browser for the agent's pod filesystem — not just the workspace, the whole container.

* **Navigate** — click a folder to enter it, use the breadcrumb or **← Up** to go back, **Workspace** to jump straight to the workspace root, **Refresh** to re-read the current directory
* **View a file** — click it. Text and markdown files preview inline with size and MIME type; binary files offer a **Download** instead
* **Upload** — drop files onto the listing or click **Upload** to put files into the current directory

Handy for checking what an agent actually wrote, reading logs in place (e.g. `/home/hermes/data/logs/` on a Hermes agent), or dropping in a config file without opening the [Console](/agents/console).

## Editing the workspace locally

The workspace is a git repo, served by Pinata. Clone, edit in your IDE, commit, push.

```bash theme={null}
git clone https://agents.pinata.cloud/v0/agents/{agentId}/git my-agent
```

Click **Copy with Token** in the Files tab and you get a URL with the gateway token already baked in as HTTP Basic auth — no credential setup, just clone and go.

### Pushing changes

```bash theme={null}
# edit files in workspace/ or skills/
git add -A
git commit -m "tweak the workspace"
git push
```

When you push, the server commits any pending agent-side changes first, then applies your push on top. That means your push automatically picks up anything the agent has been writing.

After a push, the agent's `build` script (if you've set one) runs automatically. Check the **Lifecycle Scripts** row to watch its status. Details: [Manifest → Scripts](/agents/manifest#scripts).

### Pulling agent-side changes

Your agent writes to the same workspace as it works. To sync that locally:

```bash theme={null}
git pull
```

### Persistent credentials

Copying the tokened URL every time gets old. Expand **Setup Persistent Credentials** in the Files tab and follow the instructions — git stores the credentials, future operations authenticate automatically.

## Reading or uploading files programmatically

Two endpoints, both on the agent's subdomain:

* `GET /v0/agents/{agentId}/files?path=<path>` — read any file inside the container
* `POST /v0/agents/{agentId}/files/upload` — upload a base64-encoded file into `<workspace>/uploads/`

Both require auth (Pinata JWT or gateway token). See [API → Files](/agents/api#files).


# Tasks
Source: https://docs.pinata.cloud/agents/tasks

Run prompts on a schedule

Tasks are scheduled prompts. The agent runs them automatically — every Monday morning, every six hours, once next Tuesday at 2pm, whatever you set. Use them for daily check-ins, periodic reports, automation triggered by time rather than by a message.

<Frame>
  <img alt="Image" />
</Frame>

## Create a task

Open your agent → **Tasks** → **+ ADD**.

The dialog asks for:

1. **Name** — anything readable, e.g. `daily-check-in`
2. **Schedule** — when it runs
3. **Prompt or system event** — what gets delivered to the agent
4. **Delivery (optional)** — where the response goes if you want it to leave the agent

That's it. Save and the task starts running on the schedule.

## Schedule types

You have three choices for `schedule.kind`:

* **Cron** — standard cron expression. Most flexible.

  ```text theme={null}
  0 9 * * *      # Every day at 9am
  0 */6 * * *    # Every six hours
  0 9 * * 1-5    # Weekdays at 9am
  ```

* **Every** — run every N milliseconds. Set `staggerMs` if you want to randomize the start offset so multiple tasks don't fire at the same instant.

* **At** — run once at a specific ISO timestamp.

You can set a timezone (`tz`) so cron respects DST and local time. Minimum interval across all kinds is 1 minute — anything tighter is rejected.

## What gets delivered

Two payload kinds:

* **`agentTurn`** (default) — your task fires a chat message. The agent treats it like a regular conversation turn. You can pin a specific `model`, toggle reasoning on or off via `thinking`, and set a `timeoutSeconds`.

  Good prompts to schedule:

  > "Check my portfolio and summarize anything that moved more than 5% today."
  >
  > "Generate the weekly status report from the work log."

* **`systemEvent`** — fires a system event instead. The agent sees it as a non-conversational trigger. Useful for heartbeats, periodic maintenance, or anything you don't want polluting the chat history.

## Where the response goes

By default, the response lands back in the agent's main conversation. If you want it to go somewhere else:

* **Announce to a channel** — send the response to Telegram, Slack, or Discord. Set `delivery.mode` to `announce`, plus a channel and recipient. Good for alerts or daily standups.

  ```json theme={null}
  "delivery": { "mode": "announce", "channel": "telegram", "to": "123456789" }
  ```

* **Webhook** — POST it to a URL. Good for hooking into other services.

  ```json theme={null}
  "delivery": { "mode": "webhook", "to": "https://example.com/hook" }
  ```

Set `bestEffort: true` in delivery if you'd rather swallow a delivery failure than mark the whole run as failed.

## Sessions: main vs isolated

A task can run in the agent's main conversation (so its output is part of the running chat history) or in an isolated session of its own. Pick one with `sessionTarget`:

* `main` — output joins the live conversation
* `isolated` — one-off session that doesn't touch the main thread. Good for batch jobs and webhook-style work.

## Wake mode

`wakeMode` controls timing:

* `now` (default) — fire the task at the scheduled time, even if the agent is busy
* `next-heartbeat` — wait for the next heartbeat tick. Useful for low-priority background work.

## Managing tasks

Each task in the list has:

* **Run now** — fire it immediately, doesn't affect the schedule
* **Enable / disable** — pause without deleting
* **Edit** — change schedule, prompt, or delivery
* **Delete** — remove it
* **Runs** — open the run history

Run history shows what fired and when, and whether each run succeeded.

## From the CLI

```bash theme={null}
pinata agents tasks list <agent-id>
pinata agents tasks create <agent-id>
pinata agents tasks run <agent-id> <task-id>
pinata agents tasks delete <agent-id> <task-id>
```

## Limits and gotchas

* **20 tasks** per agent
* Minimum interval **1 minute**
* A task that runs longer than `timeoutSeconds` (or 5 minutes if unset) is cancelled and marked failed
* Tasks survive agent restarts. Schedules that would have fired during downtime are skipped, not queued.


# Creating Templates
Source: https://docs.pinata.cloud/agents/templates/creating

Share your agent configuration as a template

Templates let you package an agent configuration for reuse or sharing. This guide walks through the workflow: clone the starter, customize files, deploy from your template to test, and (optionally) submit it to the marketplace.

For the full `manifest.json` field reference, see the [manifest reference](/agents/manifest).

<Card title="Reference Template" icon="github" href="https://github.com/PinataCloud/agent-template">
  Clone this starter repo to create your own template.
</Card>

## Clone the Starter

Fork or clone the [reference template](https://github.com/PinataCloud/agent-template) to get started:

```bash theme={null}
git clone https://github.com/PinataCloud/agent-template my-template
cd my-template
```

Your repository should have this structure:

```text theme={null}
manifest.json                # Agent config - see /agents/manifest
README.md                    # Description shown in marketplace listing
workspace/
  BOOTSTRAP.md               # First-run conversation guide (self-deletes after setup)
  SOUL.md                    # Agent personality and principles
  AGENTS.md                  # Workspace conventions, memory system, safety rules
  IDENTITY.md                # Agent name, vibe, emoji (filled in during bootstrap)
  USER.md                    # Notes about the human (learned over time)
  TOOLS.md                   # Environment-specific notes
  HEARTBEAT.md               # Periodic tasks (empty by default)
```

## Configure `manifest.json`

The manifest defines your agent's identity, required secrets, skills, scripts, routes, channels, tasks, and marketplace metadata. The starter ships with a `_docs` block documenting every field inline.

| Section    | What it does                                                                           |
| ---------- | -------------------------------------------------------------------------------------- |
| `agent`    | Name, description, vibe, emoji                                                         |
| `model`    | Default AI model                                                                       |
| `secrets`  | Encrypted API keys and credentials                                                     |
| `skills`   | Attachable skill packages — objects with `clawhub_slug` or `cid` and a `name` (max 10) |
| `tasks`    | Cron-scheduled prompts (max 20)                                                        |
| `scripts`  | Lifecycle hooks - `build` runs after deploy/push, `start` runs on agent boot           |
| `routes`   | Port forwarding for web apps/APIs (max 10)                                             |
| `channels` | Telegram, Discord, Slack configuration                                                 |
| `template` | Marketplace listing metadata                                                           |

See the [manifest reference](/agents/manifest) for every field's type, defaults, and limits.

<Warning>
  Remove the `_docs` block before submitting to the marketplace.
</Warning>

### Skills

Skills are objects with a `name` and either a `clawhub_slug` or a `cid`. Use `clawhub_slug` for marketplace skills — they'll automatically resolve to the latest published version when someone deploys your template. Use `cid` to pin a specific uploaded skill by its IPFS CID for byte-stable behavior.

```json theme={null}
// ClawHub marketplace skill
{
  "clawhub_slug": "@pinata/api",
  "name": "Pinata API"
}

// Uploaded skill pinned by CID
{
  "cid": "bafkreiexample...",
  "name": "My Custom Skill"
}
```

### Example

A complete manifest that includes secrets, skills, lifecycle scripts, a web route, and a daily scheduled task:

```json theme={null}
{
  "$schema": "https://agents.pinata.cloud/schemas/manifest.v1.json",
  "version": 1,
  "engine": "openclaw",
  "agent": {
    "name": "Useful Assistant",
    "description": "Personal AI helper with file reading, web search, and memory management",
    "emoji": "🧠",
    "vibe": "Resourceful and opinionated"
  },
  "template": {
    "slug": "useful-assistant",
    "category": "general",
    "partnerName": "Pinata",
    "tags": ["assistant", "personal", "productivity"]
  },
  "secrets": [
    {
      "name": "EXAMPLE_API_KEY",
      "description": "Optional API key for external service",
      "required": false
    }
  ],
  "scripts": {
    "build": "cd workspace/projects/hello-test && npm install --include=dev",
    "start": "cd workspace/projects/hello-test && npx vite --host 0.0.0.0"
  },
  "skills": [
    { "clawhub_slug": "@pinata/api", "name": "Pinata API" }
  ],
  "routes": [
    { "port": 5173, "path": "/app", "protected": false }
  ],
  "tasks": [
    {
      "name": "daily-check-in",
      "schedule": { "kind": "cron", "expr": "0 9 * * *" },
      "payload": { "kind": "agentTurn", "text": "Good morning! Review my workspace and suggest priorities for today." }
    }
  ]
}
```

## Customize Workspace Files

Files in `workspace/` are copied to the agent's workspace on deploy. Customize these to define your agent's personality and behavior:

| File           | Purpose                                                 |
| -------------- | ------------------------------------------------------- |
| `BOOTSTRAP.md` | First-run conversation guide (self-deletes after setup) |
| `SOUL.md`      | Agent personality and principles - **customize this**   |
| `AGENTS.md`    | Workspace conventions, memory system, safety rules      |
| `IDENTITY.md`  | Agent name, vibe, emoji (filled in during bootstrap)    |
| `USER.md`      | Notes about the human (learned over time)               |
| `TOOLS.md`     | Environment-specific notes                              |
| `HEARTBEAT.md` | Periodic tasks (empty by default)                       |

Focus on `SOUL.md` to give your agent a distinct personality, and `BOOTSTRAP.md` if you want a custom onboarding flow.

## Add Web App Support (Optional)

If your agent runs a server, API, or frontend dev server, add two things to `manifest.json`:

1. **`scripts`** - lifecycle hooks that install deps and start the server
2. **`routes`** - port forwarding rules that expose the server to the internet

Example from a Vite + React agent:

```json theme={null}
{
  "scripts": {
    "build": "cd workspace/projects/myapp && npm install --include=dev",
    "start": "cd workspace/projects/myapp && npx vite --host 0.0.0.0"
  },
  "routes": [
    { "port": 5173, "path": "/app", "protected": false }
  ]
}
```

**Important details:**

* **`build`** - Runs once after deploy or git push (e.g. `npm install`, compile). Executes from `/home/node/clawd`. Output logged to `/tmp/user-build.log`. 5 min timeout. If build fails, start does not run.
* **`start`** - Launches a long-running background process after build completes (e.g. a web server). Executes from `/home/node/clawd`. Output logged to `/tmp/user-start.log`. Runs detached so it survives the runner exiting.
* **Retry scripts** - To re-run the build/start lifecycle (e.g. after pushing changes that didn't trigger a build), use the **Retry Scripts** action on the Danger tab or call `POST /v0/agents/{agentId}/scripts/retry`.
* Your server **must bind to `0.0.0.0`**, not `localhost`, or it won't be reachable
* Set `protected: false` for public routes, or `true` to require a gateway token
* Use `__AGENT_HOST__` as a placeholder in config files - replaced at runtime with the agent's public hostname
* For WebSocket/HMR setups (e.g. Vite), connect via WSS on port 443 through `__AGENT_HOST__`

Example Vite config using the host placeholder:

```ts theme={null}
export default defineConfig({
  base: "/app",
  server: {
    host: "0.0.0.0",
    allowedHosts: ["__AGENT_HOST__"],
    hmr: {
      host: "__AGENT_HOST__",
      protocol: "wss",
      clientPort: 443,
    },
  },
});
```

## Deploy From Your Template

You can deploy an agent from your template before submitting it to the marketplace. This is the fastest way to iterate.

**Via the UI:**

1. Go to [agents.pinata.cloud/agents](https://agents.pinata.cloud/agents) and click **Create Agent**
2. Select **Start from a Template**
3. Paste your public repository URL (GitHub or GitLab)
4. Complete the setup flow and deploy

**Via the CLI:**

```bash theme={null}
pinata agents create --template https://github.com/you/my-template
```

Push changes to your repo and redeploy to test updates. Once you're happy with your template, you can optionally submit it to the marketplace.

<Frame>
  <img alt="Image" />
</Frame>

## Submit to Marketplace (Optional)

Share your template with the community by submitting it for review.

**Via the UI:**

1. Go to [My Templates](https://agents.pinata.cloud/templates) → **+ New Template**
2. Enter your public repository URL
3. Click **Validate** to check for required files
4. Review the parsed manifest and workspace files
5. Click **Submit** to send for review

**Via the CLI:**

```bash theme={null}
# Validate your repo
pinata agents templates validate https://github.com/user/my-template

# Submit for review
pinata agents templates submit https://github.com/user/my-template

# Submit from a specific branch
pinata agents templates submit https://github.com/user/my-template --branch develop
```

Template status progression:

* **draft** → **pending\_review** → **published** (or **rejected**)
* Published templates can be **archived** (soft-deleted) and resubmitted later

## Managing Submissions

Track and update your templates in [**My Templates**](https://agents.pinata.cloud/templates) or via CLI:

```bash theme={null}
# List your submissions
pinata agents templates mine

# Update (re-pull from repo)
pinata agents templates update <template-id>

# Archive a submission
pinata agents templates delete <template-id>
```

## Paid Templates

Templates submitted with a paid tier use x402 micropayments for one-time purchase. Buyers pay before deploy; once paid, the template behaves like any other in their account.

Mark a template as paid in the `template` block of `manifest.json` - see the [manifest reference](/agents/manifest#template) for the full schema.


# Templates
Source: https://docs.pinata.cloud/agents/templates/overview

Deploy pre-configured agents in one click

<Note>
  Templates are currently in beta.
</Note>

Templates are the fastest path to a working agent. Instead of configuring everything yourself, you pick a template that does what you need — it ships with the right skills, settings, and personality already in place. You add your API keys, and you deploy.

<Frame>
  <img alt="Image" />
</Frame>

## Where to look

Two places, depending on what you're after:

* [**Marketplace**](https://agents.pinata.cloud/marketplace) — every published template, from Pinata and from other builders. Filter by category, engine, or tags. Featured templates float to the top. Some are paid; those use x402 micropayments and you complete the purchase in flow.
* [**My Templates**](https://agents.pinata.cloud/templates) — templates you've created, submitted, or imported. The **New Template** card at the top-left lets you import one from a public GitHub or GitLab repo.

Click any template card to open its details — description, skills it bundles, secrets it'll ask you for, and any scheduled tasks it sets up.

## Deploying

Once you've found one you like:

1. Click **Deploy This Agent**
2. Name your agent
3. Provide whatever secrets the template asks for (usually just your LLM key)
4. **Deploy**

Provisioning takes about 30 seconds. You'll land on the agent's Chat tab, ready to talk to it.

### A note on secrets

Every template needs at least an LLM provider key (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.). Many need more — a Telegram template needs a bot token, a trading template might need exchange credentials.

The template card spells out exactly what it needs. You can add the secrets ahead of time in the [Secrets Vault](/agents/secrets), or wait — the deploy flow prompts you for anything missing.

## Using the CLI

```bash theme={null}
pinata agents templates list                    # Browse templates
pinata agents create --template <template-id>   # Deploy one
```

## Create Your Own

Built something useful? Package it as a template and share it with others - or just use it to quickly spin up copies of your own agents.

<CardGroup>
  <Card title="Creating Templates" icon="hammer" href="/agents/templates/creating">
    Walk through packaging your agent as a reusable template
  </Card>

  <Card title="Manifest reference" icon="file-code" href="/agents/manifest">
    Full reference for every field in `manifest.json`
  </Card>
</CardGroup>

<Card title="Reference Template" icon="github" href="https://github.com/PinataCloud/agent-template">
  Clone this starter repo - it includes working examples of routes, tasks, lifecycle scripts, and secrets.
</Card>


# Troubleshooting
Source: https://docs.pinata.cloud/agents/troubleshooting

What to check when your agent isn't behaving

Most problems with an agent fall into a small number of buckets, and the answer is almost always in one of five places. Before you scroll through symptoms, walk these in order — you'll often find it on step one.

1. **Logs** — open the [Logs](/agents/logs) tab, uncheck everything except WARN and ERROR, and scroll up to the most recent red line. That's usually the answer.
2. **Danger tab** — the [Danger](/agents/devtools) tab is the inventory page. Check status, last sync, the lifecycle-script state, and whether your secrets show as `Synced`.
3. **Console** — the [Console](/agents/console) is a shell. `tail /tmp/user-build.log`, `env`, `ps aux` answer about 80% of "why doesn't my service work" questions.
4. **Files** — if something used to work and broke after a recent change, restore an earlier snapshot from the [Files](/agents/snapshots) tab.
5. **Validate config** — `POST /v0/templates/validate` checks `manifest.json`; `POST /v0/agents/{agentId}/config/validate` checks the engine runtime config shown on the Danger tab. These are different files, see [Concepts](/agents/concepts#manifest-json-vs-openclaw-json) for the difference.

If you're staring at an HTTP error code, the [Error reference](/agents/errors) goes status-by-status with the common responses.

The rest of this page is symptom-first. Find what's happening, jump there.

## Agent won't start / stays in `starting`

Symptom: status badge stuck on `starting`, no logs appearing, chat won't connect.

**Check:**

```bash theme={null}
# Are we still spinning up the container?
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID | jq '.processStatus'

# Did the build script blow up?
# Console → tail -n 200 /tmp/user-build.log
```

**Common causes:**

| Cause                                     | Fix                                                                                                             |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `build` script failed                     | Fix the script, then **Retry Scripts** on the Danger tab (or `POST /v0/agents/{agentId}/scripts/retry`)         |
| Missing required secret                   | The Danger page flags missing secrets. Attach in the [Secrets Vault](/agents/secrets), then restart the gateway |
| Container OOM                             | A heavy `npm install` or model load can hit the memory ceiling. Slim dependencies or break up the script        |
| Engine version pinned to a broken release | Danger → **Change** to update to latest, when the engine exposes version changes                                |

## Gateway won't connect / chat is offline

Symptom: chat shows "disconnected" or fails to reconnect.

**Check:** the agent is `running` on the Danger tab, then look at Logs filtered to `gateway/ws`.

**Common causes:**

| Cause                                                         | Fix                                                                        |
| ------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Gateway crashed                                               | Danger → **Restart Gateway**                                               |
| Gateway token rotated and your client cached the old one      | Copy the latest token from Danger, update your client                      |
| Network/route returning 502                                   | Hard-refresh the page; if it persists, restart the gateway                 |
| Browser blocked third-party cookies for `agents.pinata.cloud` | Pass the token via header or query string instead of relying on the cookie |

## Build script failed

Symptom: Danger → **Lifecycle Scripts** shows `build` failed; `start` never ran.

**Read the log:**

```bash theme={null}
# Console
tail -n 200 /tmp/user-build.log
```

**Common causes:**

* Wrong working directory - `build` runs from `/home/node/clawd`, not the workspace
* Missing tooling (`pnpm`, `yarn`) - install in the build script first, or use a tool that ships with the image (`npm`)
* `package.json` not found - `cd` into the right project subfolder
* 5-minute timeout - split the work or pre-install in a snapshot

Fix the script, then `POST /v0/agents/{agentId}/scripts/retry` (or click **Retry Scripts**).

## Start script crashes immediately

Symptom: `start` finishes seconds after launching - service is unreachable.

```bash theme={null}
tail -n 200 /tmp/user-start.log
ss -tlnp
```

**Common causes:**

| Cause                                                | Fix                                                                                    |                |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------- |
| Bound to `127.0.0.1` / `localhost`                   | Bind to `0.0.0.0` so the gateway can reach it                                          |                |
| Port mismatch with `routes` entry                    | Make sure `manifest.json` `routes[i].port` matches what the server actually listens on |                |
| Missing env var                                      | `start` runs after `build` with the agent's full env - confirm with \`env              | grep MY\_VAR\` |
| Process exits because it expected an interactive TTY | Add `--no-color`/`--no-progress` flags, or daemonize properly                          |                |

<a />

## Routes not working / 404 / 502

Symptom: the URL for your path route or custom domain doesn't load.

**Walk through:**

```bash theme={null}
# Is your service even listening?
ss -tlnp

# Hit it from inside the container
curl -i http://localhost:5173/

# Does the route show up in the API?
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/port-forwarding
```

**Common causes:**

| Cause                                   | Fix                                                                                                                                               |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Service bound to `localhost`            | Bind to `0.0.0.0`                                                                                                                                 |
| Wrong port                              | Match the listener's port in the route definition                                                                                                 |
| Path prefix mismatch                    | The prefix is stripped before reaching your service - `/app/foo` becomes `/foo`. Set your framework `base` accordingly (e.g. Vite `base: "/app"`) |
| Allowed-hosts not configured            | Vite/Next dev servers reject unknown hosts - add `__AGENT_HOST__` to the allow-list                                                               |
| Custom domain stuck `pending_ownership` | TXT record not visible yet. Wait for DNS propagation or `dig TXT _pinata-verify.<your-domain>`                                                    |
| Custom domain stuck `pending`           | Cloudflare is still provisioning - usually clears in a few minutes                                                                                |

See [Routes & Domains](/agents/routes) for the full domain status table.

## Secret update didn't apply

Symptom: you updated a secret in the Vault but the agent still uses the old value.

**Check:** Danger → **Secrets** → the per-secret **Synced** indicator. Out of sync means the running gateway hasn't been restarted since the update.

**Fix:** Danger → **Restart Gateway**, or `POST /v0/agents/{agentId}/restart`.

Secrets are injected at process start - they are not hot-reloaded.

## Skill missing / not loading

Symptom: agent doesn't seem to know about a skill you attached.

**Check:**

```bash theme={null}
# Is it actually attached?
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID | jq '.skills'

# Are the files in the workspace?
# Console: ls workspace/skills/
```

**Common causes:**

| Cause                                                          | Fix                                                                                                         |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Skill attached but agent not restarted                         | Restart the gateway                                                                                         |
| Required env var declared in `.env.example` is missing         | The Skills tab flags missing vars when you open the skill. Add the value to the Secrets Vault and attach it |
| Skill version is out of date                                   | Skills tab → click the skill → **Update**                                                                   |
| Skill installed from CID, not slug, and a newer version exists | Switch to the slug to track latest, or pin to a newer CID                                                   |

## Custom domain stuck pending

See [Routes → Custom domains](/agents/routes#custom-domains) - statuses are `pending_ownership` (TXT record), `pending` (Cloudflare provisioning), then `active`.

```bash theme={null}
# Confirm DNS
dig TXT _pinata-verify.app.example.com +short

# Re-trigger verification
curl -X POST \
  -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/domains/$DOMAIN_ID/verify
```

## Device pairing rejects / stuck pending

Symptom: a paired client (CLI, mobile) can't connect, or stays pending forever.

```bash theme={null}
curl -H "Authorization: Bearer $PINATA_JWT" \
  https://agents.pinata.cloud/v0/agents/$AGENT_ID/devices
```

**Common causes:**

* The device never sent an approval request - re-pair from the client
* You're on a different workspace than the one you approved from - check **Account → Workspaces**
* The agent is `not_running` - pairing requires the gateway to be up

**Fix:** approve the request from the Danger page or `POST /v0/agents/{agentId}/devices/approve-all`.

## Git push rejected

Symptom: `git push` to your agent's workspace fails.

**Common causes:**

| Cause                         | Fix                                                                                                  |
| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| Authentication failed         | Copy a fresh URL with **Copy with Token** from the Files tab                                         |
| Local branch diverged         | `git pull --rebase` first - the server commits the agent's pending changes before applying your push |
| Pre-receive validation failed | Look at the rejected output - often a manifest validation error. Validate locally before pushing     |

## Tasks aren't firing

Symptom: a scheduled task isn't running.

**Check:**

* Is the task enabled? `GET /v0/agents/{agentId}/tasks` with `includeDisabled=true`
* Is the agent running? Tasks fire only while the gateway is up; missed runs during downtime are skipped
* Check `cron/runner` lines in Logs around the expected time
* Trigger manually: `POST /v0/agents/{agentId}/tasks/{jobId}/run` - if that works, the schedule is wrong

## Costs / model usage looks off

Switch the agent's model dropdown in chat to see what model is actually being used, or run `/status`. Tasks can specify their own `model` - re-check `payload.model` if a scheduled run looks unexpected.

## Still stuck?

Open a ticket from **Support → Chat with us** in the sidebar, or email `team@pinata.cloud`. Useful info to include:

* Agent ID (Danger → Agent → Agent ID)
* Engine and version, if shown on the Danger tab
* A recent `EXPORT VISIBLE` from the Logs tab (filter to `ERROR` first)
* The relevant section of `manifest.json` if config-related
* What you expected vs. what happened

For HTTP errors, the [Error reference](/agents/errors) often has the answer faster than a support round-trip.


# 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).

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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"

<img />

Additionally, with our Bulk File Actions tool, you can select and manage multiple files at once - up to 100!

<img />


# 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

This will return the Group info

<CodeGroup>
  ```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"
    }
  }
  ```
</CodeGroup>

### 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).

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

Another option is to add files after the fact using the [addFiles](/sdk/groups/public/add-files) method.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

Removing files can be done the exact same way with the [removeFiles](/sdk/groups/public/remove-files) method.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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`.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

This will return the same group info received upon creation.

<CodeGroup>
  ```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"
    }
  }
  ```
</CodeGroup>

### List All Groups

If you want to get all Groups or filter through them, you can use the [list](/sdk/groups/public/list) method.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

Results can be filtered with the following queries.

#### name

* Type: `boolean`

Filters groups based on the group name

<CodeGroup>
  ```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"
  ```
</CodeGroup>

#### limit

* Type: `number`

Limits the number of results

<CodeGroup>
  ```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"
  ```
</CodeGroup>

This will return an array of Groups and their respective info:

<CodeGroup>
  ```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"
  }
  ```
</CodeGroup>

### Updating a Group

You can update the name of a group using either the SDK or the API.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

This will return the updated Group info.

<CodeGroup>
  ```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"
    }
  }
  ```
</CodeGroup>

### Delete a Group

<Note>
  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
</Note>

To delete a Group you can use the [delete](/sdk/groups/public/delete) method and pass in the `groupId`.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

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.

![cover](https://dweb.mypinata.cloud/files/bafybeieo4ww5lykpsegfgmse5o2d5s5onfvlkglzqmvbeldb2dkx26z2ve)

## 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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).

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## 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 `===`.

<Tip>
  You can chain multiple key-value queries together and it will only return files that meet both values.
</Tip>

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## Deleting

You can remove a key-value entry by making the value `null`.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## Further Reading

Check out some of our reading material on some of the possibilities of key-values and file-centric architecture!

<CardGroup>
  <Card title="Pinata’s KV Store - A File-Centric Database" href="https://pinata.cloud/blog/pinatas-kv-store-a-file-centric-database" />

  <Card title="Using File-Centric Architecture to Build Simple and Capable Apps" href="https://pinata.cloud/blog/using-file-centric-architecture-to-build-simple-and-capable-apps/" />
</CardGroup>


# 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

This will return an array of file objects

<CodeGroup>
  ```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"
  	}
  }
  ```
</CodeGroup>

## Filters

When listing files there a few ways you can filter the results

### name

* Type: `string`

Filter results based on name

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### group

* Type: `string`

Filter results based on group ID

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### noGroup

* Type: `boolean`

Filter results to only show files that are not part of a group

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### cid

* Type: `string`

Filter results based on CID

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### mimeType

* Type: `string`

Filter results based on mime type

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### keyvalues

* Type: `Record<string | string>`

Filter results based on keyvalue pairs in metadata

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### order

* Type: `"ASC" | "DESC"`

Order results either ascending or descending by created date

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### limit

* Type: `number`

Limit the number of results

<CodeGroup>
  ```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"
  ```
</CodeGroup>

### cidPending

* Type: `boolean`

Filters results and only returns files where `cid` is still `pending`

<CodeGroup>
  ```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"
  ```
</CodeGroup>

## 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 ✨


# MPP Server
Source: https://docs.pinata.cloud/files/mpp/overview

Account-free IPFS uploads and downloads using crypto payments via the Machine Payments Protocol

Pinata's MPP Server enables account-free file uploads and downloads to IPFS using crypto payments. No API key or Pinata account required — just pay with USDC on Tempo and get your files stored on IPFS.

## Overview

The MPP Server is a payment-gated IPFS gateway built on the [Machine Payments Protocol (MPP)](https://docs.tempo.xyz). It uses the HTTP 402 challenge-response pattern:

1. Client makes a request to upload or download a file
2. Server responds with `402 Payment Required` and a payment challenge
3. Client pays USDC on the Tempo network
4. Client retries the request with proof of payment
5. Server verifies payment and fulfills the request

This is ideal for **machine-to-machine** workflows where agents, bots, or services need to store and retrieve files without managing API keys.

## Endpoints

| Method | Endpoint                          | Description             |
| ------ | --------------------------------- | ----------------------- |
| `POST` | `/v1/pin/public?fileSize={bytes}` | Get a signed upload URL |
| `GET`  | `/v1/pin/public/:cid`             | Download a file by CID  |

### Upload

Send a `POST` request with the `fileSize` query parameter (in bytes). After payment, the server returns a signed Pinata upload URL that you can use to upload your file directly.

**Pricing:** `fileSize (GB) x $0.10 x 12 months`, minimum \$0.01 USDC

| File Size | Cost             |
| --------- | ---------------- |
| 1 KB      | \$0.01 (minimum) |
| 1 MB      | \$0.01 (minimum) |
| 1 GB      | \$1.20           |
| 10 GB     | \$12.00          |

The signed URL is valid for **120 seconds** after generation.

### Download

Send a `GET` request with the file's CID. After payment, the server proxies the file from Pinata's IPFS gateway with the correct `Content-Type` and `Content-Disposition` headers.

**Pricing:** Flat \$0.01 USDC per download

## Network

| Property | Value                                               |
| -------- | --------------------------------------------------- |
| Chain    | Tempo Mainnet                                       |
| Chain ID | 4217                                                |
| RPC      | `https://rpc.tempo.xyz`                             |
| Token    | USDC (`0x20C000000000000000000000b9537d11c60E8b50`) |

## Production URL

The Pinata-hosted MPP Server is available at:

```
https://mpp.pinata.cloud
```

## Next Steps

<CardGroup>
  <Card title="Quick Start" icon="bolt" href="/files/mpp/quickstart">
    Upload your first file with MPP
  </Card>

  <Card title="Self-Hosting" icon="server" href="/files/mpp/self-hosting">
    Deploy your own MPP Server
  </Card>
</CardGroup>


# Quick Start
Source: https://docs.pinata.cloud/files/mpp/quickstart

Upload and download files from IPFS using the MPP Server

Upload your first file to IPFS using MPP payments. No Pinata account required.

## Prerequisites

* A wallet with USDC on the Tempo network (chain ID 4217)
* One of the following:
  * [Tempo CLI](https://docs.tempo.xyz) installed
  * `mppx` client SDK (`npm install mppx viem`)

## Using the Tempo CLI

The simplest way to interact with the MPP Server.

### Upload a File

```bash theme={null}
# 1. Get a signed upload URL (pay USDC automatically)
RESPONSE=$(tempo request -X POST \
  "https://mpp.pinata.cloud/v1/pin/public?fileSize=1024")

# 2. Extract the signed URL
URL=$(echo $RESPONSE | jq -r '.url')

# 3. Upload your file to the signed URL
curl -X POST "$URL" \
  -F "file=@your-file.txt"
```

### Download a File

```bash theme={null}
# Download a file by CID (pay USDC automatically)
tempo request -X GET \
  "https://mpp.pinata.cloud/v1/pin/public/QmYourCid" \
  -o your-file.txt
```

## Using the mppx SDK

For programmatic access in TypeScript/JavaScript applications.

### Install Dependencies

```bash theme={null}
npm install mppx viem
```

### Upload a File

```typescript theme={null}
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const mppx = Mppx.create({ methods: [tempo({ account })] });

// 1. Get a signed upload URL (handles 402 payment automatically)
const response = await mppx.fetch(
  "https://mpp.pinata.cloud/v1/pin/public?fileSize=1024",
  { method: "POST" }
);

const { url } = await response.json();

// 2. Upload your file to the signed URL
const file = new File(["hello world"], "hello.txt", { type: "text/plain" });
const formData = new FormData();
formData.append("file", file);
await fetch(url, {
  method: "POST",
  body: formData,
});
```

### Download a File

```typescript theme={null}
import { Mppx, tempo } from "mppx/client";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY");
const mppx = Mppx.create({ methods: [tempo({ account })] });

// Download a file by CID (handles 402 payment automatically)
const response = await mppx.fetch(
  "https://mpp.pinata.cloud/v1/pin/public/QmYourCid"
);

const content = await response.blob();
```

## How the Payment Flow Works

When using the `mppx` SDK or Tempo CLI, the 402 payment flow is handled automatically. Here's what happens under the hood:

```
Client                          MPP Server                    Tempo Network
  |                                |                              |
  |  POST /v1/pin/public           |                              |
  |------------------------------->|                              |
  |                                |                              |
  |  402 + Payment Challenge       |                              |
  |<-------------------------------|                              |
  |                                |                              |
  |  Pay USDC                      |                              |
  |-------------------------------------------------------------->|
  |                                |                              |
  |  POST /v1/pin/public           |                              |
  |  + Authorization (payment proof)                              |
  |------------------------------->|                              |
  |                                |  Verify payment              |
  |                                |----------------------------->|
  |                                |                              |
  |  200 + { url: "signed_url" }   |                              |
  |<-------------------------------|                              |
```

## Error Handling

| Status | Meaning                                                            |
| ------ | ------------------------------------------------------------------ |
| `400`  | Missing or invalid `fileSize` query parameter                      |
| `402`  | Payment required — include payment proof in `Authorization` header |
| `404`  | File not found on IPFS (download only)                             |

## Next Steps

* [Self-Hosting](/files/mpp/self-hosting) — Deploy your own MPP Server instance
* [x402 Monetization](/files/x402/intro) — Monetize private IPFS content with x402


# Self-Hosting
Source: https://docs.pinata.cloud/files/mpp/self-hosting

Deploy your own MPP Server on Cloudflare Workers

The MPP Server is open source and designed to run on Cloudflare Workers. Deploy your own instance to customize pricing, integrate with your own Pinata account, and control the payment flow.

## Prerequisites

* [Node.js](https://nodejs.org/) installed
* [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/) installed
* Cloudflare account
* Pinata account with API key
* Stripe account with crypto payments enabled

## Setup

### 1. Clone the Repository

```bash theme={null}
git clone git@github.com:PinataCloud/mpp-server.git
cd mpp-server
npm install
```

### 2. Configure Environment Variables

Copy the example environment file and fill in your values:

```bash theme={null}
cp .dev.vars.example .dev.vars
```

| Variable               | Description                                                                                   |
| ---------------------- | --------------------------------------------------------------------------------------------- |
| `PINATA_JWT`           | Your Pinata API JWT from the [Pinata dashboard](https://app.pinata.cloud/developers/api-keys) |
| `PINATA_GATEWAY_TOKEN` | Gateway authentication token                                                                  |
| `PINATA_GATEWAY_URL`   | Your Pinata gateway domain (e.g., `your-gateway.mypinata.cloud`)                              |
| `MPP_SECRET_KEY`       | 32-byte hex key for signing 402 challenges                                                    |
| `STRIPE_SECRET_KEY`    | Stripe API key with crypto payments preview access                                            |

Generate an MPP secret key:

```bash theme={null}
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```

### 3. Local Development

```bash theme={null}
npm run dev
```

The server starts at `http://localhost:8787`.

### 4. Deploy

```bash theme={null}
# Deploy to production
npm run deploy

# Or deploy to a specific environment
npm run deploy:dev
npm run deploy:prod
```

Set your secrets in Cloudflare (alternative to `.dev.vars` for deployed environments):

```bash theme={null}
npx wrangler secret put PINATA_JWT
npx wrangler secret put PINATA_GATEWAY_TOKEN
npx wrangler secret put MPP_SECRET_KEY
npx wrangler secret put STRIPE_SECRET_KEY
```

## Architecture

The MPP Server is built with:

| Component           | Technology                                     |
| ------------------- | ---------------------------------------------- |
| Runtime             | Cloudflare Workers                             |
| Framework           | [Hono](https://hono.dev/)                      |
| Payments            | [mppx](https://www.npmjs.com/package/mppx) SDK |
| Storage             | [Pinata](https://pinata.cloud) IPFS            |
| Blockchain          | Tempo Mainnet (USDC)                           |
| Recipient Addresses | Stripe PaymentIntents API                      |

### Request Flow

1. **CORS middleware** handles cross-origin requests
2. **MPP middleware** intercepts `/v1/pin/*` routes, calculates pricing, and manages the 402 challenge-response flow
3. **Route handlers** create signed Pinata upload URLs or proxy file downloads
4. **Stripe** generates unique deposit addresses for each payment

### Pricing Logic

Upload pricing is calculated per request based on file size:

```
price = fileSize (GB) x $0.10 x 12 months
minimum = $0.01 USDC
```

Download pricing is a flat rate:

```
price = $0.01 USDC per download
```

You can customize these values in `src/index.ts`.

## Project Structure

```
mpp-server/
├── src/
│   ├── index.ts          # App entry, middleware, pricing logic
│   ├── main.ts           # Landing page HTML
│   ├── routes/
│   │   └── pin.ts        # Upload and download handlers
│   └── utils/
│       └── types.ts      # TypeScript types, helper functions
├── wrangler.jsonc        # Cloudflare Workers configuration
├── .dev.vars.example     # Environment variable template
└── package.json
```

## Resources

* [MPP Server Repository](https://github.com/PinataCloud/mpp-server)
* [Tempo Documentation](https://docs.tempo.xyz)
* [mppx SDK](https://www.npmjs.com/package/mppx)
* [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)


# 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

<img alt="Image" />

Each collection shows:

| Field      | Description     |
| ---------- | --------------- |
| Name       | Collection name |
| Chain      | Blockchain      |
| NFTs Owned | Your NFT count  |
| CIDs       | IPFS CIDs found |

## Collection Details

<img alt="Image" />

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

<CardGroup>
  <Card title="Manual Sync" icon="rotate">
    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.
  </Card>

  <Card title="Automated Backups" icon="clock">
    Enable auto-backup to automatically pin new CIDs whenever you sync. Set it and forget it—your NFTs stay protected.
  </Card>
</CardGroup>

## 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.

<iframe title="YouTube video player" />

## 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

<CardGroup>
  <Card title="Wallets" icon="wallet" href="/files/nft-backup/wallets">
    Add and sync wallets
  </Card>

  <Card title="Collections" icon="grid-2" href="/files/nft-backup/collections">
    View your collections
  </Card>

  <Card title="Sync" icon="cloud-arrow-up" href="/files/nft-backup/sync">
    Sync CIDs
  </Card>
</CardGroup>


# 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

<img alt="Image" />

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.

<img alt="Image" />

## 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-values).

For a more robust example, check out our guides on Hono and React!

<CardGroup>
  <Card href="/frameworks/hono" icon="fire" title="Hono Guide" />

  <Card href="/frameworks/react" icon="react" title="React Guide" />
</CardGroup>

## Usage

Setting up a server side API endpoint might look something like this:

<CodeGroup>
  ```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 });
    }
  }
  ```
</CodeGroup>

Then back on the client side code, you can upload using the signed URL instead of the regular upload endpoint.

<Note>
  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.
</Note>

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## 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<string, string>`

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



<Note>
  Available on Enterprise plans. Contact [sales@pinata.cloud](mailto:sales@pinata.cloud) for more info.
</Note>

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

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### Listing and Querying

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### Groups

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## 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.

<Card title="PinataCloud/concealmint-client" href="https://github.com/PinataCloud/concealmint-client" icon="github">
  GitHub repo for [CONCEALMINT](https://concealmint.com), an app for creating and access Private NFTs
</Card>

<CardGroup>
  <Card title="Making Private NFTs" href="https://pinata.cloud/blog/making-private-nfts/">
    By using Private IPFS you can gate acceess to files based on NFT ownership
  </Card>

  <Card title="How to Build a Farcaster Native Gumroad on Base" href="https://www.pinata.cloud/blog/how-to-build-a-farcaster-native-gumroad-on-base/">
    Sell digital content inside Farcaster frames and keeping it secure through Private IPFS
  </Card>

  <Card title="How To Build Simple Logging For Your Crypto App With Private IPFS" href="https://www.pinata.cloud/blog/how-to-build-simple-logging-for-your-crypto-app-with-private-ipfs/">
    Add simple yet private logging to your crypto app using Private IPFS
  </Card>

  <Card title="Building a Macintosh-Inspired Retro File System in the Browser" href="https://www.pinata.cloud/blog/building-a-macintosh-inspired-retro-file-system-in-the-browser/">
    See the possibilities of file management using Groups and Private IPFS
  </Card>
</CardGroup>


# 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).

<CodeGroup>
  ```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
  ```
</CodeGroup>

## 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://<YOUR_GATEWAY_DOMAIN>.mypinata.cloud/ipfs/<CID>`,
  {
    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.

<Note>
  This method will check all CIDs on Pinata and will return a signature if it exists
</Note>

```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.

<CodeGroup>
  ```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
  ```
</CodeGroup>

## 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



<img />

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

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

This will return the following response

<CodeGroup>
  ```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
    }
  }
  ```
</CodeGroup>

* `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

<Tip>
  [Check out the Key-Values doc for more info](/files/key-values)
</Tip>

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### Groups

Pinata offers Private IPFS Groups to organize your content. You can upload a file to a group by using the `group` method.

<Tip>
  [Check out the Groups doc for more info](/files/file-groups)
</Tip>

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

You can also set or update the expiration on an existing file using the update method or endpoint:

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### CID Version

The `cid_version` option selects the UnixFS import profile Pinata uses to hash your upload. It changes the CID your content gets — the bytes you upload and the file you download are identical either way. Every profile produces CIDs that standard IPFS tooling can reproduce and resolve.

|                         | `v1` (default)          | `v0` (legacy)            | `unixfs-v1-2025`                             |
| ----------------------- | ----------------------- | ------------------------ | -------------------------------------------- |
| CID format              | CIDv1, base32 (`bafy…`) | CIDv0, base58btc (`Qm…`) | CIDv1, base32 (`bafy…`)                      |
| Leaf blocks             | Raw                     | dag-pb (UnixFS protobuf) | Raw                                          |
| Chunk size              | 256 KiB                 | 256 KiB                  | 1 MiB                                        |
| Max links per file node | 174                     | 174                      | 1024                                         |
| Hash function           | sha2-256                | sha2-256                 | sha2-256                                     |
| `ipfs add` equivalent   | `--cid-version=1`       | `--cid-version=0`        | `ipfs config profile apply test-cid-v1-wide` |

Use `v1` unless you have a reason not to. Use `v0` for backward compatibility with systems that expect the classic `Qm…` format. Use `unixfs-v1-2025` — the profile proposed in [IPIP-499](https://github.com/ipfs/specs/pull/499) — when you want a large file to hash to the same CID in Pinata and in other IPFS implementations, with fewer blocks per file thanks to the larger 1 MiB chunks.

<Note>
  The `unixfs-v1-2025` chunk and link widening applies to file DAGs. Directory shard settings are unchanged, so folder uploads large enough to shard (roughly 2,000+ entries) may not match a `test-cid-v1-wide` kubo import.
</Note>

<CodeGroup>
  ```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("cid_version", "unixfs-v1-2025");

      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);
    }
  }
  ```

  ```typescript SDK {3} theme={null}
  const upload = await pinata.upload.public
    .file(file)
    .cidVersion("v0")
  ```
</CodeGroup>

<Tip>
  The SDK's `cidVersion()` types currently accept `v0` and `v1`. To request `unixfs-v1-2025`, use the API directly with the `cid_version` form field.
</Tip>

The same option is available everywhere an upload starts:

* **Resumable (TUS) uploads** — pass it as `cid_version` in the upload metadata, or as the `X-Upload-Option-Cid-Version` query parameter on the creation request. The profile is fixed when the upload is created, so changing it while resuming has no effect.
* **Signed upload URLs** — pass `cid_version` in the body of [`POST /files/sign`](/api-reference/endpoint/create-signed-upload-url), and every upload made with that URL uses it.

The profile a file was uploaded with is returned as `cid_version` on the file's metadata, so you can always tell how an existing CID was produced.

### 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:

<CodeGroup>
  ```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 });
    }
  }
  ```
</CodeGroup>

Then back on the client side code, you can upload using the signed URL instead of the regular upload endpoint.

<Note>
  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.
</Note>

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

<Tip>
  For more information on client side uploads check out our [Presigned URLs Guide](/files/presigned-urls)
</Tip>

### 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### JSON

Pinata makes it easy to upload JSON objects using the [json](/sdk/upload/public/json) method.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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.

<Note>
  Support for file streams will be coming in a later version of the SDK.
</Note>

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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.

<Warning>
  Folder uploads are currently only supported on Public IPFS
</Warning>

<CodeGroup>
  ```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<File[]>();

    const changeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
      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 (
      <>
        <label className="form-label"> Choose File</label>
        <input
          directory=""
          webkitdirectory=""
          type="file"
          onChange={changeHandler}
        />
        <button onClick={handleSubmission}>Submit</button>
      </>
    );
  }

  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();
  ```
</CodeGroup>

<br />

<Info>
  We also have other tools like the [Pinata IPFS CLI](/tools/cli/ipfs) which can be used to upload using [API Keys](/account-management/api-keys)!
</Info>

### 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

### 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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

## 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.<network>.file()` by checking the file size before uploading.

<Warning>
  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!
</Warning>

```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-management/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.

<CodeGroup>
  ```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);
    }
  }
  ```
</CodeGroup>

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:

<img />

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:

<ParamField type="string">
  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.
</ParamField>

## 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.

<img />

Start uploading by [signing up for a free account](https://app.pinata.cloud/register)!


# File Vectors (Beta)
Source: https://docs.pinata.cloud/files/vectors



<Warning>
  The file vectors feature is still in beta and is only available on Private IPFS. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

## 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



<Note>
  x402 is available on paid Pinata accounts. [Upgrade your account](https://app.pinata.cloud/billing) to access x402 monetization features.
</Note>

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

<Note>
  You must use your own dedicated Pinata gateway domain. Replace `your-gateway.mypinata.cloud` with your actual gateway domain throughout these examples.
</Note>

## 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

<CardGroup>
  <Card title="x402 Overview" icon="sparkle" href="/sdk/x402/index">
    Get started with x402 monetization
  </Card>

  <Card title="Payment Instructions" icon="code" href="/sdk/x402/payment-instructions/list">
    Create and manage payment instructions
  </Card>

  <Card title="CID Management" icon="link" href="/sdk/x402/cids/list">
    Attach CIDs to payment instructions
  </Card>

  <Card title="Types" icon="brackets-curly" href="/sdk/types/x402">
    TypeScript type definitions
  </Card>
</CardGroup>

## 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

<Note>
  The content creator provides their dedicated Pinata gateway URL. Replace `your-gateway.mypinata.cloud` in examples with the actual gateway domain provided.
</Note>

### 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)

<Note>
  Pinata uses Coinbase Facilitator to process x402 payments, which provides access to Coinbase's Discovery Bazaar network for broader content discovery.
</Note>

## 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

<Note>
  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.
</Note>

### 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;
};
```

<Note>
  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.
</Note>

## 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.

<Note>
  x402 is available on paid Pinata accounts. [Upgrade your account](https://app.pinata.cloud/billing) to access x402 monetization features.
</Note>

## 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}
```

<Note>
  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.
</Note>

## 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:

<CardGroup>
  <Card title="Upload" icon="upload" href="/api-reference/endpoint/x402/pin">
    Upload files with payment
  </Card>

  <Card title="Retrieve" icon="download" href="/api-reference/endpoint/x402/retrieve">
    Access private files with payment
  </Card>
</CardGroup>

## 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.

<Note>
  If you are just getting started we recommend using Admin privileges, then move
  to scope keys as you better understand your needs
</Note>

<img />

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!

<Note>
  The API keys are only shown once, be sure to copy them somewhere safe!
</Note>

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.

<img />

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}
<script lang="ts">
let url: string;
let isUploading: boolean;

async function submit(e: SubmitEvent) {
	isUploading = true;
	e.preventDefault();
	const formData = new FormData(e.currentTarget as HTMLFormElement);
	const request = await fetch("/api/upload", {
		method: "POST",
		body: formData,
	});
	const response = await request.json();
	url = response.data;
	isUploading = false;
}
</script>

<style>
  img {
    max-width: 500px;
  }
  form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 2rem;
  }
  button {
   background-color: #6E57FF;
   color: white;
   border: none;
   border-radius: 5px;
   padding: 0.75rem 1.5rem;
   font-weight: bold;
  }
</style>

<form on:submit={submit}>
  <input type="file" id="file" name="file" required />
  <button>{isUploading ? "Uploading..." : "Upload"}</button>
  {#if url}
    <img src={url} alt="pinnie" />
  {/if}
</form>
```

This component creates a `<form>` with a file `<input>` as well as a `<button>` to send it. The form will trigger the `submit` function, which takes our form data and makes an API request to our server side code. Once complete the API will send response with a `url` that we can use in the `<img>` tag to display it on the page.

## Server Side API Route

In the `src/pages` folder make a new folder called `api` and inside there make a file called `upload.ts`. Paste the following code inside it:

```typescript theme={null}
import type { APIRoute } from "astro";
import { pinata } from "../../utils/pinata";

export const POST: APIRoute = async ({ request }) => {
	const data = await request.formData();
	const file = data.get("file") as File;
	if (!file) {
		return new Response(
			JSON.stringify({
				message: "Missing file",
			}),
			{ status: 400 },
		);
	}
	const { cid } = await pinata.upload.public.file(file);
	const url = await pinata.gateways.public.convert(cid)
	return new Response(
		JSON.stringify({
			data: url,
		}),
		{ status: 200 },
	);
};
```

This simple API route will parse the incoming `formData` and get the `file` we attached. If there isn't a file attached we'll send a error response. Otherwise we'll upload the file using the SDK method `pinata.upload.public.file` and deconstruct the response to get the `cid`. With that `cid` we can use the `convert` method to create a URL, which we can then send back to the client.

## Add Component to the Page

The last step here is to update the `src/pages/index.astro` file with our new component!

```html src/pages/index.astro theme={null}
---
import UploadForm from "../components/UploadForm.svelte";
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>Astro</title>
	</head>
	<body>
		<h1>Astro + Pinata</h1>
		<UploadForm client:load />
	</body>
</html>
```


# Hono
Source: https://docs.pinata.cloud/frameworks/hono



[Hono](https://hono.dev) is a lightweight web framework for Deno and Node.js. It is designed to be fast, simple, and easy to use. In this framework guide we'll show you how to create a server that sends [Presigned URLs](/files/presigned-urls) to a client app for a secure client side upload flow. We would highly recommend pairing this guide with the [React framework guide](/frameworks/react)!

## Installation

### 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.

<img />

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!

<Note>
  The API keys are only shown once, so be sure to copy them somewhere safe!
</Note>

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.

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

### Start up Hono Project

Run the command below to make a new Hono project:

```bash theme={null}
npm create hono@latest pinata-server
```

After doing so you should see a list of different deployment templates. We would recommend using `Cloudflare Workers` which is what we'll follow through with in this guide. After selecting that `cd` into the project and install the Pinata SDK.

```bash theme={null}
cd pinata-server
npm i pinata
```

After making the project, create a `.dev.vars` file in the root of the project and put in the following variables:

```
PINATA_JWT=
GATEWAY_URL
```

Use the `JWT` from the API key creation in the previous step as well as the `Gateway Domain`. The format of the Gateway domain should be `mydomain.mypinata.cloud`.

### Implement the Server Code

Inside the `src/index.ts` file paste in the following code:

```typescript src/index.ts theme={null}
import { Hono } from 'hono'
import { cors } from 'hono/cors';
import { PinataSDK } from 'pinata'

interface Bindings {
  PINATA_JWT: string;
  GATEWAY_URL: string;
}

const app = new Hono<{ Bindings: Bindings }>()

app.use(cors())

app.get('/', (c) => {
  return c.text('Hello Hono!')
})

app.get('/presigned_url', async (c) => {

	// Handle Auth

  const pinata = new PinataSDK({
    pinataJwt: c.env.PINATA_JWT,
    pinataGateway: c.env.GATEWAY_URL
  })

  const url = await pinata.upload.public.createSignedURL({
    expires: 60 // Last for 60 seconds
  })

  return c.json({ url }, { status: 200 })
})

export default app
```

This creates just two simple endpoints:

* `/` - A home route that just returns `Hello Hono!`
* `/presigned_url` - A route that will return a Presigned URL that is only valid for 60 seconds which our client can use to upload

<Tip>
  As with most server frameworks, you will likely want to protect your API route with an auth layer. You would place there where the comment says `// Handle Auth`
</Tip>

### Usage

Test it out by running the following command in your terminal

```bash theme={null}
npm run dev
```

You should see the console say it's serving at `http://localhost:8787`. While this is running, in a separate terminal window run this curl to test the API endpoint:

```bash theme={null}
curl http://localhost:8787/presigned_url
```

If successful you shoud have a JSON object like the following

```JSON theme={null}
{"url":"https://uploads.pinata.cloud/v3/files/redacted?X-Algorithm=PINATA2&X-Date=1743695529&X-Expires=60&X-Method=%5B%22HEAD%22%2C%22PATCH%22%2C%22POST%22%5D&X-Upload-Option-Network=public&X-User-ID=redacted&X-Signature=redacted"}
```

<Card href="/frameworks/react" icon="react" title="Continue to React Guide">
  To see how you can use Presigned URLs with a client side framework check out the React guide
</Card>


# Next.js
Source: https://docs.pinata.cloud/frameworks/next-js

Get started using Pinata with Next.js

This guide will walk you through setting up Pinata in a Next.js app.

## 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.

<Note>
  If you are just getting started we recommend using Admin privileges, then move
  to scope keys as you better understand your needs
</Note>

<img />

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!

<Note>
  The API keys are only shown once, be sure to copy them somewhere safe!
</Note>

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.

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

## Server-Side Setup

<Warning>
  Next.js has a limit of how large a file can be passed through the built in API routes, if you need to enable larger uploads follow the client side setup guide
</Warning>

### Start up Next.js Project

As with any Next.js project we can start one up with the following command

```bash theme={null}
npx create-next-app@latest
```

After the project is created `cd` into the repo and install `pinata`

```bash theme={null}
npm i pinata
```

After making the project, create a `.env.local` file in the root of the project and put in the following variables:

```
PINATA_JWT=
NEXT_PUBLIC_GATEWAY_URL=
```

Use the `JWT` from the API key creation in the previous step as well as the `Gateway Domain`. The format of the Gateway domain should be `mydomain.mypinata.cloud`.

### Setup Pinata

Create a directory called `utils` in the root of the project and then make a file called `config.ts` inside of it. In that file we'll export an instance of the Files SDK that we can use throughout the rest of the app.

```typescript utils/config.ts theme={null}
"server only"

import { PinataSDK } from "pinata"

export const pinata = new PinataSDK({
  pinataJwt: `${process.env.PINATA_JWT}`,
  pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
})
```

### Create Client Side Form

Next we'll want to make an upload form on the client side that will allow someone to select a file and upload it.

In the `/app/page.tsx` file take out the boiler plate code and use the following.

```typescript app/page.tsx theme={null}
"use client";

import { useState } from "react";

export default function Home() {
  const [file, setFile] = useState<File>();
  const [url, setUrl] = useState("");
  const [uploading, setUploading] = useState(false);

  const uploadFile = async () => {
    try {
      if (!file) {
        alert("No file selected");
        return;
      }

      setUploading(true);
      const data = new FormData();
      data.set("file", file);
      const uploadRequest = await fetch("/api/files", {
        method: "POST",
        body: data,
      });
      const signedUrl = await uploadRequest.json();
      setUrl(signedUrl);
      setUploading(false);
    } catch (e) {
      console.log(e);
      setUploading(false);
      alert("Trouble uploading file");
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFile(e.target?.files?.[0]);
  };

  return (
    <main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
      <input type="file" onChange={handleChange} />
      <button type="button" disabled={uploading} onClick={uploadFile} >
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </main>
  );
}
```

This will take a file from the client side and upload it through an API route we are going to make next.

<Note>
  Next.js does have a file size limitation for what can be passed through the
  API routes, so if you need more than the limit then it is advised to make
  signed JWTs by following [this
  guide](https://www.pinata.cloud/blog/how-to-upload-to-ipfs-from-the-frontend-with-signed-jwts).
</Note>

### Create API Route

Next.js is ideal for file uploads as it's API routes keep keys hidden and unexposed to the client. In the last step we made a function that uploads to `/api/files` so now we need to create that route by making `/app/api/files/route.ts` in our app.

Once you have created that file you can paste in the following code.

```typescript app/api/files/route.ts theme={null}
import { NextResponse, type NextRequest } from "next/server";
import { pinata } from "@/utils/config"

export async function POST(request: NextRequest) {
  try {
    const data = await request.formData();
    const file: File | null = data.get("file") as unknown as File;
    const { cid } = await pinata.upload.public.file(file)
    const url = await pinata.gateways.public.convert(cid);
    return NextResponse.json(url, { status: 200 });
  } catch (e) {
    console.log(e);
    return NextResponse.json(
      { error: "Internal Server Error" },
      { status: 500 }
    );
  }
}
```

This will accept a `POST` request from the client, then send an API request to Pinata with the upload, then make one more request to get a signed URL we can use to see the content. Once complete it will return the URL to the client.

With our URL we can render the image we uploaded by adding the following code to the `page.tsx` file.

```typescript app/page.tsx theme={null}
    return (
		<main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
			<input type="file" onChange={handleChange} />
			<button type="button" disabled={uploading} onClick={uploadFile}>
				{uploading ? "Uploading..." : "Upload"}
			</button>
			{/* Add a conditional looking for the signed url and use it as the source */}
			{url && <img src={url} alt="Image from Pinata" />}
		</main>
	);
```

And just like that we have uploaded an image to Pinata and recieved a usable URL in return!

## Client-Side Setup

Next.js has a file size limit as to what can be pass through API routes, so another workaround is to upload the file on the client side. To do this securely you can make an API route that generates a temporary upload URL that is used in the upload request.

### Start up Next.js Project

As with any Next.js project we can start one up with the following command

```bash theme={null}
npx create-next-app@latest
```

After the project is created `cd` into the repo and install `pinata`

```bash theme={null}
npm i pinata
```

After making the project, create a `.env.local` file in the root of the project and put in the following variables:

```
PINATA_JWT=
NEXT_PUBLIC_GATEWAY_URL=
```

Use the `JWT` from the API key creation in the previous step as well as the `Gateway Domain`. The format of the Gateway domain should be `mydomain.mypinata.cloud`.

### Setup Pinata

Create a directory called `utils` in the root of the project and then make a file called `config.ts` inside of it. In that file we'll export an instance of the Files SDK that we can use throughout the rest of the app.

```typescript utils/config.ts theme={null}
"server only"

import { PinataSDK } from "pinata"

export const pinata = new PinataSDK({
  pinataJwt: `${process.env.PINATA_JWT}`,
  pinataGateway: `${process.env.NEXT_PUBLIC_GATEWAY_URL}`
})
```

### Create API Route

In order to upload on the client side we need to upload it securely without leaking our admin API key. To avoid this we'll make an API route in our Next project under `app/api/url/route.ts`.

Once you have created that file you can paste in the following code.

```typescript app/api/url/route.ts theme={null}
import { NextResponse } from "next/server";
import { pinata } from "@/utils/config"

export const dynamic = "force-dynamic";

export async function GET() {
  // If you're going to use auth you'll want to verify here
  try {
    const url = await pinata.upload.public.createSignedURL({
      expires: 30, // The only required param
    })
    return NextResponse.json({ url: url }, { status: 200 }); // Returns the signed upload URL
  } catch (error) {
    console.log(error);
    return NextResponse.json({ text: "Error creating API Key:" }, { status: 500 });
  }
}
```

When the client makes a `GET` request to `/api/url` it will return a temporary signed upload URL that is only valid for 30 seconds, which we can use on the client to make the upload request.

### Create Client Side Form

Next we'll want to make an upload form on the client side that will allow someone to select a file and upload it with the signed upload URL

In the `/app/page.tsx` file take out the boiler plate code and use the following.

```typescript app/page.tsx theme={null}
"use client";

import { useState } from "react";
import { pinata } from "@/utils/config";

export default function Home() {
  const [file, setFile] = useState<File>();
  const [uploading, setUploading] = useState(false);

  const uploadFile = async () => {
    if (!file) {
      alert("No file selected");
      return;
    }

    try {
      setUploading(true);
      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
      console.log(upload);
      setUploading(false);
    } catch (e) {
      console.log(e);
      setUploading(false);
      alert("Trouble uploading file");
    }
  };

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFile(e.target?.files?.[0]);
  };

  return (
    <main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
      <input type="file" onChange={handleChange} />
      <button type="button" disabled={uploading} onClick={uploadFile}>
        {uploading ? "Uploading..." : "Upload"}
      </button>
    </main>
  );
}
```

The main thing to understand here is that we are able to use the `url()` method in combination with our upload methods which passes in the signed upload url instead of trying to access the admin key. We can take the response and create a URL to access the file.

```typescript app/page.tsx theme={null}
"use client";

import { useState } from "react";
import { pinata } from "@/utils/config";

export default function Home() {
	const [file, setFile] = useState<File>();
	const [url, setUrl] = useState("");
	const [uploading, setUploading] = useState(false);

	const uploadFile = async () => {
		if (!file) {
			alert("No file selected");
			return;
		}

		try {
			setUploading(true);
			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
      const fileUrl = await pinata.gateways.public.convert(upload.cid)
			setUrl(fileUrl);
			setUploading(false);
		} catch (e) {
			console.log(e);
			setUploading(false);
			alert("Trouble uploading file");
		}
	};

	const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		setFile(e.target?.files?.[0]);
	};

	return (
		<main className="w-full min-h-screen m-auto flex flex-col justify-center items-center">
			<input type="file" onChange={handleChange} />
			<button type="button" disabled={uploading} onClick={uploadFile}>
				{uploading ? "Uploading..." : "Upload"}
			</button>
			{url && <img src={url} alt="Image from Pinata" />}
		</main>
	);
}
```


# Node.js
Source: https://docs.pinata.cloud/frameworks/node-js



This Node.js quickstart should get you up and running with your standard backend javascript setup, and by using the API will give you flexibility when it comes to [uploading files](/files/uploading-files).

## Generate your API Keys

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.

<Note>
  If you are just getting started, we recommend using Admin privileges, then move to scope keys as you better understand your needs
</Note>

<img />

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!

<Note>
  The API keys are only shown once, be sure to copy them somewhere safe!
</Note>

## Setup a Node.js Project

To start you can go to your terminal and put in the following code

```bash theme={null}
mkdir pinata-starter && cd pinata-starter && npm init -y
```

Then install the [Files SDK](/sdk) as well as `dotenv`

```bash theme={null}
npm i pinata dotenv
```

In the root of the directory make a `.env` file where we're pull the following variables.

```text theme={null}
PINATA_JWT=
GATEWAY_URL=
```

Use the `JWT` from the API key creation in the previous step as well as the `Gateway Domain`. The format of the Gateway domain should be `mydomain.mypinata.cloud`.

Now we'll make two files, `index.js` and `hello-world.txt`.

```bash theme={null}
touch index.js && touch hello-world.txt
```

Inside the `hello-world.txt` you can put whatever you'd like such as `Hello World!`.

## Upload a File to Pinata

Once we have our initial project setup we can put the following code into our `index.js` file.

<CodeGroup>
  ```javascript index.js (Node v.20) theme={null}
  const { PinataSDK } = require("pinata")
  const fs = require("fs")
  const { Blob } = require("buffer")
  require("dotenv").config()

  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT,
    pinataGateway: process.env.GATEWAY_URL
  })

  async function upload(){
    try {
      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);
      console.log(upload)
    } catch (error) {
      console.log(error)
    }
  }

  upload()
  ```

  ```javascript index.js (Node v.18) theme={null}
  const { PinataSDK } = require("pinata")
  const fs = require("fs")
  const { Blob } = require("buffer")
  require("dotenv").config()

  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT,
    pinataGateway: process.env.GATEWAY_URL
  })

  async function upload(){
    try {
      const blob: any = new Blob([fs.readFileSync("./hello-world.txt")]);
      const upload = await pinata.upload.public.file(blob);
      console.log(upload)
    } catch (error) {
      console.log(error)
    }
  }

  upload()
  ```
</CodeGroup>

You can run this by using `node index.js` in the terminal. After uploading a file you should get a response that looks like this

```javascript 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
}
```

## Fetch the File through a Gateway

With the `cid` you can view the file or fetch the data using your Gateway. Lets make a new file called `fetch.js` and put in the following code.

```javascript fetch.js theme={null}
const { PinataSDK } = require("pinata")
require("dotenv").config()

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT,
  pinataGateway: process.env.GATEWAY_URL
})

async function main() {
  try {
    const file = await pinata.gateways.public.get("bafkreiac3t35fklpiwqonav2vj4x2dh6x2zugkdu7dsh6zkaq5jr33lcwy")
    console.log(file.data)
  } catch (error) {
    console.log(error);
  }
}

main()
```


# React
Source: https://docs.pinata.cloud/frameworks/react



Since React is a client side framework it is recommend to handle uploads using [Presigned URLs](/files/presigned-urls) using a server side framework like [Hono](/frameworks/hono). We would highly recommend following the Hono guide below before doing this React guide!

<Card href="/frameworks/hono" icon="fire" title="Hono Guide" />

## Installation

### Get Gateway URL

Before we start you'll need your Dedicated 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.

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

### Start up React Project

Run the command below to make a new React project:

```bash theme={null}
npm create vite@latest
```

Give the project a name and select the React framework. Then `cd` into the project and install `pinata`.

```bash theme={null}
npm i pinata
```

After making the project, create a `.env.local` file in the root of the project and put in the following variable:

```
VITE_SERVER_URL=
VITE_GATEWAY_URL=
```

The `VITE_SERVER_URL` is the URL of your server endpoint that will return Presigned URLs. If you don't have one be sure to follow the [Hono guide](/frameworks/hono) first. If you did follow it then you'll remember it was `http://localhost:8787` and we'll need to make sure it's running in order for our flow to work.

The format of the Gateway domain should be `mydomain.mypinata.cloud`.

## Implementation

Now that we have our initial setup, open the `src/App.tsx` file and replace it with the following code.

```typescript src/App.tsx theme={null}
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { PinataSDK } from 'pinata'

const pinata = new PinataSDK({
  pinataJwt: "",
  pinataGateway: import.meta.env.VITE_GATEWAY_URL
})

function App() {
  const [file, setFile] = useState<File | null>(null)
  const [uploadStatus, setUploadStatus] = useState('')
  const [link, setLink] = useState('')

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      setFile(e.target.files[0])
    }
  }

  const handleUpload = async () => {
    if (!file) return

    try {
      setUploadStatus('Getting upload URL...')
      const urlResponse = await fetch(`${import.meta.env.VITE_SERVER_URL}/presigned_url`, {
        method: "GET",
        headers: {
          // Handle your own server authorization here
        }
      })
      const data = await urlResponse.json()

      setUploadStatus('Uploading file...')

      const upload = await pinata.upload.public
        .file(file)
        .url(data.url)

      if (upload.cid) {
        setUploadStatus('File uploaded successfully!')
        const ipfsLink = await pinata.gateways.public.convert(upload.cid)
        setLink(ipfsLink)
      } else {
        setUploadStatus('Upload failed')
      }
    } catch (error) {
      setUploadStatus(`Error: ${error instanceof Error ? error.message : String(error)}`)
    }
  }

  return (
    <>
      <div>
        <a href="<https://vite.dev>" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="<https://react.dev>" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React + Pinata</h1>
      <div className="card">
        <input type="file" onChange={handleFileChange} />
        <button onClick={handleUpload} disabled={!file}>
          Upload to Pinata
        </button>
        {uploadStatus && <p>{uploadStatus}</p>}
        {link && <a href={link} target='_blank'>View File</a>}
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App
```

In our code we have a pretty simple form that will trigger `handleSubmit()`. This function will do the following:

* Make a request to our server for a Presigned URL (you will want to handle any server side auth here)
* Use the returned Presigned URL in a client side upload
* Use the upload response to create a link to the file

To try it out make sure your server is running, then in another terminal Run

```bash theme={null}
npm run dev
```

Open up the site on `http://localhost:5173` and upload a file!


# Remix
Source: https://docs.pinata.cloud/frameworks/remix

Get started using Pinata with Remix

This guide will walk you through setting up Pinata in a Remix app.

## 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.

<Note>
  If you are just getting started we recommend using Admin privileges, then move
  to scope keys as you better understand your needs
</Note>

<img />

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!

<Note>
  The API keys are only shown once, be sure to copy them somewhere safe!
</Note>

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.

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

## Startup Remix Project

We can start a Remix app by entering the command below into the terminal

```bash theme={null}
npx create-remix@latest pinata-remix
```

After it's finished installed we'll want to `cd` into the project and install the `pinata` SDK.

```bash theme={null}
cd pinata-remix && npm i pinata
```

Now let's make a file called `.env` file in the root of the project with the following variables:

```
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
```

## Setup Pinata

Make a folder in the root of the project called `utils`, and inside make a file called `pinata.ts` with the following code:

```typescript theme={null}
import { PinataSDK } from "pinata";

export const pinata = new PinataSDK({
	pinataJwt: process.env.PINATA_JWT,
	pinataGateway: process.env.GATEWAY_URL,
});
```

This will create an instance of the SDK we can use throughout the app.

## Upload Form and Server Action

In the `app/routes` directory we can remove the boilerplate code and replace it with the code below.

```typescript app/routes/_index.tsx theme={null}
import type { ActionFunctionArgs, MetaFunction } from "@remix-run/node";
import { json } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import { pinata } from "utils/config";

export const meta: MetaFunction = () => {
	return [
		{ title: "Remix + Pinata" },
		{ name: "description", content: "Upload files on Remix with Pinata!" },
	];
};

// Server side action to handle our upload
export const action = async ({ request }: ActionFunctionArgs) => {
	const formData = await request.formData();
	const file = formData.get("file") as File;
	const { cid } = await pinata.upload.public.file(file);
	const url = await pinata.gateways.public.convert(cid);
	return json({ url });
};

export default function Index() {
	const actionData = useActionData<typeof action>();
	const navigation = useNavigation();
	const isSubmitting = navigation.state === "submitting";

	return (
		<div className="font-sans p-4 flex flex-col gap-4 justify-center items-center min-h-screen max-w-[500px] mx-auto">
			<h1 className="text-3xl font-bold">Remix + Pinata</h1>
			<Form
				encType="multipart/form-data"
				method="post"
				className="flex flex-col gap-4"
			>
				<input type="file" name="file" className="" />
				<button
					className="bg-[#582CD6] text-white rounded-md p-2"
					type="submit"
				>
					{isSubmitting ? "Uploading..." : "Upload"}
				</button>
			</Form>
			{actionData?.url && (
				<div className="mt-4">
					<a
						href={actionData.url}
						target="_blank"
						rel="noreferrer"
						className="text-[#582CD6] underline"
					>
						{actionData.url}
					</a>
				</div>
			)}
		</div>
	);
}
```

Let's walk through what's happening here. To start we have our client side markup:

```typescript theme={null}
<div className="font-sans p-4 flex flex-col gap-4 justify-center items-center min-h-screen max-w-[500px] mx-auto">
			<h1 className="text-3xl font-bold">Remix + Pinata</h1>
			<Form
				encType="multipart/form-data"
				method="post"
				className="flex flex-col gap-4"
			>
				<input type="file" name="file" className="" />
				<button
					className="bg-[#582CD6] text-white rounded-md p-2"
					type="submit"
				>
					{isSubmitting ? "Uploading..." : "Upload"}
				</button>
			</Form>
			{actionData?.url && (
				<div className="mt-4">
					<a
						href={actionData.url}
						target="_blank"
						rel="noreferrer"
						className="text-[#582CD6] underline"
					>
						{actionData.url}
					</a>
				</div>
			)}
		</div>
```

This is a simple `Form` element provided by Remix which lets us make a submission to a server action. The most important piece to notice here is the `encType="multipart/form-data"`, which is crucial to make file / multipart requests work in Remix!

Then we have our server action:

```typescript theme={null}
export const action = async ({ request }: ActionFunctionArgs) => {
	const formData = await request.formData();
	const file = formData.get("file") as File;
	const { cid } = await pinata.upload.public.file(file);
	const url = await pinata.gateways.public.convert(cid);
	return json({ url });
};
```

This will be the default endpoint used when we submit our form. It will parse the incoming `formData`, get the `file` attached, and then we upload it to Pinata using `pinata.upload.public.file()`. From that method we deconstruct the result and return the `cid` which we can use in `convert`. This will make a URL we can return to the client and make accessible to the user, and in our case the `<a>` tag that links to the content.


# SvelteKit
Source: https://docs.pinata.cloud/frameworks/sveltekit



This guide will walk you through setting up Pinata in a SvelteKit app.

## 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.

<Note>
  If you are just getting started we recommend using Admin privileges, then move
  to scope keys as you better understand your needs
</Note>

<img />

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!

<Note>
  The API keys are only shown once, be sure to copy them somewhere safe!
</Note>

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.

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

## Server-side Setup

This guide will walk you through a server side upload flow in SvelteKit

### Start up SvelteKit Project

The easiest way to start building a SvelteKit app is to run `npm create`

```bash theme={null}
npm create svelte@latest pinata-app
- Which Svelte app template? Skeleton project
- Add type checking with TypeScript? Yes, using TypeScript syntax
- Select additional options (use arrow keys/space bar) <- These are optional

```

After the project is created `cd` into the repo and install `pinata`

```bash theme={null}
npm i pinata
```

In this demo we will be using `tailwindcss` with typography plugin. Follow other CSS Framework guides respectively.

```bash theme={null}
npx svelte-add@latest tailwindcss
```

After making the project, create a `.env.local` file in the root of the project and put in the following variables:

```
PINATA_JWT=
PUBLIC_GATEWAY_URL=
```

Use the `JWT` from the API key creation in the previous step as well as the `Gateway Domain`. The format of the Gateway domain should be `mydomain.mypinata.cloud`.

### Setup Pinata

Create a directory called `server` in the `src/lib` folder of the project and then make a file called `pinata.ts` inside of it. In that file we'll export an instance of the Files SDK that we can use throughout the rest of the app.

<Note>
  The use of the `server` directory prevents it being used client side.
</Note>

```typescript src/lib/server/pinata.ts theme={null}
import { PinataSDK } from "pinata"
import { PINATA_JWT } from "$env/static/private";
import { PUBLIC_GATEWAY_URL } from "$env/static/public";

export const pinata = new PinataSDK({
  pinataJwt: `${PINATA_JWT}`,
  pinataGateway: `${PUBLIC_GATEWAY_URL}`
})
```

### Create Client Side Form

Next we'll want to add a form on our home page that will allow someone to select a file and upload it.
In the `src/routes/+page.svelte` file take out the boiler plate code and use the following.
Lets also restrict the allowed extensions for the file. (e.g. jpg, jpeg, png, webp).

```typescript src/routes/+page.svelte theme={null}
<script lang="ts">
  import { enhance } from '$app/forms';

  let uploading = false;

  function handleUpload() {
    uploading = true;
      return async ({ update }) => {
        await update();
        uploading = false;
      };
  }
</script>

<main class="w-full min-h-screen m-auto flex flex-col justify-center items-center">
  <form method="POST" enctype="multipart/form-data" use:enhance={handleUpload}>
    <input type="file" id="file" name="fileToUpload" accept=".jpg, .jpeg, .png, .webp" />
    <button disabled={uploading} type="submit">
      {uploading ? 'Uploading...' : 'Upload'}
    </button>
  </form>
</main>

```

This will take a file from the client side and upload it through a form Action we are going to make next.

### Create Server Action

Create a server file, `src/routes/+page.server.ts`, so that we can add a form action to use.
Since this demo and page only has 1 form, we will keep the `default` action.

```typescript src/routes/+page.server.ts theme={null}
import { fail, json, type Actions } from "@sveltejs/kit";
import { pinata } from "$lib/server/pinata";

export const actions: Actions = {
  default: async ({ request }) => {
    try {
      const formData = await request.formData();
      const uploadedFile = (formData?.get('fileToUpload') as File);

      if (!uploadedFile.name || uploadedFile.size === 0) {
        return fail(400, {
          error: true,
          message: "You must provide a file to upload"
        })
      }

      const upload = await pinata.upload.public.file(uploadedFile);
      const url = await pinata.gateways.public.convert(upload.cid);
      return { url, filename: uploadedFile.name, status: 200 };
    } catch (error) {
      console.log(error);
      return json(
        { error: "Internal Server Error" },
        { status: 500 }
      );
    }
  }
}
```

The form action will send an API request to Pinata with the upload, then use the resulting CID to create a URL we can use to see the content.
Once complete, we can define the return data sent to the client.

### Access the Returned value of the Form Action

We can access the returned data by exporting the `form` prop on our `+page.svelte`. Now, after submitting the form,
the `form` prop will be updated and we can conditionally render the image we just uploaded.

```typescript src/routes/+page.svelte theme={null}
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';

  export let form: ActionData;
  let uploading = false;

  function handleUpload() {
    uploading = true;
      return async ({ update }) => {
        await update();
        uploading = false;
      };
  }
</script>

<main class="w-full min-h-screen m-auto flex flex-col justify-center items-center">
  <form method="POST" enctype="multipart/form-data" use:enhance={handleUpload}>
    <input type="file" id="file" name="fileToUpload" accept=".jpg, .jpeg, .png, .webp" />
    <button disabled={uploading} type="submit">
      {uploading ? 'Uploading...' : 'Upload'}
    </button>
  </form>
  {#if form && form.status === 200}
    <img src={form.url} alt={form.filename} />
  {/if}
</main>

```

And just like that we have uploaded an image to Pinata and recieved a usable URL in return!
Remember that the url for the image is ephemeral, once you refresh the page the url and image will be gone.


# Dedicated IPFS Gateways
Source: https://docs.pinata.cloud/gateways/dedicated-ipfs-gateways



Dedicated Gateways are the fastest way to fetch content from IPFS, and are the ideal tool when building decentralized applications. When you create a Pinata account, you'll automatically have a Dedicated Gateway created for you! To see it, simply visit the [Gateways Page](https://app.pinata.cloud/gateway).

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

## Viewing Content Through Your Gateway

To view content through your gateway, grab the CID of the file you'd like to view and add it to your gateway URL, like so:

<img />

Simple as that!

You can also fetch the data programatically using the [get](/sdk/gateways/public/get) method in the [SDK](/sdk).

```typescript theme={null}
import { PinataSDK } from "pinata-web3";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const file = await pinata.gateways.public.get("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
```

## Restricted vs Open

All Pinata Dedicated Gateways are restricted by default. This mean they will only serve CIDs that are pinned to your account, which keeps your gateway safe from abuse by outside actors who may want to use it for themselves.

If you plan to upload content to your own account then use your gateway to fetch it, you shouldn't have to touch a thing and it will work as expected! However, lets say you're building a marketplace and you need to fetch content outside your account. In that case, you will want to use [Gateway Access Controls](/gateways/gateway-access-controls). Adding one of these will allow you to access CIDs from the public IPFS network, but it has to meet that access control condition, like a Gateway API Key or Host Origin requirement. Be sure to read our [docs on Gateway Access Controls](/gateways/gateway-access-controls) to learn more.

<img />

<Note>
  The only way to open a Dedicated Gateway and allow any CID to go through is to
  add a Gateway Access Control
</Note>

## Convert IPFS Links or Gateway URLs to use Your Dedicated Gateway

If you're a developer building an app to index the blockchain, it's likely you'll encounter several IPFS URL formats that might not be ideal to work with:

```
ipfs://QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng

https://ipfs.io/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng
```

The Pinata SDK is designed to assist you in scenarios like this. It can take IPFS urls, whether they are the protocol standard (`ipfs://`) or another gateway (`https://ipfs.io`), and turn them into your specified Dedicated Gateway.

```typescript theme={null}
import { PinataSDK } from "pinata-web3";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const url = await pinata.gateways.public
  .convert("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")

// Optionally add a different gateway prefix
// const url = await pinata.gateways.public
//   .convert("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng", "https://ipfs.io")
```

## Adding a Custom Domain

Pinata also allows you to create a custom domain for your Dedicated Gateway. Simply visit the [Gateways Page](https://app.pinata.cloud/gateway), click the menu button on the right side of your gateway, then click Add Custom Domain. You'll need to own the domain you want to use. When you enter your domain, you will be prompted to enter DNS information through your registrar.

## Billing and Usage

When it comes to using a Dedicated Gateway, there are a few metrics Pinata uses for billing.

| Metric    | Description                                                                                                                                                                                                                                                                                                                                                                              |
| :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Bandwidth | Bandwidth is the amount of data that is being going through your Gateway. For instance, if you have a 10MB file go through your gateway 100 times, that would be 1GB of bandwidth used for the month.                                                                                                                                                                                    |
| Requests  | A request is anytime content is queried through the gateway, so if you run `wget https://mygateway.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng` in your terminal, that would be one request. Or if you had someone visit your app that uses the gateway on the client side, each time the website requests content from IPFS using the gateway, that's a request. |

Both of these metrics reset on a monthly basis based on your billing cycle.

<Note>
  If you're using a Dedicated Gateway for your NFT project, be sure to check out
  [this
  guide](https://knowledge.pinata.cloud/en/articles/6461213-token-uris-in-nft-projects)
  on how you should do that.
</Note>

If you have any questions in regards to billing, please don't hesitate to reach out via our chat in the bottom right of our app or [email us](mailto:team@pinata.cloud)!


# Gateway Access Controls
Source: https://docs.pinata.cloud/gateways/gateway-access-controls



Pinata's Dedicated Gateways make it possible to fetch and serve IPFS content quickly and reliably, however there can be security risks in exposing an open gateway to the world. That's why Pinata has created Gateway Access Controls. These controls will allow you to further limit your gateway, making sure only your platform is using it. This is accomplished with **Gateway Keys, IP Address Restrictions,** and **Host Origin Restrictions.**

By default, your Dedicated Gateway is restricted, it will only serve content pinned to your account. When you add an Access Control, your gateway becomes open and will be able to serve any content from IPFS, but only when requests meet your security requirements.

## Restricted vs Open

When working with IPFS Gateways the behavior usually falls into two categories: Restricted or Open.

### Restricted Gateway

Restricted means the gateway can only load content that is pinned to that user's account. This is the default behavior for Pinata's Dedicated Gateway, as it provides the maximum level of security. Any requests made to a CID outside the user's account will be unauthorized.

<img />

<Info>This check of whether or not a file is pinned or not using `/data/pinList` in the Pinata API, which means recursive CIDs in folders are considered unauthorized. Instead, use the folder CID in the path of the gateway followed by the file name.</Info>

### Open Gateway

<img />

Open means the gateway can access any content on IPFS, not just pinned content. An example might be a public gateway like `ipfs.io/ipfs/` which is open to everyone, but has heavy rate limiting. For Dedicated Gateways, you achieve this open state by adding Access Controls.

### Why Access Controls?

<img />

If someone were to open their Dedicated Gateway without any permissions, anyone who found the domain could use it for themselves and abuse it, leaving the owner with a big bill of overages.

<img />

Because of this Pinata developed Gateway Access Controls to allow users to open their Gateways, but with restrictions that must be met first.

## Access Controls

Pinata currently provides three primary methods for opening your gateway securely:

* [Gateway Keys](#gateway-keys)
* [IP Address](#ip-address)
* [Host Origin](#host-origin)

### Gateway Keys

Adding a Gateway Key restriction means that content served through your gateway will only be served successfully if the key is present with the request. **Importantly, content pinned to your account won't be accessible through your gateway if you've implemented a gateway key restriction and fail to include that token in content requests.**

To create a Gateway Key, click on the button that says "Request Key."

<img />

When you create a key you will have the ability to preview the token by clicking the "eye" icon, or copy the token to your clipboard with the "copy" icon. At any point, you can delete a gateway key by clicking the "trash" icon.

<img />

Once you have the key, there are two ways you can use it in the gateway request.

#### Query Parameter

To use the query parameter method, simply add this to the end of a gateway request url:

```
?pinataGatewayToken=PASTE_IN_ACCESS_TOKEN
```

#### Header

Another way to use the gateway key is in the request header. The Key Value would look like this:

| Key                    | Value        |
| ---------------------- | ------------ |
| x-pinata-gateway-token | GATEWAY\_KEY |

**Please keep in mind that using the gateway key in the request header may not work in a client side application, so consider using IP Address restriction instead for those use cases.**

### IP Address

You can also restrict your gateway by IP Address. You can add up to 100 different IP addresses (individually). When you add this restriction, only content requested from an IP address that you've added will be served through your gateway.

To start, click "Set IP Address" on the right side of the menu.

<img />

You will get window asking for a valid IP Address which will allow any requests being made from the IP Address to go through!

<img />

### Host Origin

With the Host Origin restriction, you can make sure your gateway can only be used on a specific domain (for example, '[https://app.pinata.cloud](https://app.pinata.cloud)'). To get started, click on "Add Host Origin."

<img />

After that, you can add the domain you would like your gateway to be used from!

<img />

<Warning>
  Make sure you include the `https://` protocol prefix in the URL and that there are no trailing slashes! To test localhost use the IP Access control with your public facing IP.
</Warning>

Keep in mind, if you are rendering content on the client side using Host Origins, you will need to include a `crossorigin` tag in your `img`, `video`, `audio`, `link`, or `script` elements. Here is an example with an img element in React:

<CodeGroup>
  ```javascript html theme={null}
  <img
    src="https://pinata-media.mypinata.cloud/ipfs/CID"
    crossorigin="anonymous"
    alt="pinnie"
  />
  ```

  ```javascript React theme={null}
  <img
    src="https://pinata-media.mypinata.cloud/ipfs/CID"
    crossOrigin="anonymous"
    alt="pinnie"
  />
  ```
</CodeGroup>

For more info on `crossorigin` please read this article [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/crossorigin)!

### Multiple Restrictions

You can add multiple Access Controls, and they will perform as an "OR" operator. This means that if you have Host Origins and a Gateway Key set, you can use either one for content to go through.


# Image Optimizations
Source: https://docs.pinata.cloud/gateways/image-optimizations



Pinata image optimizations provides image optimization functionality directly through your [Gateway](/gateways/retrieving-files). These capabilities that can significantly improve the load time and experience when viewing image content.

## Usage

To use image optimizations on files you've uploaded simply add on the queries listed in the [reference](#reference) to your full Gateway url, which might look something like this:

```
https://example-gateway.mypinata.cloud/files/{cid}?img-width=500&img-height=500
```

There are differences when using image optmizations for public files vs private files, so be sure to visit the [retrieving files](/gateways/retrieving-files) for more information!

## Reference

At least one option must be specified. Options are comma-separated (spaces are not allowed anywhere). Names of options can be specified in full or abbreviated.

<ParamField type="string">
  Specifies maximum width of the image in pixels. Exact behavior depends on the fit mode (described below).
</ParamField>

<ParamField type="string">
  Specifies maximum height of the image in pixels. Exact behavior depends on the fit mode (described below).
</ParamField>

<ParamField type="string">
  Device Pixel Ratio. Default 1. Multiplier for width/height that makes it easier to specify higher-DPI sizes in .
</ParamField>

<ParamField type="string">
  Affects interpretation of width and height. All resizing modes preserve aspect ratio. Available modes are:

  * `img-fit=scale-down` Image will be shrunk in size to fully fit within the given width or height, but won’t be enlarged.

  * `img-fit=contain` Image will be resized (shrunk or enlarged) to be as large as possible within the given width or height while preserving the aspect ratio.

  * `img-fit=cover` Image will be resized to exactly fill the entire area specified by width and height, and will cropped if necessary.

  * `img-fit=crop` Image will be shrunk and cropped to fit within the area specified by width and height. The image won’t be enlarged. For images smaller than the given dimensions it’s the same as scale-down. For images larger than the given dimensions, it’s the same as cover.

  * `img-fit=pad` Image will be resized (shrunk or enlarged) to be as large as possible within the given width or height while preserving the aspect ratio, and the extra area will be filled with a background color (white by default). Transparent background may be very expensive, and it’s better to use fit=contain and CSS object-fit: contain property instead.
</ParamField>

<ParamField type="string">
  When cropping with `fit=cover`, specifies the most important side or point in the image that shouldn’t be cropped off.

  * `img-gravity=auto` The point will be guessed by looking for areas that stand out the most from image background

  * `img-gravity=side` and `img-gravity=XxY`
    If a side (left, right, top, bottom) or coordinates specified on a scale from 0.0 (top or left) to 1.0 (bottom or right), 0.5 being the center. The X and Y coordinates are separated by lowercase x, e.g. 0x1 means left and bottom, 0.5x0.5 is the center, 0.5x0.33 is a point in the top third of the image.
</ParamField>

<ParamField type="string">
  Specifies quality for images in JPEG, WebP and AVIF formats. The quality is in 1-100 scale, but useful values are between 50 (low quality, small file size) and 90 (high quality, large file size). 85 is the default. When using the PNG format, an explicit quality setting allows use of PNG8 (palette) variant of the format.
</ParamField>

<ParamField type="string">
  Allows serving of the WebP format to browsers that support it. If this option is not specified, a standard format like JPEG or PNG will be used.
</ParamField>

<ParamField type="string">
  Reduces animations to still images. This setting is recommended to avoid surprisingly large animGIF files, or flashing images.
</ParamField>

<ParamField type="string">
  Specifies strength of sharpening filter. The value is a floating-point number between 0 (no sharpening) and 10 (max). 1 is a recommended value.
</ParamField>

<ParamField type="string">
  In case of a fatal error that prevents the image from being resized use `img-onerror=redirect` to redirect to the unresized source image URL. This may be useful in case some images require user authentication and cannot be fetched. This option shouldn’t be used if the source images may be very large. This option is ignored if the image is from another domain (subdomains are OK).

  * `img-onerror=redirect` Redirects to original source url
</ParamField>

<ParamField type="string">
  Controls amount of invisible metadata (EXIF data) that should be preserved. Color profiles and EXIF rotation are applied to the image even if the metadata is discarded. Note that if the Polish feature is enabled, all metadata may have been removed already and this option may have no effect.

  * `img-metadata=keep` Preserve most of the image metadata (including GPS location) when possible.

  * `img-metadata=copyright` Discard all metadata except EXIF copyright tag. This is the default for JPEG images. img-metadata=none Discard all invisible metadata.
</ParamField>

## Formats and limitations

Read JPEG, PNG, GIF (including animations), and WebP images. SVG is not supported, since this format is inherently scalable and does not need resizing. Resize and generate JPEG and PNG images, and optionally AVIF or WebP. AVIF format is supported on a best-effort basis. Images that cannot be compressed as AVIF will be served as WebP instead.

## SDK Method Reference

Image Optimizations are built into both [`get`](/sdk/gateways/public/get) and [`createSignedURL`](/sdk/gateways/private/create-signed-url) methods of the SDK.

```typescript theme={null}
type OptimizeImageOptions = {
  width?: number;
  height?: number;
  dpr?: number;
  fit?: "scaleDown" | "contain" | "cover" | "crop" | "pad";
  gravity?: "auto" | "side" | string;
  quality?: number;
  format?: "auto" | "webp";
  animation?: boolean;
  sharpen?: number;
  onError?: boolean;
  metadata?: "keep" | "copyright" | "none";
};
```

```typescript theme={null}
const data = await pinata.gateways.public
  .get("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
  .optimizeImage({
    width: 500,
    height: 500,
    format: "webp"
  })
```


# Content Addressable
Source: https://docs.pinata.cloud/gateways/plugins/content-addressable

Use the Content Addressable plugin to include CID signatures

The Content Addressable plugin works in tandem with the Signatures API, allowing users to get a signature field in the headers of a Dedicated Gateway request. This can be very useful to help verify content ownership when retrieving files from IPFS.

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures)
</Note>

## Installation

Follow the steps in the [Getting Started](/gateways/plugins/getting-started) guide to install the Content Addressable Plugin.

## Usage

Once the plugin is installed the rest is quite simple. If a CID as a signature attached to it, then there will be a `pinata-signature` field in the headers.

```typescript theme={null}
const signatureReq = await fetch(
  `https://<YOUR_GATEWAY_DOMAIN>.mypinata.cloud/ipfs/<CID>`,
  {
    method: "HEAD",
  }
);
const signature = signatureReq.headers.get("pinata-signature");

//0xbe92f9747e1712f2673ff0de6c7058968b869ce14ed6e0949fa3c485e420a27c66695401f56d3d3158fcbb6a5a7809e91acb9660dc00f31673d7efe6f77665b31c
```


# Getting Started
Source: https://docs.pinata.cloud/gateways/plugins/getting-started

Learn how to use Dedicated Gateway plugins to supercharge your IPFS delivery system

Dedicated Gateways are crucial for retrieving and delivering content on IPFS, but there are times you may need additonal functionality. Gateway Plugins serve that purpose, anything from hot swapping CIDs to delivering signatures and more!

## Installing Plugins

You can install plugins either through the Pinata App or the Pinata API.

### Pinata App

To install a plugin navigate to the Plugins Marketplace tab on the right side.

<Frame>
  <img />
</Frame>

Once there you can find the plugin you want to install and click "Install." This will bring up a drop down of your Dedicated Gateways to choose which the plugin is installed to.

<Frame>
  <img />
</Frame>

Once installed you can confirm its there by going to the "My Plugins" tab.

<Frame>
  <img />
</Frame>

### Pinata API

With the Pinata API you can list all available plugins using the [/ipfs/plugins\_marketplace](/api-reference/endpoint/ipfs/list-marketplace-plugins) endpoint.

<CodeGroup>
  ```typescript Get Plugins theme={null}
  const plugins = await fetch("https://api.pinata.cloud/v3/ipfs/plugins_marketplace", {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${PINATA_JWT}`
    }
  })
  ```

  ```json Response theme={null}
  {
    "data": [
      {
        "id": 1,
        "name": "Content Addressable Attestation",
        "description": "Verify the authenticity and ownership of files added to the IPFS network by signing them with your Ethereum wallet.",
        "image": "https://mktg.mypinata.cloud/ipfs/QmVm3jRZ7S2KMmYEvkmkwncRCmBSUEuzNKfm2AgJkfBGKZ",
        "tags": [
          "verifiability",
          "security"
        ]
      },
      {
        "id": 2,
        "name": "Hot Swaps",
        "description": "Make IPFS mutable by pointing your original CID to a new file.",
        "image": "https://mktg.mypinata.cloud/ipfs/QmVpy6v7YASWUQjmqgUxKwbn6zgzuVSRudzdZMynpBjSJo",
        "tags": [
          "files"
        ]
      }
    ]
  }
  ```
</CodeGroup>

To install one you can make a request to [/ipfs/gateway\_plugins/:gateway\_id](/api-reference/endpoint/ipfs/install-gateway-plugin) by using the [gateway\_id](/api-reference/endpoint/ipfs/list-gateways) as a path parameter and the `plugin_id` as body of the request.

<CodeGroup>
  ```typescript Install Plugin theme={null}
  const data = JSON.stringify({
    plugin_id: 1
  })

  const gatewayId = "8673cd80-bf53-4bca-b684-bec1d6bdf004"

  const installPlugin = await fetch(`https://api.pinata.cloud/v3/ipfs/gateway_plugins/${gatewayId}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${PINATA_JWT}`
    },
    body: data
  })
  ```

  ```json Response theme={null}
  {
    "data": {
      "gateway_id": "8673cd80-bf53-4bca-b684-bec1d6bdf004",
      "plugin_id": 1
    }
  }
  ```
</CodeGroup>

You can confirm the plugin is installed by using [/ipfs/gateway\_plugins/:gateway\_id](/api-reference/endpoint/ipfs/list-installed-plugins-for-gateway)

<CodeGroup>
  ```typescript Get Plugins for Gateway theme={null}
  const gatewayId = "8673cd80-bf53-4bca-b684-bec1d6bdf004"
  const installPlugin = await fetch(`https://api.pinata.cloud/v3/ipfs/gateway_plugins/${gatewayId}`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${PINATA_JWT}`
    }
  })
  ```

  ```json Response theme={null}
  {
    "data": {
      "gateway_id": "8673cd80-bf53-4bca-b684-bec1d6bdf004",
      "plugin_id": 1
    }
  }
  ```
</CodeGroup>

## Using Plugins

Each plugin has its own unique use case and usage, please see the appropriate links for each one to see how they are used.

[Content Addressable Plugin](/gateways/plugins/content-addressable)

## Uninstalling Plugins

Just like installing, removing a plugin can be done through the Pinata App or the Pinata API.

### Pinata App

To unintall a plugin first navigate to the Plugin Marketplace tab and then select the "My Plugins" tab.

<Frame>
  <img />
</Frame>

Then locate the target gateway and plugin, then click the action item on the right hand side and select "Uninstall" from the dropdown menu.

<Frame>
  <img />
</Frame>

### Pinata API

To uninstall a plug from the API you can use the [/ipfs/gateway\_plugins/:gateway\_id/plugin/:plugin\_id](/api-reference/endpoint/ipfs/uninstall-gateway-plugin) endpoint, where the `gateway_id` is the target gateway and the `plugin_id` is the target plugin to uninstall.

```typescript Install Plugin theme={null}
const gatewayId = "8673cd80-bf53-4bca-b684-bec1d6bdf004"
const pluginId = 1
const uninstallPlugin = await fetch(`https://api.pinata.cloud/v3/ipfs/gateway_plugins/${gatewayId}/plugin/${pluginId}`, {
  method: "DELETE",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${PINATA_JWT}`
  }
})
```


# Hot Swaps
Source: https://docs.pinata.cloud/gateways/plugins/hot-swaps

Use the Hot Swaps plugin to make one CID map to another

The Hot Swaps plugin can be used to "redirect" a CID to another CID. This can be useful when you need to replace content without updaing the CID hash.

While it may seem like this undermines the benefits of IPFS' imutable content system, it should be made clear that this only works on a per gateway level for those who install it. Additionally:

* Any CID can be called by another gateway to view the original content
* Pinata has an [SDK method](/sdk/files/public/get-swap-history) and [API endpoint](/api-reference/endpoint/get-swap-history) that any Pinata user can call to check the history of a swap
* All gateway requests that have the hot swap plugin installed and are serving a swapped CID will have a header of `etag` that will have the CID that is currently being directed to.

To demonstrate how this plugin works, consider the following example:

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example.mypinata.cloud", // Gateway has Hot Swaps installed
});

async function main() {
  try {
    // Upload the first file
    const file = new File(["The original CID"], "cid.txt", {
      type: "text/plain",
    });
    const { IpfsHash: CID1 } = await pinata.upload.public.file(file);
    console.log("This is the original CID hash: ", CID1);

    // Upload a second file
    const file2 = new File(["The new CID"], "cid.txt", { type: "text/plain" });
    const { IpfsHash: CID2 } = await pinata.upload.public.file(file2);
    console.log("This is the new CID hash: ", CID2);

    // Create the swap, so when we visit CID1 we will get the content of CID2
    const swap = await pinata.files.public.addSwap({
      cid: CID1,
      swapCid: CID2,
    });
    console.log("Swap created: ", swap);

    // Fetch CID1 through our gateway that has Hot Swaps installed, get the content of CID2
    const data = await pinata.gateways.public.get(CID1);
    console.log("Result of requestingt CID1 through the gateway: ", data);
  } catch (error) {
    console.log(error);
  }
}

main();
```

## Installation

Follow the steps in the [Getting Started](/gateways/plugins/getting-started) guide to install the Hot Swaps Plugin.

## Usage

After installing the plugin you can then make CID swaps and have them reflect when making Gateway requests. The first parameter `cid` will be the original CID, and `swapCid` will be the content you want it to point to instead.

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const swap = await pinata.files.public.addSwap({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```

You can fetch the history of CID swaps using the `swapHistory` method, passing in the `cid` of the original CID and the `domain` of the gateway that has the Hot Swaps plugin installed.

```typescript theme={null}
const history = await pinata.files.public.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})

// [
//   {
//     mappedCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu",
//     createdAt: "2024-08-19T14:34:46.492432Z"
//   },
//   { mappedCid: null, createdAt: "2024-08-19T14:25:10.208726Z" },
//   {
//     mappedCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu",
//     createdAt: "2024-08-19T00:23:41.755206Z"
//   }
// ]
```

To delete a CID swap you can simply use the `deleteSwap` method and pass in the CID.

```typescript theme={null}
const deleteSwap = await pinata.files.public.deleteSwap(
  "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske"
)
// OK
```


# Retrieving Files
Source: https://docs.pinata.cloud/gateways/retrieving-files



Gateways are Pinata's tool to deliver your content with speed and security. They're similar to CDNs but with way more features. 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).

<img />

The gateway domains are randomly generated and might look something like this:

```
aquamarine-casual-tarantula-177.mypinata.cloud
```

## Retrieving Public Files

If you uploaded or added files to Public IPFS then you can access them with the methods mentioned previously, or just by appending the `cid` to the `gateway` url like so:

```
https://example-gateway.mypinata.cloud/ipfs/{cid}
```

Since these files are public there is no need create a temporary url. At this point you can also use some handy queries such as `?filename=image.png`, `?download=true`.

<Tip>
  By default your gateway will only be able to fetch the files that are pinned to your account. If you want to open your gateway to access any file on IPFS check out our guide for [Access Controls](/gateways/gateway-access-controls).
</Tip>

One of the simplest ways to fetch content is through the [get](/sdk/gateways/private/get) method in the [SDK](/sdk). All content is referenced by the `cid`, a special identifier given to each file based on the cotnent of that file.

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const { data, contentType } = await pinata.gateways.public.get(
  "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq"
)
```

You can also use the SDK to provide a URL for your file using the `pinataGateway` in the config.

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const url = await pinata.gateways.public.convert(
  "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq"
)
```

### Image Optimizations

With public files you can simply append the [Image Optimizations](/gateways/image-optimizations) queries which would look something like this:

```
https://example-gateway.mypinata.cloud/files/{cid}?img-width=500&img-height=500
```

## Adding a Custom Domain

Pinata also allows you to create a custom domain for your Dedicated Gateway. Simply visit the [Gateways Page](https://app.pinata.cloud/gateway), click the menu button on the right side of your gateway, then click Add Custom Domain. You'll need to own the domain you want to use. When you enter your domain, you will be prompted to enter DNS information through your registrar.

## Retrieving Private Files

All content uploaded with Private IPFS is by default private, and there are a few ways you can view it. One of the simplest ways to fetch content is through the [get](/sdk/gateways/private/get) method in the [SDK](/sdk). All content is referenced by the `cid`, a special identifier given to each file based on the cotnent of that file.

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const { data, contentType } = await pinata.gateways.private.get(
  "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq"
)
```

Under the hood this method generates a 30 second access link to access the content with. Alternatively you can also create an access link that can be used to access the content for a specified limited amount of time.

<CodeGroup>
  ```typescript SDK {8-11} theme={null}
  import { PinataSDK } from "pinata";

  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: "example-gateway.mypinata.cloud",
  });

  const url = await pinata.gateways.private.createAccessLink({
  	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
  	expires: 30, // Number of seconds link is valid for
  });
  ```

  ```typescript API {5-10} theme={null}
  const JWT = "YOUR_PINATA_JWT";

  async function url() {
    try {
      const payload = JSON.stringify({
        url: "https://example.mypinata.cloud/files/bafybeifq444z4b7yqzcyz4a5gspb2rpyfcdxp3mrfpigmllh52ld5tyzwm",
        expires: 500000,
        date: 1724875300,
        method: "GET"
      })

      const request = await fetch(
        `https://api.pinata.cloud/v3/files/download_link`,
      {
        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);
    }
  }
  ```
</CodeGroup>

### Image Optmizations

In order ot use [Image Optmizations](/gateways/image-optimizations) with private files the query parameters must be part of the request of getting a signed url.

<CodeGroup>
  ```typescript SDK {3-7} theme={null}
  const data = await pinata.gateways.private
    .createSignedURL("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
    .optimizeImage({
      width: 500,
      height: 500,
      format: "webp"
    })
  ```

  ```typescript API {6} theme={null}
  const JWT = "YOUR_PINATA_JWT";

  async function url() {
    try {
      const payload = JSON.stringify({
        url: "https://example.mypinata.cloud/files/bafybeifq444z4b7yqzcyz4a5gspb2rpyfcdxp3mrfpigmllh52ld5tyzwm?img-width=500&img-height=500&img-format=webp",
        expires: 500000,
        date: 1724875300,
        method: "GET"
      })

      const request = await fetch(
        `https://api.pinata.cloud/v3/files/download_link`,
      {
        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);
    }
  }
  ```
</CodeGroup>


# Chat Completions
Source: https://docs.pinata.cloud/inference/chat-completions

OpenAI-compatible chat completions endpoint

The chat completions endpoint is OpenAI-compatible, so most OpenAI SDKs and tools work by changing only the base URL and API key. [Enable inference](/inference/overview#enable-inference) first to mint your `privateKey`, then send requests.

## Endpoint

```http theme={null}
POST https://agents.pinata.cloud/v0/llm/chat/completions
```

|                  |                                                                                                                                                        |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Auth**         | `Authorization: Bearer <PINATA_LLM_KEY>` — the `privateKey` returned when you [enabled](/inference/overview#enable-inference), **not** your Pinata JWT |
| **Content-Type** | `application/json`                                                                                                                                     |
| **Billing**      | Token usage draws down [credits](/inference/credits)                                                                                                   |

<Note>
  For an OpenAI-compatible client, set the base URL to `https://agents.pinata.cloud/v0/llm` — the SDK appends `/chat/completions` itself.
</Note>

## Request

```bash cURL theme={null}
curl https://agents.pinata.cloud/v0/llm/chat/completions \
  -H "Authorization: Bearer $PINATA_LLM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-4-6",
    "messages": [
      { "role": "user", "content": "Hello!" }
    ]
  }'
```

```ts OpenAI SDK theme={null}
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.PINATA_LLM_KEY,
  baseURL: "https://agents.pinata.cloud/v0/llm",
});

const completion = await client.chat.completions.create({
  model: "anthropic/claude-sonnet-4-6",
  messages: [{ role: "user", content: "Hello!" }],
});

console.log(completion.choices[0].message.content);
```

### Parameters

| Field                                  | Type    | Description                                                                                                                                                   |
| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`                                | string  | A `provider/model` id from [Models](/inference/models)                                                                                                        |
| `messages`                             | array   | Conversation history (`role` + `content`)                                                                                                                     |
| `stream`                               | boolean | Stream tokens as server-sent events. Defaults to `false`                                                                                                      |
| `max_tokens` / `max_completion_tokens` | number  | Output token cap. Defaults to `4096` if neither is set. For OpenAI `gpt-5.x` / o-series models, `max_tokens` is translated to `max_completion_tokens` for you |

Other standard OpenAI sampling fields (`temperature`, `top_p`, etc.) are passed through to the underlying model.

## Response

```json theme={null}
{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "anthropic/claude-sonnet-4-6",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hi there!" },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "total_tokens": 0
  }
}
```

## Streaming

Set `"stream": true` to receive server-sent events. Usage is included in the stream, and the final event is `data: [DONE]`.

```bash theme={null}
curl https://agents.pinata.cloud/v0/llm/chat/completions \
  -H "Authorization: Bearer $PINATA_LLM_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "anthropic/claude-sonnet-4-6", "messages": [...], "stream": true }'
```

## Errors

| Status | Meaning                                                                                                                       |
| ------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing `Authorization` header, or an invalid/revoked key                                                                     |
| `402`  | Insufficient credits — top up or set [auto top-up](/inference/credits#auto-top-up). The body includes `availableMicrocredits` |
| `429`  | Rate limited — retry after a short wait                                                                                       |

Error bodies follow the OpenAI error shape, e.g. `{ "error": { "message": "..." } }`.


# Credits
Source: https://docs.pinata.cloud/inference/credits

Fund Pinata-hosted model usage and configure auto top-up

Credits are a prepaid balance that funds usage of Pinata-hosted models — both inside an [agent](/agents/models) (the `Pinata` provider) and through the [Inference](/inference/overview) endpoint directly. Token usage draws the balance down; when it runs out, those requests fail until you add more.

<Note>
  Credits only apply to **Pinata-hosted** models. Agents that use your own provider keys (OpenAI, Anthropic, OpenRouter, etc. — see [Secrets](/agents/secrets)) bill through that provider, not credits. The [free tier](/agents/models#free-tier-agents) doesn't use credits either.
</Note>

## How credits are consumed

* Usage is metered per token across prompt and completion, at each model's published rates — see [Models](/inference/models) for current pricing.
* When you run out of credits, Pinata-hosted requests return `402` until you top up. Requests against your own provider keys are unaffected.

All credit routes authenticate with your standard Pinata JWT.

### Check your balance

```bash theme={null}
curl https://agents.pinata.cloud/v0/credits/balance \
  -H "Authorization: Bearer $PINATA_JWT"
```

```json Response theme={null}
{ "total": 100.0, "loaned": 0.5, "available": 99.5 }
```

`available` is what you can spend right now — `total` minus `loaned` (credits held against in-flight requests). Values are in credits.

## Top up

Buy credits from the **Billing** dashboard in the [Agents app](https://agents.pinata.cloud). Purchases are charged through Stripe against the payment method on file, and your balance updates as soon as the payment clears.

<Frame>
  <img alt="Buying credits in the Agents app Billing dashboard" />
</Frame>

## Auto top-up

Auto top-up keeps the balance from running dry by automatically purchasing more credits when it drops below a threshold — useful for production agents and Inference traffic you don't want interrupted. You can set it up in the **Billing** dashboard of the [Agents app](https://agents.pinata.cloud), or via the API below.

| Setting       | Rules                                                                                 |
| ------------- | ------------------------------------------------------------------------------------- |
| **Threshold** | When `available` drops below this, a top-up triggers. Must be at least `5`, or `null` |
| **Amount**    | How many credits to buy each time it triggers. Positive integer, or `null`            |

Set both to `null` to disable auto top-up.

### Configure auto top-up

```bash theme={null}
curl -X PUT https://agents.pinata.cloud/v0/credits/topup/config \
  -H "Authorization: Bearer $PINATA_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "threshold": 10, "amount": 50 }'
```

```json Response theme={null}
{ "ok": true }
```

Read the current config with `GET /v0/credits/topup/config`:

```json Response theme={null}
{ "threshold": 10, "amount": 50 }
```

<Warning>
  Auto top-up charges the payment method on file whenever the threshold is crossed. Set the threshold and amount deliberately so a runaway agent can't trigger repeated charges.
</Warning>

## Related

<CardGroup>
  <Card title="Inference" icon="bolt" href="/inference/overview">
    The Pinata LLM endpoint credits pay for
  </Card>

  <Card title="Agent Models" icon="microchip" href="/agents/models">
    Pinata-hosted vs. bring-your-own-key
  </Card>
</CardGroup>


# Models
Source: https://docs.pinata.cloud/inference/models

Models available on Pinata Inference

Models are referenced as `provider/model` (e.g. `anthropic/claude-sonnet-4-6`) and passed as the `model` field to the [chat completions endpoint](/inference/chat-completions). The list and pricing are served live from the API, so query the endpoint for the current set rather than relying on a static table.

<Note>
  These are the same Pinata-hosted models an [agent](/agents/models) can use via the `Pinata` provider. Calling them directly through Inference and using them inside an agent both draw down the same [credit balance](/inference/credits).
</Note>

## List models

This endpoint is public — no authentication required.

```bash theme={null}
curl https://agents.pinata.cloud/v0/llm/models
```

```json Response theme={null}
[
  {
    "model_id": "anthropic/claude-sonnet-4-6",
    "input_usd_per_1m_tokens": 3.0,
    "output_usd_per_1m_tokens": 15.0,
    "cache_read_usd_per_1m_tokens": 0.3
  }
]
```

| Field                          | Description                                |
| ------------------------------ | ------------------------------------------ |
| `model_id`                     | The `provider/model` id to pass as `model` |
| `input_usd_per_1m_tokens`      | Price per 1M input (prompt) tokens         |
| `output_usd_per_1m_tokens`     | Price per 1M output (completion) tokens    |
| `cache_read_usd_per_1m_tokens` | Price per 1M cached-input tokens read      |

<Note>
  The example prices above are illustrative — always read the live endpoint for current rates.
</Note>

## Pricing

Usage is metered per token at the rates above and billed against your [credit balance](/inference/credits). See [Credits](/inference/credits) for how billing works and how to fund usage.


# Overview
Source: https://docs.pinata.cloud/inference/overview

Run LLM inference on Pinata with an OpenAI-compatible API

Pinata Inference is a hosted, OpenAI-compatible LLM endpoint. Enable it on your account, then use it two ways:

* **API** — point any OpenAI-compatible client at Pinata and call [chat completions](/inference/chat-completions) directly, no agent required.
* **Agents app** — the same hosted models show up alongside your other providers in an agent's [Models](/agents/models) list (connect the **Pinata** provider in the [Secrets Vault](/agents/secrets#connect-a-provider)).

Either way, requests run against the [models](/inference/models) Pinata hosts, and usage is metered and drawn down from your [credit balance](/inference/credits).

<Note>
  Inference is billed through credits. Before your first request, make sure your workspace has a credit balance and (optionally) [auto top-up](/inference/credits#auto-top-up) enabled so requests don't fail on an empty balance.
</Note>

## Enable inference

Turn Pinata-hosted inference on for your account with a single call, authenticated with your standard Pinata JWT:

```bash theme={null}
curl -X POST https://agents.pinata.cloud/v0/llm/enable \
  -H "Authorization: Bearer $PINATA_JWT"
```

```json Response theme={null}
{
  "success": true,
  "privateKey": "<your-inference-key>"
}
```

Enabling generates an Ed25519 key pair: the **private key is returned once** in this response and also stored encrypted in your [secrets](/agents/secrets) as `PINATA_LLM_KEY`; the public key is kept for request validation.

<Warning>
  The `privateKey` is your inference credential — it's what authenticates [chat completions](/inference/chat-completions), **not** your Pinata JWT. Save it now; it isn't returned again.
</Warning>

Once enabled, the [chat completions endpoint](/inference/chat-completions) accepts requests and usage starts drawing down your [credit balance](/inference/credits).

<Tip>
  Prefer the dashboard? Connecting the **Pinata** provider in the [Secrets Vault](/agents/secrets#connect-a-provider) does the same thing, and is also how Pinata-hosted models become selectable inside your agents.
</Tip>

### Check status

```bash theme={null}
curl https://agents.pinata.cloud/v0/llm/status \
  -H "Authorization: Bearer $PINATA_JWT"
```

```json Response theme={null}
{ "enabled": true, "createdAt": "2026-06-29T00:00:00.000Z" }
```

## Disable inference

```bash theme={null}
curl -X DELETE https://agents.pinata.cloud/v0/llm/disable \
  -H "Authorization: Bearer $PINATA_JWT"
```

```json Response theme={null}
{ "success": true }
```

Disabling revokes the inference key and removes the associated secrets. New requests to the chat completions endpoint are rejected and Pinata-hosted models are no longer selectable in agents — any running agents using the managed key are automatically flipped to the [free tier](/agents/models#free-tier-agents) fallback so they don't break. Your credit balance is untouched — disabling does not refund or expire credits — and you can re-enable at any time (which mints a **new** `privateKey`).

<Warning>
  Disabling takes effect immediately. Any in-flight requests may be cut off, and anything pointing at the endpoint will start getting errors until you re-enable.
</Warning>

## Authentication

There are two credentials, used for different routes:

| Routes                                         | Credential                                                                             |
| ---------------------------------------------- | -------------------------------------------------------------------------------------- |
| **Management** — `enable`, `disable`, `status` | Your **Pinata JWT** (from [Account → API Keys](/account-management/api-keys))          |
| **Inference** — `chat/completions`             | The **`privateKey`** returned when you enabled (stored as the `PINATA_LLM_KEY` secret) |
| **Catalog** — `models`                         | None — public                                                                          |

```http theme={null}
# Management
Authorization: Bearer <PINATA_JWT>

# Inference
Authorization: Bearer <PINATA_LLM_KEY>
```

## Next steps

<CardGroup>
  <Card title="Chat Completions" icon="message" href="/inference/chat-completions">
    Call the OpenAI-compatible endpoint
  </Card>

  <Card title="Models" icon="microchip" href="/inference/models">
    See which models Pinata hosts
  </Card>

  <Card title="Credits" icon="coins" href="/inference/credits">
    Fund usage and set auto top-up
  </Card>
</CardGroup>


# How does IPFS work with NFTs?
Source: https://docs.pinata.cloud/ipfs-101/how-does-ipfs-work-with-nfts



Pinata has been empowering the NFT space since 2018 and there's a good reason for that. IPFS is the perfect pairing for NFTs for several reasons.

* **IPFS is Immutable** - Anything that is uploaded to IPFS cannot change, which helps preserve the value of an NFT.
* **IPFS is Decentralized** - Instead of using a centralized server where one person can control the content, IPFS is distributed and makes sure anyone can pin the content and keep it persisted.
* **IPFS is Portable** - Since the [CID](/ipfs-101/what-are-cids) for content will be constant, and IPFS pinning works in a decentralized manner, anyone can take a CID and pin it themselves. This allows content to be "transferred" and kept up on the network as people value it. (read more about that concept [here](https://medium.com/pinata/who-is-responsible-for-nft-data-99fb4e8147e4))

One of the biggest reasons you want to use IPFS for NFTs is to prevent tampering or "rug pulls" where someone can just delete the data for an NFT and make it worthless. NFTs are tokens on the blockchain that have a "Token URI" which is simply a link pointing to data about that NFT off-chain, because putting data on-chain is far too expensive. If this link is a centralized server, like `https://server.com/pinnie.png`, then whoever has control of the server can simply upload totally different content and keep the same name, thus keeping the same link. Or they could just delete it and it would be empty!

<img />

This is where IPFS becomes necessary. Since the address or link to the content on IPFS is the [CID](/ipfs-101/what-are-cids), which is based on the content itself and is immutable, you can't change it or alter it. In addition, the ability for multiple people to [pin](/ipfs-101/what-is-ipfs-pinning) content and help it persist on the network makes it harder for something to just be deleted.

## How to Make an NFT with Pinata

It's important to note that Pinata is currently not providing any minting services. This means you would use Pinata to host the media content and the metadata for the NFT, and then another service or self-deployed smart contract to actually mint the NFT. But don't worry, we'll show you a few different ways you could do that!

### Step 1: Upload the Content

The first thing you need to do is upload the content to Pinata. Since IPFS supports any kind of file, the truth is any kind of file can be an NFT. How that file is referenced in the metadata is a bit of a different story, so be sure to check [metadata standards](https://docs.opensea.io/docs/metadata-standards) to make sure you content will be seen on marketplaces or wallets.

To upload the content you can either do a simple upload through the Pinata App by navigating to the [files page](https://app.pinata.cloud/ipfs/files) and uploading through the UI like so:

<img />

Or you can upload through the [SDK](/sdk) using a script like this:

```typescript 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"
})

async function upload(){
  try {
    const blob = new Blob([fs.readFileSync("./path/to/image.png")]);
    const file = new File([blob], "image.png", { type: "image/png"})
    const upload = await pinata.upload.file(file);
    console.log(upload)
  } catch (error) {
    console.log(error)
  }
}
```

In each case you will want to grab the CID for that file, which will look something like this:

<img />

### Step 2: Create and Upload Metadata

Now that we have the CID for our content on IPFS, we need to create a metadata file that will have all the other information about the NFT. You will want to use a JSON format and follow [industry metadata standards](https://docs.opensea.io/docs/metadata-standards) to make sure that it will show up in marketplaces and wallets. You can use the template below as we'll walk through each piece.

```json metadata.json theme={null}
{
  "name": "Name of NFT",
  "description": "Description of NFT",
  "external_url": "https://pinata.cloud",
  "image": "ipfs://CID_GOES_HERE"
}
```

* `name` - This will be the name of this particular NFT, not the collection.
* `description` - Describes the NFT.
* `external_url` - A link to the website of the NFT project or creator.
* `image` - This would be the link to the image, if it was a video or gif then you would want to follow metadata standards and have a backup image, and also add an `animation_url` for the video.

You'll notice that we are using the IPFS protocol URL for the image link. There are other ways to reference CIDs in NFT metadata which you can read about more [here]().

<img />

Once you have that file filled out you will want to save it as something like `metadata.json` (this might be different if you are making a large project using folders). Then you can upload the metadata file to Pinata using the app like before, or if you are using the API we have a [JSON endpoint](/api-reference/endpoint/ipfs/pin-json-to-ipfs) you can use to simplify the process, like so:

```typescript 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.json({
  name: "Pinnie NFT",
  description: "A Pinnie NFT from Pinata",
  image: "ipfs://bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4",
  external_url: "https://pinata.cloud"
})
```

After the metadata is uploaded you should now have a CID for that metadata file; this will be the core identity of your NFT and will act as the **Token URI**.

### Step 3: Mint the NFT

Now that you have the all important Token URI metadata CID, you can mint an NFT! How you go about doing this will depend on multiple factors.

#### Starting an NFT Project

If you are non-technical and you're just looking for the easy way to create an NFT project, you will likely want to use a minting service like [Bueno](https://bueno.art), [Mintplex.xyz](https://mintplex.xyz), or [Manifold](https://manifold.xyz). There are a lot of complexities with smart contracts and making sure the content is setup properly, and using a service like this will make things much easier!

#### Just Exploring NFTs

If you want to get your feet wet with smart contracts you could follow the video below! It covers the Base layer 2 blockchain in particular, but the method will work on other EVM chains.

<iframe title="YouTube video player" />

#### Implementing into an Decentralized App

If you are a developer that is trying to do NFT minting on a larger scale, you could use an NFT minting API like [Crossmint](https://crossmint.com). Lucky for you we have some tutorials on how to do just that!

<iframe title="YouTube video player" />


# What are CIDs?
Source: https://docs.pinata.cloud/ipfs-101/what-are-cids



Content Identifiers (CIDs) are one of the most important parts of IPFS. Whenever you share a file through IPFS, that file or folder gets assigned a CID that looks like this:

<img />

What's happening behind the scenes: IPFS is breaking the file into blocks and running it through a cryptographic hash. It's like a math equation, where if you had `x + 2 = y`, and then you pass in `x = 2`, then `y = 4`. No matter how many times you run it, you will have the same answer if you keep passing in `x = 2`. In the same way,  IPFS creates CIDs for files that are reproducible. If you share a file of `Pinnie.png` you will get the same CID every time. It is through this CID that content is fetched across the IPFS network, which each piece pointing to a blocks on different nodes, altogether building a cohesive piece of content.

<Note>The CID is determined by the content of the file or folder</Note>

Files can be retrieved either with a local IPFS node using a URL like `ipfs://{CID}` or if you don't have an IPFS node, you can use an [IPFS Gateway](/ipfs-101/what-are-ipfs-gateways) like `https://ipfs.io/ipfs/{CID}`. This simple concept of CIDs give them several super powers.

### Verifiability

The CID's uniqueness and dependence on the content it represents make the data inherently verifiable. Suppose a creator shares an image of a digital artwork on the IPFS network: if someone tries to replace the original image with a different one, the CID for the new image will be different. This guarantees that the content cannot be tampered with, reinforcing trust within the network.

### Portability and Addressability

CIDs also facilitate data portability. Because a CID represents the content itself rather than its location, you can move your data across various nodes within the IPFS network without losing access to it. This content-addressed system breaks away from the traditional location-based addressing used in systems like HTTP, leading to a more robust and reliable data sharing network.

### Ownership and Persistence

The CID's capabilities don't stop at verification and portability. They also empower users to take ownership of data they care about. By 'pinning' a CID to a pinning service or their own node, users can ensure the longevity of data, even if the original data owner stops hosting it. This is particularly valuable in the context of non-fungible tokens (NFTs). For instance, if you purchase an NFT, you can pin the CID of the token's metadata and image to a pinning service or a node you control, securing its existence indefinitely. This practice truly underlines the principle of digital ownership.

### Community Preservation

In addition to ownership and persistence, communities can preserve digital content through the practice of pinning much like how traditional art is preserved in museums. CIDs provide an opportunity for groups to maintain access to valuable data, ensuring its longevity.

## V0 & V1

There are two different types of CIDs you might see while using IPFS. The first is V0, which looks something like this:

`QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng`

This was the first kind of IPFS CID that was used for several years, however over time a new version was developed called the V1. This version was more flexible and future proof, and looks something like this:

`bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4`

Both of these CIDs are the same content, a wonderful picture of Pinnie, however the cryptographic algorithm they were put through was different. Pinata has used the V0 for several years but now uses V1 for the previously mentioned reasons.

## How Do I Reference CIDs?

When it comes to providing an IPFS link, there are a few options to choose from.

### IPFS Protocol URL

An IPFS protocol URL looks like this:

<img />

If you copy and paste that into your browser, you may not get anything back. That is because in order to use this one, you have to have a local IPFS node running to participate in the network. Even when you do, it will likely be very very slow since IPFS is still a growing network.

Why use it? Well for a couple of reasons. If you are building on a blockchain that already uses IPFS a lot, like Ethereum or another L2 chain, lots of marketplaces and apps are used to seeing this format. When they see it, they use tools to convert the url into a gateway url so it can display the content on a website. This can be a good thing or a bad thing. If the platform has a dedicated/private gateway, the speed will be very fast (much like our own dedicated gateways). However, if the platform uses a public gateway, the speeds will be very slow. In the end, the platform has control over how well your content is received. Additionally, using the standard IPFS url might help future proof your assets, as public gateways might be stopped down the road (however the CID is still in the url in those cases, so if the platform knows what to do, they can still get the content if pinned).

If you are on a different L1 chain, you may want to test it first.. There are some platforms on other blockchains that expect "https" instead and nothing will load if you use this.

### Public IPFS Gateway URL

A public gateway URL looks something like this:

<img />

This will deliver the content in the browser without the need of a local IPFS node. However, since this gateway is a public gateway, your speed might vary due to the heavy traffic and congestion. Some platforms will see this kind of the url and switch it out with their own faster gateway choice, but not always. Generally you want to assume that if you take this path, the assets will be slow.

### Dedicated IPFS Gateway URL

A [Dedicated Gateway](/gateways/dedicated-ipfs-gateways) URL looks something like this:

<img />

Dedicated Gateways are much much faster than any other method, and should ideally be used when trying to display content on your own platform. However, using them in NFTs should be done in caution. If you use a Dedicated Gateway in your NFT project metadata and image links, your speed will be great, however anytime another marketplace or rarity bot asks the blockchain for the IPFS data, your gateway will be hit. Since most Dedicated Gateways are paid services, this could greatly drive up your costs and usage. You'll get the best performance, control, and flexibility with this method, however you might have to pay more than the other methods.


# What are IPFS Gateways?
Source: https://docs.pinata.cloud/ipfs-101/what-are-ipfs-gateways



Once you have uploaded content to Pinata, naturally the first thing you want to do is view it on the IPFS network! But there's a problem: IPFS is a separate protocol, just like HTTP for regular websites is a protocol. To access that content, we need a gateway to bridge IPFS and HTTP. IPFS gateways help us do exactly that!

An example of accessing content from this gateway can be seen with: `https://ipfs.io/ipfs/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4`

What's going on here exactly? Let's break it down. First you have the gateway domain. There are lots of different domains out there, like `ipfs.io`, and if you have a Pinata account you get a Dedicated Gateway which might look something like `aquamarine-casual-tarantula-177.mypinata.cloud`. Each of these can be used to bridge files from IPFS to HTTPs. Next you have the IPFS path which looks like `ipfs/` and this is necessary for a gateway to work. Finally we have the CID at the end, which is the actual IPFS address for our content.

<img />

## Folder Paths

Sometimes your CID might be a folder, in which case you might have difficulty loading it through the gateway. This happens because if you do not designate a complete folder path, then the gateway will try to load all of the files and index them into a sheet showing every file. This can be pretty intensive depending what kind of gateway you're using, and will likely stall out due to how long it can take. To remedy this, simply add on the file path of the content you're trying to get inside. For instance, if we have a folder with the CID of `QmWfHgs3nKiyFWx3tFEYvm8DiHTrCsxEHxvDdBh95ZQSLT` and the inside looks something like this:

<img />

Then we could access the files inside by adding on `/pinnie.png` or `clouds.json` to the end of our folder path. In the end we would have something like this.

<img />

## Public Gateways

The most common kind of IPFS Gateways are Public Gateways. These are usually run and maintained by IPFS Pinning Services, protocols, or even smaller groups that want to help build the IPFS ecosystem. They're referred to as "Public" because anyone can access them! You just have to add a CID to the end of one to start using it. You can try that now by adding this CID `bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4` to the end of a Public Gateway:

```text theme={null}
https://ipfs.io/ipfs/
```

There's something you might notice when you start using Public Gateways frequently, and that is the speed and rate limits. Since these are Public, they're used **heavily** by people all around the world. They are constantly getting hammered, and because of that they're just naturally slower. You can think of it like a highway or interstate: if you have a high flow of traffic, its likely going to bottleneck and cause a traffic jam. Same goes for IPFS Gateways!

<Warning>Public Gateways are not meant for production apps, be sure to only use them for testing!</Warning>

## [Dedicated Gateways](/gateways/dedicated-ipfs-gateways)

Thankfully Pinata has you covered! When you sign up for a free Pinata account, you get your own Dedicated Gateway. Dedicated Gateways are like the toll roads on highways and interstates; it's your own private boulevard to get unmatched speeds. Our Dedicated Gateways are well known in the industry as being fast, reliable, and just plain simple to use, and they do that through a large network of IPFS nodes and a built in global CDN that helps cache content to be much faster on subsequent loads. Check on the next few pages to learn more about how they work!


# What is IPFS?
Source: https://docs.pinata.cloud/ipfs-101/what-is-ipfs



[IPFS](https://ipfs.io) (Inter-Planetary File System) is a peer-to-peer distributed file system that is used primarily for data that can't be stored on a blockchain. Unlike HTTP, a more standard file sharing protocol, IPFS has special properties that make it ideal for a decentralized data model that works in tandem with blockchains. Some of those abilities include:

* **Distributed** - IPFS is made of multiple IPFS nodes, so not one entity can control what is shared on the network.
* **Immutability** - Content that is shared on IPFS cannot be changed or altered.
* **Addressability** - Content that is shared on IPFS uses unique addresses to locate content on the network.
* **Portability** - Data that is pinned to IPFS can be re-pinned or transferred to other nodes, helping valued content persevere on the network.
* **Garbage Collection** -  The way IPFS handles data works in such a way that any content that is not pinned by a node will eventually fall off the network, keeping it clean from unwanted data and clutter.

Due to these properties it is ideal for Web3 data storage such as NFTs, DeFi, and other decentralized infrastructures.

## How Does IPFS Work?

The best way to understand IPFS is to see it as an alternative to HTTP. With HTTP server model the end user has a computer which makes a request to see content from a server, then that server sends back the requested content. This is a simple two way communication, and while there are more complex instances, the key is that the data is stored on the server and has ultimate control of that content.

<img />

IPFS on the other hand, is a network is made up of a multitude of different IPFS nodes which share data with each other. Whenever a node wants to share content on IPFS, it runs the content through a cryptographic hash and returns a [CID](/ipfs-101/what-are-cids), or "Content Identifier." This CID serves as both the address and the verification that the content is what it says it is. After the CID has been created the IPFS node will "pin" that content to the network, essentially sharing it and saying it is worth keeping.

<img />

As the CID is requested by other IPFS nodes, the content will pass through other nodes leaving a cache that can be used for faster retrieval the more it is requested. This cache will eventually get pretty big and can cause bloat, and this is where pinning really comes into play. The IPFS node will run a garbage collector from time to time which will clear the contents of the cache, except for any CIDs that are actively being pinned by another IPFS node. As long as one IPFS node is pinning that content, it will stay on the network. If the content is unpinned and there is no other node holding it, eventually the content will fade from the network. This creates a model that can persevere data but not to a point that will cause clutter and bloat.

## Why IPFS?

Blockchains have an incredible ability to keep a long ledger of transactions and who sends or signs them, however blockchains are terrible at storing data. Since blockchains require gas fees to be paid anytime data is stored on the blockchain, larger forms of data can cause astronomical prices. As of 2023, storing 1GB of data on Ethereum would cost around \$250,000,000! This caused a problem in particular for NFTs as they were first taking off.

NFTs, or really anything being used on the blockchain, will usually have a token element that is on chain that has a pointer to data off-chain (referred to as the token URI). This pointer is usually a link to a metadata file in the form of JSON, which has information like the name of the NFT, a description, and usually another link to an image that is also off-chain. Back in the earlier days of NFTs these pointers or token URIs were centralized servers, and that caused a problem. There was nothing stopping the owner of that server from changing the data to be something completely different. A link like `https://drive.google.com/nft/pinnie.png` could be dynamic; at any time the owner of that file could upload a picture of something else and name it `pinnie.png` and the link would still be the same. What was the point of buying a piece of art on the blockchain if it could change and be worthless?

<img />

This is where IPFS steps in, along with it's special abilities of cryptography and distributed nature. Whenever a file is stored on IPFS, it gets a unique CID that is based on the content of the file. If the file is changed by a single pixel or character, then it will receive a completely different CID. It solves the problem of `pinnie.png` being replaced with something else since you couldn't simply just change the name. Due to the cryptographic nature of IPFS, anything shared on IPFS is immutable and cannot be changed.

<img />

Since IPFS is decentralized in the way it stores the data, it also solved the potential crisis of something deleting the data or the server going offline. With IPFS pinning, multiple nodes can pin data to ensure that it stays online. A good metaphor for this is a fridge. If you wanted to put a picture on a fridge, you would likely use a magnet to put it up there. If you have two or three magnets on that picture, you could remove the original magnet and the picture would stay up. IPFS works in a similar way; anyone can take the CID for some content and pin it themselves to help preserve it. Content can persist as long as a single IPFS node is pinning it!

## What Does Pinata Do?

Pinata is an IPFS Pinning Service that provides user with, you guessed it, IPFS services! These range from uploading files and pinning them to IPFS, to blazing fast [Dedicated Gateways](/gateways/dedicated-ipfs-gateways) which come in handy if you need to fetch content from IPFS. Our focus is to provide developers easy to use tools so they can add in IPFS to their Web3 architecture as simply as possible, and to make it reliable and trustworthy. If you're not sure where to start, check out our [Getting Started](/quickstart) page which will have you up and running with Pinata in no time! 🚀

<iframe title="YouTube video player" />


# What is Pinning?
Source: https://docs.pinata.cloud/ipfs-101/what-is-ipfs-pinning



IPFS "Pinning" is the foundation to getting content on IPFS. When you upload a file to IPFS, the IPFS node will create a [CID](/ipfs-101/what-are-cids) for that file which will act as the identifier and the address. Then it will "pin" that file to the IPFS network, making it available for other nodes to request it. At that point any other IPFS node can request the content for a CID, and the content will pass through other nodes leaving a cache on that node. This makes it faster to fetch files again if those nodes are used.

<img />

As the cache on these nodes through, it will soon become bloated with too much data, similar to how your computer can get slow if it's loaded down with too many cache files. IPFS nodes have a way to handle this though, and that's through something called "Garbage Collection." This is a process where the IPFS node will dump any content in the cache that is not being pinned to IPFS. However if there is content in the cache that is still being pinned by at least one IPFS node, then it will stay. Pinning is what keeps content on IPFS, and as long as content is being pinned by one node, it will stay available on the network.

## How Do I Pin Files to IPFS?

With Pinata there are a few ways you can pin files to IPFS

### API & SDKs

If you're a developer that needs to build decentralized applications then you will likely want to use the [Pinata SDK](/sdk) or [Pinata API](/api-reference/endpoint/upload-a-file). These make it simple to upload files or raw JSON to IPFS!

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: "PINATA_JWT",
  pinataGateway: "example-gateway.mypinata.cloud",
});

const file = new File(["Hello IPFS!"], "hello-world.txt", { type: "text/plain" });
const upload = await pinata.upload.file(file);
```

<Note>
  Check out the [Quickstart](/quickstart) guide to upload your first file
  through the SDK!
</Note>

We also have other tools like the [Pinata CLI](/tools/cli/ipfs) or [Next.js Starter](/tools/nextjs-starter) which can be used to upload using [API Keys](/account-management/api-keys).

### Web App

If you're non-technical you can use [Pinata App](https://app.pinata.cloud/ipfs/files) to upload files, perfect if you just want to get started with NFTs and IPFS! It's as simple as clicking the "Add" button in the top right corner of the files page, selecting your file, give it a name, then upload. Once its complete, you'll see it listed in the files page!

<img />

<Note>
  Start uploading by [signing up for a free
  account](https://app.pinata.cloud/register)!
</Note>

### 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:

<img />

Or you can pin by CID with our API using the [Pin By CID](/api-reference/endpoint/ipfs/pin-by-cid) endpoint with a code snippet like this:

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: "PINATA_JWT",
  pinataGateway: "example-gateway.mypinata.cloud",
});

const pin = await pinata.upload.cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
```


# LLM Docs
Source: https://docs.pinata.cloud/llm-docs



The Pinata Docs have multiple ways they can be used in LLMs or AI empowered code editors like Cursor or Zed. Both of these options return raw text that can be pasted into the LLM or used in a prompt/docs feature for an editor.

## Smaller Context

If you are concerned about context tokens then you will want to use our hand crafted [AI Docs](https://ai-docs.pinata.cloud)

```bash theme={null}
curl https://ai-docs.pinata.cloud
```

<iframe title="YouTube video player" />

<br />

<Card title="How to Use Pinata with Cursor, Zed, and other LLMs" href="https://pinata.cloud/blog/how-to-use-pinata-with-cursor-zed-and-other-llms/" />

## Larger Context

Pinata's docs also offer larger contexts if you prefer it. The standard route will return URL routes which make it easy for LLMs to crawl.

```bash Routes theme={null}
curl https://docs.pinata.cloud/llms.txt
```

If you prefer to pull all of the documentation you can use the route below.

```bash Full Docs theme={null}
curl https://docs.pinata.cloud/llms-full.txt
```


# Quickstart
Source: https://docs.pinata.cloud/quickstart

Start uploading and retrieving content in no time

<img />

## Getting Started with Pinata

Whether you're brand new or a seasoned developer, Pinata makes it simple to store and retrieve content with speed and security. All you need to kick off your journey is a [free Pinata account](https://app.pinata.cloud/register)!

<CardGroup>
  <Card title="Next.Js" icon="https://mintlify.s3.us-west-1.amazonaws.com/pinata/%3Csvg%20width=%22800px%22%20height=%22800px%22%20class=%22h-6%20w-6%22%20viewBox=%220%200%20256%20256%22%20version=%221.1%22%20xmlns=%22http:/www.w3.org/2000/svg%22%20xmlns:xlink=%22http:/www.w3.org/1999/xlink%22%20preserveAspectRatio=%22xMidYMid%22%3E%3Cg%3E%3Cpath%20d=%22M119.616813,0.0688905149%20C119.066276,0.118932037%20117.314565,0.294077364%20115.738025,0.419181169%20C79.3775171,3.69690087%2045.3192571,23.3131775%2023.7481916,53.4631946%20C11.7364614,70.2271045%204.05395894,89.2428829%201.15112414,109.384595%20C0.12512219,116.415429%200,118.492153%200,128.025062%20C0,137.557972%200.12512219,139.634696%201.15112414,146.665529%20C8.10791789,194.730411%2042.3163245,235.11392%2088.7116325,250.076335%20C97.0197458,252.753556%20105.778299,254.580072%20115.738025,255.680985%20C119.616813,256.106338%20136.383187,256.106338%20140.261975,255.680985%20C157.453763,253.779407%20172.017986,249.525878%20186.382014,242.194795%20C188.584164,241.068861%20189.00958,240.768612%20188.709286,240.518404%20C188.509091,240.36828%20179.124927,227.782837%20167.86393,212.570214%20L147.393939,184.922273%20L121.743891,146.965779%20C107.630108,126.098464%2096.0187683,109.034305%2095.9186706,109.034305%20C95.8185728,109.009284%2095.7184751,125.873277%2095.6684262,146.465363%20C95.5933529,182.52028%2095.5683284,183.971484%2095.1178886,184.82219%20C94.4672532,186.048207%2093.9667644,186.548623%2092.915738,187.099079%20C92.114956,187.499411%2091.4142717,187.574474%2087.6355816,187.574474%20L83.3063539,187.574474%20L82.1552297,186.848872%20C81.4044966,186.373477%2080.8539589,185.747958%2080.4785924,185.022356%20L79.9530792,183.896422%20L80.0031281,133.729796%20L80.0782014,83.5381493%20L80.8539589,82.5623397%20C81.25435,82.0369037%2082.1051808,81.3613431%2082.7057674,81.0360732%20C83.7317693,80.535658%2084.1321603,80.4856165%2088.4613881,80.4856165%20C93.5663734,80.4856165%2094.4172043,80.6857826%2095.7434995,82.1369867%20C96.1188661,82.5373189%20110.007429,103.454675%20126.623656,128.650581%20C143.239883,153.846488%20165.962072,188.250034%20177.122972,205.139048%20L197.392766,235.839522%20L198.418768,235.163961%20C207.502639,229.259062%20217.112023,220.852086%20224.719453,212.09482%20C240.910264,193.504394%20251.345455,170.835585%20254.848876,146.665529%20C255.874878,139.634696%20256,137.557972%20256,128.025062%20C256,118.492153%20255.874878,116.415429%20254.848876,109.384595%20C247.892082,61.3197135%20213.683675,20.9362052%20167.288368,5.97379012%20C159.105376,3.32158945%20150.396872,1.49507389%20140.637341,0.394160408%20C138.234995,0.143952798%20121.693842,-0.131275573%20119.616813,0.0688905149%20L119.616813,0.0688905149%20Z%20M172.017986,77.4831252%20C173.219159,78.0836234%20174.195112,79.2345784%20174.545455,80.435575%20C174.74565,81.0861148%20174.795699,94.9976579%20174.74565,126.348671%20L174.670577,171.336%20L166.73783,159.17591%20L158.780059,147.01582%20L158.780059,114.313685%20C158.780059,93.1711423%20158.880156,81.2862808%20159.030303,80.7108033%20C159.430694,79.3096407%20160.306549,78.2087272%20161.507722,77.5581875%20C162.533724,77.0327515%20162.909091,76.98271%20166.837928,76.98271%20C170.541544,76.98271%20171.19218,77.0327515%20172.017986,77.4831252%20Z%22%3E%3C/path%3E%3C/g%3E%3C/svg%3E" href="/frameworks/next-js">
    Quickstart
  </Card>

  <Card
    title="Hono"
    icon="https://mintlify.s3.us-west-1.amazonaws.com/pinata/%3Csvg%20xmlns=%22http:/www.w3.org/2000/svg%22%20class=%22h-6%20w-6%22%20width=%2224.83%22%20height=%2232%22%20viewBox=%220%200%20256%20330%22%3E%3Cpath%20fill=%22#FF5B11&#x22; d=&#x22;M134.129.029q1.315-.17 2.319.662a1256 1256 0 0 1 69.573 93.427q24.141 36.346 41.082 76.862q27.055 72.162-28.16 125.564q-48.313 40.83-111.318 31.805q-75.312-15.355-102.373-87.133Q-1.796 217.85.614 193.51q4.014-41.896 19.878-80.838q6.61-15.888 17.228-29.154a382 382 0 0 1 16.565 21.203q3.66 3.825 7.62 7.289Q92.138 52.013 134.13.029&#x22; opacity=&#x22;.993&#x22;/><path fill=&#x22;#FF9758&#x22; d=&#x22;M129.49 53.7q36.47 42.3 65.93 90.114a187.3 187.3 0 0 1 15.24 33.13q12.507 49.206-26.836 81.169q-38.05 26.774-83.488 15.902q-48.999-15.205-56.653-65.929q-1.857-15.993 3.314-31.142a225.4 225.4 0 0 1 17.89-35.78l19.878-29.155a5510 5510 0 0 0 44.726-58.31&#x22;/></svg>
"
    href="/frameworks/hono"
  >
    Quickstart
  </Card>

  <Card
    title="React"
    icon="https://mintlify.s3.us-west-1.amazonaws.com/pinata/%3Csvg%20xmlns=%22http:/www.w3.org/2000/svg%22%20class=%22h-6%20w-6%22%20width=%2228%22%20height=%2232%22%20viewBox=%220%200%20256%20256%22%3E%3Cpath%20fill=%22#00D8FF&#x22; d=&#x22;M210.483 73.824a172 172 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171 171 0 0 0-6.375 5.848a156 156 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a171 171 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a146 146 0 0 0 6.921 2.165a168 168 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a146 146 0 0 0 5.342-4.923a168 168 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145 145 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844m-6.365 70.984q-2.102.694-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14m-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a157 157 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345q.785 3.162 1.386 6.193M87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a157 157 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a135 135 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94M50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a135 135 0 0 1-6.318-1.979m12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144 144 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160 160 0 0 1-1.76-7.887m110.427 27.268a348 348 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381 381 0 0 0-7.365-13.322m-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322 322 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18M82.802 87.83a323 323 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a322 322 0 0 0-7.848 12.897m8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321 321 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147m37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486m52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382 382 0 0 0 7.859-13.026a347 347 0 0 0 7.425-13.565m-16.898 8.101a359 359 0 0 1-12.281 19.815a329 329 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310 310 0 0 1-12.513-19.846h.001a307 307 0 0 1-10.923-20.627a310 310 0 0 1 10.89-20.637l-.001.001a307 307 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329 329 0 0 1 12.335 19.695a359 359 0 0 1 11.036 20.54a330 330 0 0 1-11 20.722m22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026q-.518 2.504-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a161 161 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3M128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86&#x22;/></svg>
"
    href="/frameworks/react"
  >
    Quickstart
  </Card>

  <Card
    title="Svelte"
    icon="https://mintlify.s3.us-west-1.amazonaws.com/pinata/%3Csvg%20class=%22h-6%20w-6%22%20xmlns=%22http:/www.w3.org/2000/svg%22%20width=%2226.6%22%20height=%2232%22%20viewBox=%220%200%20256%20308%22%3E%3Cpath%20fill=%22#FF3E00&#x22; d=&#x22;M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.2 82.2 0 0 0-37.135 55.056a86.57 86.57 0 0 0 8.536 55.576a82.4 82.4 0 0 0-12.296 30.719a87.6 87.6 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.18 82.18 0 0 0 37.135-55.057a86.6 86.6 0 0 0-8.53-55.577a82.4 82.4 0 0 0 12.29-30.718a87.57 87.57 0 0 0-14.963-66.244&#x22;/><path fill=&#x22;#FFF&#x22; d=&#x22;M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.7 52.7 0 0 1-9.003-39.85a50 50 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.5 92.5 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.07 16.07 0 0 0 2.89 10.656a17.14 17.14 0 0 0 18.397 6.828a15.8 15.8 0 0 0 4.403-1.935l71.67-45.672a14.92 14.92 0 0 0 6.734-9.977a15.92 15.92 0 0 0-2.713-12.011a17.16 17.16 0 0 0-18.404-6.832a15.8 15.8 0 0 0-4.396 1.933l-27.35 17.434a52.3 52.3 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.68 52.68 0 0 1-9.004-39.849a49.43 49.43 0 0 1 22.34-33.114l71.664-45.677a52.2 52.2 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.7 52.7 0 0 1 9.004 39.85a51 51 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.4 92.4 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.1 16.1 0 0 0-2.89-10.656a17.14 17.14 0 0 0-18.398-6.828a15.8 15.8 0 0 0-4.402 1.935l-71.67 45.674a14.9 14.9 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.16 17.16 0 0 0 18.404 6.832a15.8 15.8 0 0 0 4.402-1.935l27.345-17.427a52.2 52.2 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.68 52.68 0 0 1 9.003 39.848a49.45 49.45 0 0 1-22.34 33.12l-71.664 45.673a52.2 52.2 0 0 1-14.563 6.398&#x22;/></svg>
"
    href="/frameworks/sveltekit"
  >
    Quickstart
  </Card>

  <Card
    title="Astro"
    icon="https://mintlify.s3.us-west-1.amazonaws.com/pinata/%3Csvg%20class=%22h-7%20w-7%22%20xmlns=%22http:/www.w3.org/2000/svg%22%20width=%2232%22%20height=%2232%22%20viewBox=%220%200%2032%2032%22%3E%3Cpath%20fill=%22url(#vscodeIconsFileTypeAstro0)&#x22; d=&#x22;M11.025 20.499c-.532 1.75-.154 4.184 1.105 5.331v-.042l.042-.112c.154-.741.756-1.203 1.526-1.175c.713.014 1.12.392 1.217 1.217c.042.308.042.616.056.938v.098c0 .7.196 1.371.588 1.959c.35.56.84.993 1.497 1.287l-.028-.056l-.028-.112c-.49-1.469-.14-2.49 1.147-3.358l.392-.266l.868-.573a4.25 4.25 0 0 0 1.791-3.037c.07-.532 0-1.05-.154-1.553l-.21.14c-1.945 1.035-4.17 1.4-6.325.98c-1.301-.197-2.56-.56-3.498-1.652z&#x22;/><path fill=&#x22;#fff&#x22; d=&#x22;M4.925 20.191s3.736-1.82 7.486-1.82l2.84-8.759c.098-.42.406-.7.756-.7s.644.28.756.714l2.826 8.746c4.45 0 7.487 1.82 7.487 1.82L20.709 2.84c-.168-.518-.49-.84-.896-.84h-7.612c-.406 0-.7.322-.896.84z&#x22;/><defs><linearGradient id=&#x22;vscodeIconsFileTypeAstro0&#x22; x1=&#x22;8.19&#x22; x2=&#x22;16.91&#x22; y1=&#x22;23&#x22; y2=&#x22;18.89&#x22; gradientTransform=&#x22;translate(-.673 -2.198)scale(1.3993)&#x22; gradientUnits=&#x22;userSpaceOnUse&#x22;><stop offset=&#x22;0&#x22; stop-color=&#x22;#D83333&#x22;/><stop offset=&#x22;1&#x22; stop-color=&#x22;#F041FF&#x22;/></linearGradient></defs></svg>
"
    href="/frameworks/astro"
  >
    Quickstart
  </Card>

  <Card
    title="Remix"
    icon="https://mintlify.s3.us-west-1.amazonaws.com/pinata/%3Csvg%20class=%22h-6%20w-6%22%20xmlns=%22http:/www.w3.org/2000/svg%22%20width=%2232%22%20height=%2232%22%20viewBox=%220%200%2024%2024%22%3E%3Cpath%20fill=%22#888888&#x22; d=&#x22;M21.511 18.508c.216 2.773.216 4.073.216 5.492H15.31c0-.309.006-.592.011-.878c.018-.892.036-1.821-.109-3.698c-.19-2.747-1.374-3.358-3.55-3.358H1.574v-5H11.97c2.748 0 4.122-.835 4.122-3.049c0-1.946-1.374-3.125-4.122-3.125H1.573V0h11.541c6.221 0 9.313 2.938 9.313 7.632c0 3.511-2.176 5.8-5.114 6.182c2.48.497 3.93 1.909 4.198 4.694M1.573 24v-3.727h6.784c1.133 0 1.379.84 1.379 1.342V24Z&#x22;/></svg>
"
    href="/frameworks/remix"
  >
    Quickstart
  </Card>
</CardGroup>

### 1. Get API key and Gateway URL

<img />

Inside the [Pinata App](https://app.pinata.cloud) select "API Keys" from the sidebar, then click "New Key" in the top right. We would recommend starting with Admin privileges and unlimited uses to start. You will receive a `pinata_api_key`, `pinata_api_secret`, and a `JWT`. The JWT is the most common authentication method and what we'll be using below.

Next you will want to grab your Gateway domain by clicking the Gateways tab in the sidebar. You should see it listed in the format `fun-llama-300.mypinata.cloud` and you will want to copy it exactly like that.

### 2. Install and Setup SDK

In the root of your project run the install command with your package manager of choice.

<CodeGroup>
  ```bash npm theme={null}
  npm i pinata
  ```

  ```bash pnpm theme={null}
  pnpm i pinata
  ```

  ```bash yarn theme={null}
  yarn add pinata
  ```

  ```bash bun theme={null}
  bun i pinata
  ```
</CodeGroup>

Import and initialize the SDK in your codebase with the API key and Gateway from the previous step

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: "PINATA_JWT",
  pinataGateway: "example-gateway.mypinata.cloud",
});
```

<Note>
  The `PINATA_JWT` is a secret key, be sure to initialize the SDK in a secure environment and practice basic variable security practices. If you need to upload from a client environment, consider using signed JWTs
</Note>

### 3. Upload a File

Use the `upload` method to upload a File object.

<CodeGroup>
  ```typescript SDK theme={null}
  import { PinataSDK } from "pinata";

  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: "example-gateway.mypinata.cloud",
  });

  async function main() {
    try {
      const file = new File(["hello world!"], "hello.txt", { type: "text/plain" });
      const upload = await pinata.upload.public.file(file);
      console.log(upload);
    } catch (error) {
      console.log(error);
    }
  }

  await main();
  ```

  ```typescript 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.txt");
      const data = new FormData();
      data.append("file", file);
      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);
    }
  }
  ```
</CodeGroup>

You should get a response object like the one below

<CodeGroup>
  ```typescript SDK theme={null}
  {
    id: "0195f815-5c5e-716d-9240-d3ae380e2002",
    group_id: null,
    name: "hello.txt",
    cid: "bafkreidvbhs33ighmljlvr7zbv2ywwzcmp5adtf4kqvlly67cy56bdtmve",
    created_at: "2025-04-02T19:58:24.616Z",
    size: 12,
    number_of_files: 1,
    mime_type: "text/plain",
    vectorized: false,
    network: "public",
  }
  ```

  ```typescript API theme={null}
  {
    data: {
      id: "0195f815-5c5e-716d-9240-d3ae380e2002",
      group_id: null,
      name: "hello.txt",
      cid: "bafkreidvbhs33ighmljlvr7zbv2ywwzcmp5adtf4kqvlly67cy56bdtmve",
      created_at: "2025-04-02T19:58:24.616Z",
      size: 12,
      number_of_files: 1,
      mime_type: "text/plain",
      vectorized: false,
      network: "public",
    }
  }
  ```
</CodeGroup>

### 4. Retrieve a File through a Gateway

Use the `cid` of a file to fetch it through a Gateway directly or create a URL

<CodeGroup>
  ```typescript SDK theme={null}
  import { PinataSDK } from "pinata";

  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: "example-gateway.mypinata.cloud",
  });

  async function main() {
    try {
      const data = await pinata.gateways.public.get("bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq");
      console.log(data)

      const url = await pinata.gateways.convert(
        "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq"
      )
      console.log(url)
    } catch (error) {
      console.log(error);
    }
  }

  main();
  ```

  ```typescript API theme={null}
  const gatewayDomain = "example-llama-3000.mypinata.cloud"
  const cid = "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
  const url = `https://${gatewayDomain}/ipfs/${cid}`
  ```
</CodeGroup>

## What's Next?

Ready to see more of what Pinata has to offer? Here are some additional features and concepts to help you get the most out of our platform:

<CardGroup>
  <Card title="Groups" icon="cabinet-filing" href="https://docs.pinata.cloud/files/groups">
    With Groups, you can organize your files via the Pinata API or the web app. Create a Group, store your IPFS content, and fetch content quickly and easily.
  </Card>

  <Card title="Workspaces" icon="people-group" href="https://docs.pinata.cloud/account-management/workspaces">
    Workspaces allow you to add multiple team members to your Pinata account and collaborate seamlessly. Even if your team members don’t have a Pinata account, you can invite them easily. This feature is essential for efficient project collaboration and management.
  </Card>
</CardGroup>


# Config
Source: https://docs.pinata.cloud/sdk/config



Overview of the Private IPFS SDK Config

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});
```

## Parameters

### pinataJwt (Optional)

* Type: `string`

The Pinata API JWT key that authorizes the SDK. [Read more about API Keys](/account-management/api-keys).

### pinataGateway (Optional)

* Type: `string`

The domain of the Gateway included with your account. [Read more about Gateways](/gateways/retrieving-files).

### pinataGatewayKey (Optional)

* Type: `string`

Optional [Gateway Access Control](/gateways/gateway-access-controls) key to authorize requests outside your account.


# addSwap
Source: https://docs.pinata.cloud/sdk/files/private/add-swap

`org:files:write`

Swap a CID for another using the [Hot Swaps](/gateways/plugins/hot-swaps) gateway plugin

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const swap = await pinata.files.private.addSwap({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```

## Returns

```typescript theme={null}
export type SwapCidResponse = {
	mapped_cid: string;
	created_at: string;
};
```

## Parameters

Pass in the required parameters below to swap a CID

### cid

* Type: `string`

This would be the original CID that would be visted

```typescript {2} theme={null}
const swap = await pinata.files.private.addSwap({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```

### swapCid

* Type: `string`

This would be the CID you would want the old CID to point to

```typescript {3} theme={null}
const swap = await pinata.files.private.addSwap({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```


# delete
Source: https://docs.pinata.cloud/sdk/files/private/delete

`org:files:write`

Delte an array of files from your account

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const deletedFiles = await pinata.files.private.delete([
  "4ad9d3d1-4ab4-464c-a42a-3027fc39a546"
])
```

## Returns

```typescript theme={null}
type DeleteResponse[] = {
  id: string;
  status: string;
};
```

## Parameters

### files

* Type: `string[]`

An array of file IDs you want to delete

```typescript theme={null}
const unpin = await pinata.files.private
  .delete([
    "5e3011c0-f242-46b8-ad8d-2141bba23096",
    "e4cb100d-9065-4a08-80a3-f195f35de336"
  ])
```


# deleteVectors
Source: https://docs.pinata.cloud/sdk/files/private/delete-vectors

`org:files:write`

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

Delete vectors for a given `fileId`

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example.mypinata.cloud",
});

const update = await pinata.files.private
  .deleteVectors("52681e41-86f4-407b-8f79-33a7e7e5df68")
```

## Returns

```typescript theme={null}
type VectorizeFileResponse = {
	status: boolean;
};
```

## Parameters

### fileId

* Type: `string`

ID of the target file to delete vectors

```typescript {2} theme={null}
const update = await pinata.files.private
  .deleteVectors("52681e41-86f4-407b-8f79-33a7e7e5df68")
```


# getSwapHistory
Source: https://docs.pinata.cloud/sdk/files/private/get-swap-history

`org:files:read`

See the history of [Hot Swaps](/gateways/plugins/hot-swaps) on a Gateway domain for a specified CID.

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const history = await pinata.files.private.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})
```

## Returns

```typescript theme={null}
SwapCidResponse[]

export type SwapCidResponse = {
	mapped_cid: string;
	created_at: string;
};
```

## Parameters

Pass in the required parameters to get a swap history

### cid

* Type: `string`

The target CID for swap history

```typescript {2} theme={null}
const history = await pinata.files.private.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})
```

### domain

* Type: `string`

The Gateway domain that has the Hot Swaps plugin installed

```typescript {3} theme={null}
const history = await pinata.files.private.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})
```


# list
Source: https://docs.pinata.cloud/sdk/files/private/list

`org:files:read`

List and filter files pinned to your Pinata account

## Usage

```typescript 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.pivate.list()
```

## Returns

```typescript theme={null}
type FileListResponse = {
  files: FileListItem[];
  next_page_token: string;
};

type FileListItem = {
	id: string;
	name: string | null;
	cid: "pending" | string;
	size: number;
	numberOfFiles: number;
	mimeType: string;
	groupId: string;
	updatedAt: string;
	createdAt: string;
};
```

## Parameters

Filter response with the following additional methods. All filters are optional.

### name

* Type: `string`

Filter results based on name

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .name("pinnie")
```

### group

* Type: `string`

Filter results based on group ID

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .group("5b56981c-7e5b-4dff-aeca-de784728dddb")
```

### noGroup

* Type: `boolean`

Filter results to only show files that are not part of a group

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .noGroup(true)
```

### cid

* Type: `string`

Filter results based on CID

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .cid("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
```

### keyvalues

* Type: `Record<string, string>`

Filter results based on keyvalue pairs in metadata

```typescript {3-5} theme={null}
const files = await pinata.files.private
  .list()
  .keyvalues({
    env: "prod"
  })
```

### mimeType

* Type: `string`

Filter results based on mime type

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .mimeType("image/png")
```

### order

* Type: `"ASC" | "DESC"`

Order results either ascending or descending by created date

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .order("ASC")
```

### limit

* Type: `number`

Limit the number of results

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .limit(10)
```

### cidPending

* Type: `boolean`

Filters results and only returns files where `cid` is still `pending`

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .cid(true)
```

### pageToken

* Type: `string`

Paginates through files based on a provided page token

```typescript {3} theme={null}
const files = await pinata.files.private
  .list()
  .pageToken("MDE5MWIzZWYtM2U0Zi03YTY5LWE3OTQtOTRhZDE5NjQxMTk0")
```

## Auto Paginate

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.private.list()) {
  console.log(item.id);
}
```

Works like magic ✨


# queryVectors
Source: https://docs.pinata.cloud/sdk/files/private/query-vectors

`org:files:write`

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

Query file vectors for a given `groupId`

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example.mypinata.cloud",
});

const update = await pinata.files.private.queryVectors({
  groupId: "52681e41-86f4-407b-8f79-33a7e7e5df68",
  query: "Hello World"
})
```

## Returns

```typescript theme={null}
type VectorizeQueryResponse = {
	count: number;
	matches: VectorQueryMatch[];
};

type VectorQueryMatch = {
	file_id: string;
	cid: string;
	score: number;
};

// If using returnFile

type GetCIDResponse = {
  data?: JSON | string | Blob | null;
  contentType: ContentType;
};
```

## Parameters

### groupId

* Type: `string`

ID of the target group to query vectors

```typescript {2} theme={null}
const results = await pinata.files.private.queryVectors({
  groupId: "52681e41-86f4-407b-8f79-33a7e7e5df68",
  query: "Hello World"
})
```

### query

* Type: `string`

Query string

```typescript {3} theme={null}
const results = await pinata.files.private.queryVectors({
  groupId: "52681e41-86f4-407b-8f79-33a7e7e5df68",
  query: "Hello World"
})
```

### returnFile (Optional)

* Type: `boolean`

Return the top matched file itself if set to true

```typescript {4} theme={null}
const { data, contentType }  = await pinata.files.private.queryVectors({
  groupId: "52681e41-86f4-407b-8f79-33a7e7e5df68",
  query: "Hello World",
  returnFile: true
})
```


# update
Source: https://docs.pinata.cloud/sdk/files/private/update

`org:files:write`

Update information for an existing file

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example.mypinata.cloud",
});

const update = await pinata.files.private.update({
  id: "52681e41-86f4-407b-8f79-33a7e7e5df68",
  name: "New File Name",
})
```

## Returns

```typescript theme={null}
type FileListItem = {
	id: string;
	name: string | null;
	cid: "pending" | string;
	size: number;
	numberOfFiles: number;
	mimeType: string;
	groupId: string;
	updatedAt: string;
	createdAt: string;
};
```

## Parameters

### id

* Type: `string`

ID of the target file to update

```typescript {2} theme={null}
const update = await pinata.files.private.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  name: "New File Name",
})
```

### name (Optional)

* Type: `string`

Update the name of a file

```typescript {3} theme={null}
const update = await pinata.files.private.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  name: "New File Name"
})
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Update the keyvalues of a file

```typescript {3-5} theme={null}
const update = await pinata.files.private.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  keyvalues: {
    env: "prod"
  }
})
```

### expires\_at (Optional)

* Type: `number`

Set or update the expiration date for the file using a Unix timestamp. The timestamp must be in the future. Once the expiration time is reached, the file will be automatically deleted.

```typescript {3} theme={null}
const update = await pinata.files.private.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  expires_at: 1735689600
})
```


# vectorize
Source: https://docs.pinata.cloud/sdk/files/private/vectorize

`org:files:write`

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

Vectorize an existing file (must be part of a group)

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example.mypinata.cloud",
});

const update = await pinata.files.private
  .vectorize("52681e41-86f4-407b-8f79-33a7e7e5df68")
```

## Returns

```typescript theme={null}
type VectorizeFileResponse = {
	status: boolean;
};
```

## Parameters

### fileId

* Type: `string`

ID of the target file to vectorize

```typescript {2} theme={null}
const update = await pinata.files.private
  .vectorize("52681e41-86f4-407b-8f79-33a7e7e5df68")
```


# addSwap
Source: https://docs.pinata.cloud/sdk/files/public/add-swap

`org:files:write`

Swap a CID for another using the [Hot Swaps](/gateways/plugins/hot-swaps) gateway plugin

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
	pinataJwt: process.env.PINATA_JWT!,
	pinataGateway: "example-gateway.mypinata.cloud",
});

const swap = await pinata.files.public.addSwap({
	cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
	swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```

## Returns

```typescript theme={null}
export type SwapCidResponse = {
	mapped_cid: string;
	created_at: string;
};
```

## Parameters

Pass in the required parameters below to swap a CID

### cid

* Type: `string`

This would be the original CID that would be visted

```typescript {2} theme={null}
const swap = await pinata.files.public.addSwap({
	cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
	swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```

### swapCid

* Type: `string`

This would be the CID you would want the old CID to point to

```typescript {3} theme={null}
const swap = await pinata.files.public.addSwap({
	cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
	swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
})
```


# delete
Source: https://docs.pinata.cloud/sdk/files/public/delete

`org:files:write`

Delte an array of files from your account

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const deletedFiles = await pinata.files.public.delete([
  "4ad9d3d1-4ab4-464c-a42a-3027fc39a546"
])
```

## Returns

```typescript theme={null}
type DeleteResponse[] = {
  id: string;
  status: string;
};
```

## Parameters

### files

* Type: `string[]`

An array of file IDs you want to delete

```typescript theme={null}
const unpin = await pinata.files.public
  .delete([
    "5e3011c0-f242-46b8-ad8d-2141bba23096",
    "e4cb100d-9065-4a08-80a3-f195f35de336"
  ])
```


# deleteSwap
Source: https://docs.pinata.cloud/sdk/files/public/delete-swap

`org:files:write`

Remove a CID swap for [Hot Swaps](/gateways/plugins/hot-swaps) plugin

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const deleteSwap = await pinata.files.public.deleteSwap(
  "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske"
)
```

## Returns

```typescript theme={null}
OK
```

## Parameters

Pass in the required parameters below to remove a CID swap

### cid

* Type: `string`

This would be the original CID that was swapped

```typescript theme={null}
const deleteSwap = await pinata.files.public.deleteSwap(
  "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske"
)
```


# getSwapHistory
Source: https://docs.pinata.cloud/sdk/files/public/get-swap-history

`org:files:read`

See the history of [Hot Swaps](/gateways/plugins/hot-swaps) on a Gateway domain for a specified CID.

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const history = await pinata.files.public.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})
```

## Returns

```typescript theme={null}
SwapCidResponse[]

export type SwapCidResponse = {
	mapped_cid: string;
	created_at: string;
};
```

## Parameters

Pass in the required parameters to get a swap history

### cid

* Type: `string`

The target CID for swap history

```typescript {2} theme={null}
const history = await pinata.files.public.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})
```

### domain

* Type: `string`

The Gateway domain that has the Hot Swaps plugin installed

```typescript {3} theme={null}
const history = await pinata.files.getSwapHistory({
  cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  domain: "discordpinnie.mypinata.cloud"
})
```


# list
Source: https://docs.pinata.cloud/sdk/files/public/list

`org:files:read`

List and filter files pinned to your Pinata account

## Usage

```typescript 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()
```

## Returns

```typescript theme={null}
type FileListResponse = {
		files: FileListItem[];
		next_page_token: string;
};

type FileListItem = {
	id: string;
	name: string | null;
	cid: "pending" | string;
	size: number;
	numberOfFiles: number;
	mimeType: string;
	groupId: string;
	updatedAt: string;
	createdAt: string;
};
```

## Parameters

Filter response with the following additional methods. All filters are optional.

### name

* Type: `string`

Filter results based on name

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.name("pinnie")
```

### group

* Type: `string`

Filter results based on group ID

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.group("5b56981c-7e5b-4dff-aeca-de784728dddb")
```

### noGroup

* Type: `boolean`

Filter results to only show files that are not part of a group

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.noGroup(true)
```

### cid

* Type: `string`

Filter results based on CID

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.cid("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
```

### keyvalues

* Type: `Record<string, string>`

Filter results based on keyvalue pairs in metadata

```typescript {3-5} theme={null}
const files = await pinata.files.public
		.list()
		.keyvalues({
			env: "prod"
		})
```

### mimeType

* Type: `string`

Filter results based on mime type

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.mimeType("image/png")
```

### order

* Type: `"ASC" | "DESC"`

Order results either ascending or descending by created date

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.order("ASC")
```

### limit

* Type: `number`

Limit the number of results

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.limit(10)
```

### cidPending

* Type: `boolean`

Filters results and only returns files where `cid` is still `pending`

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.cid(true)
```

### pageToken

* Type: `string`

Paginates through files based on a provided page token

```typescript {3} theme={null}
const files = await pinata.files.public
		.list()
		.pageToken("MDE5MWIzZWYtM2U0Zi03YTY5LWE3OTQtOTRhZDE5NjQxMTk0")
```

## Auto Paginate

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.public.list()) {
		console.log(item.id);
}
```

Works like magic ✨


# queue
Source: https://docs.pinata.cloud/sdk/files/public/queue

`org:files:read`

List pin by CID requests in queue

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const queue = await pinata.files.public.queue().status("prechecking")
```

## Returns

```typescript theme={null}
export type PinQueueResponse = {
	rows: PinQueueItem[];
	next_page_token: string;
};

export type PinQueueItem = {
	id: string;
	cid: string;
	date_queued: string;
	name: string;
	status: string;
	keyvalues: any;
	host_nodes: string[];
	pin_policy: {
		regions: {
			id: string;
			desiredReplicationCount: number;
		}[];
		version: number;
	};
};
```

## Parameters

Filter results with the following methods. All filters are optional.

### cid

* Type: `string`

Filter by `cid`

```typescript theme={null}
const jobs = await pinata.files.public
  .queue()
  .cid('bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4')
```

### status

* Type: ` "prechecking" | "retrieving" | "expired" | "backfilled" | "over_free_limit" | "over_max_size" | "invalid_object" | "bad_host_node"`

Filter by current status

```typescript theme={null}
const jobs = await pinata.files.public
  .queue()
  .status("prechecking")
```

### sort

* Type: `"ASC" | "DSC"`

Order results by either ascending or descending by submission

```typescript theme={null}
const jobs = await pinata.files.public
  .queue()
  .sort("ASC")
```

### pageLimit

* Type: `number`

Limit the number of results. Default is `10`, max is `250`

```typescript theme={null}
const jobs = await pinata.files.public
  .queue()
  .pageLimit(100)
```

### pageToken

* Type: `string`

Paginate through results

```typescript theme={null}
const jobs = await pinata.files.public
  .queue()
  .pageToken("TOKEN")
```

## Auto Paginate

The `pinJobs` method has an auto pagination feature that is triggered when used inside a `for await` iterator

```typescript theme={null}
for await (const job of pinata.files.public.queue().status("expired") {
  console.log(job.cid);
}
```


# update
Source: https://docs.pinata.cloud/sdk/files/public/update

`org:files:write`

Update information for an existing file

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example.mypinata.cloud",
});

const update = await pinata.files.public.update({
  id: "52681e41-86f4-407b-8f79-33a7e7e5df68",
  name: "New File Name",
})
```

## Returns

```typescript theme={null}
type FileListItem = {
	id: string;
	name: string | null;
	cid: "pending" | string;
	size: number;
	numberOfFiles: number;
	mimeType: string;
	groupId: string;
	updatedAt: string;
	createdAt: string;
};
```

## Parameters

### id

* Type: `string`

ID of the target file to update

```typescript {2} theme={null}
const update = await pinata.files.public.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  name: "New File Name",
})
```

### name (Optional)

* Type: `string`

Update the name of a file

```typescript {3} theme={null}
const update = await pinata.files.public.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  name: "New File Name"
})
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Update the keyvalues of a file

```typescript {3-5} theme={null}
const update = await pinata.files.public.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  keyvalues: {
    env: "prod"
  }
})
```

### expires\_at (Optional)

* Type: `number`

Set or update the expiration date for the file using a Unix timestamp. The timestamp must be in the future. Once the expiration time is reached, the file will be automatically deleted.

```typescript {3} theme={null}
const update = await pinata.files.public.update({
  id: "8809812b-cd36-499f-b9b3-a37258a9cd6a",
  expires_at: 1735689600
})
```


# Getting Started
Source: https://docs.pinata.cloud/sdk/getting-started

Get up and running with the Pinata IPFS SDK

<img />

The IPFS SDK is an all-in-one tool for everything you might need, from uploading content, using Gateways, even user or group management!

## 1. Installation and Setup

Install with your package manager of choice

<CodeGroup>
  ```bash npm theme={null}
  npm i pinata
  ```

  ```bash pnpm theme={null}
  pnpm i pinata
  ```

  ```bash yarn theme={null}
  yarn add pinata
  ```

  ```bash bun theme={null}
  bun i pinata
  ```
</CodeGroup>

Import and initialize the SDK in your codebase with the following variables

* [Pinata API Key JWT](/account-management/api-keys)
* [Pinata Dedicated Gateway Domain](/gateways/retrieving-files)

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: "PINATA_JWT",
  pinataGateway: "example-gateway.mypinata.cloud",
});
```

<Note>The `PINATA_JWT` is a secret key, be sure to initialize the SDK in a secure environment and practice basic variable security practices. If you need to upload from a client environment, consider using signed JWTs</Note>

## 2. Upload a File

```typescript {11} theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

async function main() {
  try {
    const file = new File(["hello world!"], "hello.txt", { type: "text/plain" });
    const upload = await pinata.upload.public.file(file);
    console.log(upload);
  } catch (error) {
    console.log(error);
  }
}

await main();
```

This will return an object like the following

```typescript theme={null}
{
  id: "0195f815-5c5e-716d-9240-d3ae380e2002",
  group_id: null,
  name: "hello.txt",
  cid: "bafkreidvbhs33ighmljlvr7zbv2ywwzcmp5adtf4kqvlly67cy56bdtmve",
  created_at: "2025-04-02T19:58:24.616Z",
  size: 12,
  number_of_files: 1,
  mime_type: "text/plain",
  vectorized: false,
  network: "public",
}
```

## 3. Retrieve a File

Use the `cid` of a file to fetch it through a Gateway or create a URL to access it

```typescript {10,13} theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: "example-gateway.mypinata.cloud",
});

async function main() {
  try {
    const data = await pinata.gateways.public.get("bafkreibm6jg3ux5qumhcn2b3flc3tyu6dmlb4xa7u5bf44yegnrjhc4yeq");
    console.log(data)

    const url = await pinata.gateways.public.convert("bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq")
    console.log(url)

  } catch (error) {
    console.log(error);
  }
}

main();
```


# React Hooks
Source: https://docs.pinata.cloud/sdk/react

A collection of React hooks to use with Pinata

Included with the Pinata SDK is the `pinata/react` package which comes with a hook you can use to upload files and a convert helper function for converting CIDs into IPFS Gateway URLs.

## Installation

Install the Pinata SDK

```bash theme={null}
npm i pinata
```

## Usage

Import at the top of your desired page or component

```typescript theme={null}
import { useUpload, convert } from "pinata/react";
```

Inside your page or component use the hook to extract methods and state

```typescript theme={null}
const {
	upload, // Method to upload a file using a presigned URL
	progress, // Progress state as integer
	loading, // Boolean uploading state
	uploadResponse, // Either full Upload Response or just a CID if you use Resumable Uploads
	error, // Error state
	pause, // Pause upload method
	resume, // Resume upload method
	cancel, // Cancel current upload method
} = useUpload();
```

<Note>
  Keep in mind that states like `pause`, `resume`, or `progress` are only available on files over 100MB that automatically engage resumable uploads through TUS.
</Note>

Return types for `useUpload`

```typescript theme={null}
export type UseUploadReturn = {
	progress: number;
	loading: boolean;
	error: Error | null;
	uploadResponse: UploadResult | null;
	upload: (
		file: File,
		network: "public" | "private",
		url: string,
		options?: UploadOptions,
	) => Promise<void>;
	pause: () => void;
	resume: () => void;
	cancel: () => void;
};
```

To upload a file you must already have a server setup that returns a [Presigned URL](/files/presigned-urls). Then you can pass it into the `upload` method like so.

```typescript theme={null}
await upload(file, "public", "presigned_URL", {
  metadata: {
  	name: file.name || "Upload from React",
  	keyvalues: {
  		app: "React",
  		timestamp: Date.now().toString(),
  	},
  }
});
```

<Tip>
  [Learn how to send Presigned URLs from your server](/frameworks/hono)
</Tip>

Below are the available `UploadOptions` that can be passed into `upload`

```typescript theme={null}
export type UploadOptions = {
	metadata?: PinataMetadata;
	groupId?: string;
	chunkSize?: number; // Defaults to 1014 * 256 * 20 * 10 (~52MB)
};
```

Once a file is uploaded the `uploadResponse` will contain either the full upload response or just the CID if your file was over 100MB and utilized TUS resumable uploads.

The `convert` helper can be used to turn a CID into a URL like so:

```typescript theme={null}
const ipfsLink = await convert(uploadResponse.cid, "https://gateway.pinata.cloud");
// https://gateway.pinata.cloud/ipfs/CID
```

Below is a full example of implementing the `useUpload` hook with progress and abilities to pause and resume the upload.

```typescript theme={null}
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import pinataLogo from "/pinnie.png";
import "./App.css";
import { useUpload, convert } from "pinata/react";

function App() {
  const [file, setFile] = useState<File | null>(null);
  const [link, setLink] = useState("");

  const { upload, uploadResponse, loading, error, progress, pause, resume, cancel } = useUpload();

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      setFile(e.target.files[0]);
    }
  };

  const handleUpload = async () => {
    if (!file) return;

    const urlRes = await fetch(`${import.meta.env.VITE_SERVER_URL}/presigned_url`);
    const urlData = await urlRes.json();

    try {
      // Use the upload function from useUpload hook
      await upload(file, "public", urlData.url, {
        metadata: {
          name: file.name || "Upload from Web",
          keyvalues: {
            app: "Pinata Web Demo",
            timestamp: Date.now().toString(),
          },
        },
      });
    } catch (uploadError) {
      console.error("Upload error:", uploadError);
    }
  };

  // Set the link when upload is successful
  if (uploadResponse && !link) {
    async function setIpfsLink() {
      let ipfsLink: string = "";
      if (typeof uploadResponse === "string") {
        ipfsLink = await convert(uploadResponse, "https://gateway.pinata.cloud");
      } else if (uploadResponse) {
        ipfsLink = await convert(uploadResponse.cid, "https://gateway.pinata.cloud");
      }
      setLink(ipfsLink);
    }
    setIpfsLink();
  }

  // Get upload status message
  const getStatusMessage = () => {
    if (loading) return `Uploading file to Pinata...`;
    if (error) return `Upload error: ${error?.message || "Unknown error"}`;
    return "";
  };

  return (
    <>
      <div>
        <a href="https://vite.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
        <a href="https://pinata.cloud" target="_blank">
          <img src={pinataLogo} className="logo pinata" alt="Pinata logo" />
        </a>
      </div>
      <h1>Vite + React + Pinata</h1>
      <div className="card">
        <input type="file" onChange={handleFileChange} />
        <button onClick={handleUpload} disabled={!file || loading}>
          {loading ? "Uploading..." : "Upload to Pinata"}
        </button>

        {loading && (
          <div className="upload-status-container">
            <p>{getStatusMessage()}</p>

            <div className="upload-controls-container">
              {progress < 100 && (
                <>
                  <button onClick={pause} className="control-button pause">
                    Pause
                  </button>
                  <button onClick={resume} className="control-button resume">
                    Resume
                  </button>
                  <button onClick={cancel} className="control-button cancel">
                    Cancel
                  </button>
                </>
              )}
            </div>
          </div>
        )}

        {!loading && getStatusMessage() && <p>{getStatusMessage()}</p>}

        {link && (
          <div className="success-container">
            <p className="success-title">Upload Complete!</p>
            <a href={link} target="_blank" className="view-link">
              View File
            </a>
          </div>
        )}
      </div>
      <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
    </>
  );
}

export default App;
```

## Questions

Please [contact us](mailto:steve@pinata.cloud) if you have any issues!


# V2 Migration Guide
Source: https://docs.pinata.cloud/sdk/v2-migration-guide



The `pinata@1.x` package became a Private IPFS only SDK and the Public IPFS SDK was moved to `pinata-web3`. This was also a result of our APIs being disconnected as well. Due to this problem we've unified our Public and Private IPFS APIs in our V3 API, and we have also unified them in our SDK in a new V2 release. This document will walk you through how to migrate from previous versions of either `pinata` or `pinata-web3`.

## `pinata@1.x`

The `pinata` package was originally just for Private IPFS but now supports both Public and Private IPFS in `pinata@2.x`.

### API Keys

If you happened to be using a scoped API key for [legacy endpoints](/api-reference/endpoint/ipfs/pin-file-to-ipfs) like `pinFileToIPFS` or `pinJSONToIPFS` then you will need to create a new API key for the V2 SDK which uses the new [V3 endpoints](/api-reference/introduction). If you want to scope your endpoint for the new API you can use the [API docs for key creation](/api-reference/endpoint/v3-create-api-key) to do so.

### Installation and Initialization

Install the latest version by running the following command:

```bash theme={null}
npm i pinata@latest
```

Initalizing the SDK works the exact same way in previous versions

```typescript theme={null}
const pinata = new PinataSDK({
  pinataJwt: "PINATA_JWT",
  pinataGateway: "example-gateway.mypinata.cloud",
});
```

### Public & Private Methods

In `pinata@1.x` all methods were direct. In V2 there are public and private methods for the following classes:

* `upload`
* `files`
* `groups`
* `gateways`
* `signatures`

This means you can simple pass in `public` or `private` after the main class to access those specific networks.

<CodeGroup>
  ```typescript Before theme={null}
  const upload = await pinata.upload.file(file)
  ```

  ```typescript After theme={null}
  // Public IPFS
  const upload = await pinata.upload.public.file(file)
  // Private IPFS
  const upload = await pinata.upload.private.file(file)
  ```
</CodeGroup>

### Accessing Private IPFS Files

The method `createSignedURL` for accessing files on Private IPFS has been renamed to `createAccessLink`

<CodeGroup>
  ```typescript Before theme={null}
  const url = await pinata.gateways.createSignedURL({
  	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
  	expires: 30,
  });
  ```

  ```typescript After theme={null}
  const url = await pinata.gateways.private.createAccessLink({
  	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
  	expires: 30,
  });
  ```
</CodeGroup>

### Uploads with Metadata

When uploading files the `addMetadata()` method has been replaced with `name()` and `keyvalues()` to better match querying and listing files with those attributes.

<CodeGroup>
  ```typescript Before {4-7} theme={null}
  const upload = await pinata.upload
    .file(file)
    .addMetadata({
      name: "hello.txt",
      keyvalues: {
        env: "prod"
      }
    })
  ```

  ```typescript After {3-6} theme={null}
  const upload = await pinata.upload.public
    .file(file)
    .keyvalues({
      env: "prod"
    })
    .name("hello.txt")
  ```
</CodeGroup>

## `pinata-web3@0.x`

The `pinata-web3` package previously was only supporting Public IPFS, and lacked some of the more natural organization that the `pinata` package had. With `pinata@2.x` the SDK supprts both Public and Private IPFS and is recommended that you migrate to `pinata` instead of continuing to use `pinata-web3`. This guide will walk through the changes that will need to be made.

### API Keys

If you happened to be using a scoped API key for [legacy endpoints](/api-reference/endpoint/ipfs/pin-file-to-ipfs) like `pinFileToIPFS` or `pinJSONToIPFS` then you will need to create a new API key for the V2 SDK which uses the new [V3 endpoints](/api-reference/introduction). If you want to scope your endpoint for the new API you can use the [API docs for key creation](/api-reference/endpoint/v3-create-api-key) to do so.

### Installation and Initialization

Install the latest version by running the following command:

```bash theme={null}
npm i pinata@latest
```

Initalizing the SDK works the exact same way in previous versions, but you will need to change your imports from `pinata-web3` to `pinata`.

<CodeGroup>
  ```typescript Before theme={null}
  import { PinataSDK } from "pinata-web3"

  const pinata = new PinataSDK({
    pinataJwt: "PINATA_JWT",
    pinataGateway: "example-gateway.mypinata.cloud",
  });
  ```

  ```typescript After theme={null}
  import { PinataSDK } from "pinata" // Import changed

  const pinata = new PinataSDK({
    pinataJwt: "PINATA_JWT",
    pinataGateway: "example-gateway.mypinata.cloud",
  });
  ```
</CodeGroup>

### Public & Private Methods

In `pinata-web3@0.x` all methods were direct. In V2 there are public and private methods for the following classes:

* `upload`
* `files`
* `groups`
* `gateways`
* `signatures`

This means you can simple pass in `public` or `private` after the main class to access those specific networks.

<CodeGroup>
  ```typescript Before theme={null}
  const upload = await pinata.upload.file(file)
  ```

  ```typescript After theme={null}
  // Public IPFS
  const upload = await pinata.upload.public.file(file)
  // Private IPFS
  const upload = await pinata.upload.private.file(file)
  ```
</CodeGroup>

### File Management

In `pinata-web3` the methods to `listFiles()`, `updateMetadata()`, `unpin()` under the main PinataSDK class have been moved to a separate `files` class for better organization.

#### `listFiles()` -> `files.<network>.list()`

<CodeGroup>
  ```typescript Before theme={null}
  const files = await pinata.listFiles()
  ```

  ```typescript After theme={null}
  // Public IPFS
  const files = await pinata.files.public.list()
  // Private IPFS
  const files = await pinata.files.private.list()
  ```
</CodeGroup>

#### `updateMetadata()` -> `files.<network>.update()`

<CodeGroup>
  ```typescript Before theme={null}
  const update = await pinata.updateMedatadata({
    cid: "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4",
    name: "Pinnie V2",
    keyValues: {
      whimsey: 200
    }
  })
  ```

  ```typescript After theme={null}
  // Public IPFS
  const update = await pinata.files.public.update({
    id: "52681e41-86f4-407b-8f79-33a7e7e5df68",
    name: "New File Name",
    keyvalues: {
      env: "prod"
    }
  })
  // Private IPFS
  const update = await pinata.files.private.update({
    id: "52681e41-86f4-407b-8f79-33a7e7e5df68",
    name: "New File Name",
    keyvalues: {
      env: "prod"
    }
  })
  ```
</CodeGroup>

#### `unpin()` -> `files.<network>.delete()`

<CodeGroup>
  ```typescript Before theme={null}
  const unpin = await pinata.unpin([
    "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
  ])
  ```

  ```typescript After theme={null}
  // Public IPFS
  const deletedFiles = await pinata.files.public.delete([
    "4ad9d3d1-4ab4-464c-a42a-3027fc39a546"
  ])

  // Private IPFS
  const deletedFiles = await pinata.files.private.delete([
    "4ad9d3d1-4ab4-464c-a42a-3027fc39a546"
  ])
  ```
</CodeGroup>

#### `pinJobs()` -> `files.public.queue()`

To list you pending pin by CID queue you can use the `queue()` method.

<CodeGroup>
  ```typescript Before theme={null}
  const queue = await pinata.pinJobs().status("prechecking")
  ```

  ```typescript After theme={null}
  const queue = await pinata.files.public.queue().status("prechecking")
  ```
</CodeGroup>

### Swaps are now under Files

In `pinata-web3` the Hot Swap methods were under the `gateways` class. These have been moved to the `files` class instead. Some of the naming convention has been updated as well.

#### `swapCid()` -> `addSwap()`

<CodeGroup>
  ```typescript Before theme={null}
  const swap = await pinata.gateways.swapCid({
    cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
    swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
  })
  ```

  ```typescript After theme={null}
  const swap = await pinata.files.public.addSwap({
  	cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
  	swapCid: "bafkreihumyr3bgxulu45ghws33xokwjm5o7xnkkgakaz66ldtylwiecnhu"
  })
  ```
</CodeGroup>

#### `swapHistory()` -> `getSwapHistory()`

<CodeGroup>
  ```typescript Before theme={null}
  const history = await pinata.gateways.swapHistory({
    cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
    domain: "discordpinnie.mypinata.cloud"
  })
  ```

  ```typescript After theme={null}
  const history = await pinata.files.public.getSwapHistory({
    cid: "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske",
    domain: "discordpinnie.mypinata.cloud"
  })
  ```
</CodeGroup>

### Groups

Groups have been updated to follow the file ID paradigm that is now part of the SDK, and so has the method names for adding or removing files

#### `addCids()` -> `addFiles()`

<CodeGroup>
  ```typescript Before theme={null}
  const group = await pinata.groups.addCids({
  	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
  	cids: ["QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"],
  });
  ```

  ```typescript After theme={null}
  const group = await pinata.groups.public.addFiles({
  	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
  	files: [
  	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
  		"a606ef7e-70a0-40ad-9b8a-60563e009655"
  	],
  });
  ```
</CodeGroup>

<Tip>
  [See full reference](/sdk/groups/public)
</Tip>

### Analytics

In `pinata-web3` analytics was grouped under the `gateways` class. In `pinata@2.x` it has been moved into it's own class. Additionally the methods inside have been completely rewired to fit needs better.

<CodeGroup>
  ```typescript Before theme={null}
  const analytics = await pinata.gateways.topUsageAnalytics({
    domain: "docs.mypinata.cloud",
    start: "2024-08-01",
    end: "2024-08-15",
    sortBy: "requests",
    attribute: "cid"
  })
  .cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng");

  const analytics = await pinata.gateways.dateIntervalAnalytics({
    domain: "docs.mypinata.cloud",
    start: "2024-08-01",
    end: "2024-08-15",
    interval: "day"
  })
  .sort("desc");
  ```

  ```typescript After theme={null}
  const clicks = await pinata.analytics.requests
    .days(30)
    .limit(10)
    .cid("<CID>")

  const bandwidth = await pinata.analytics.bandwidth
    .days(30)
    .limit(10)
    .cid("<CID>")
  ```
</CodeGroup>

<Tip>
  See [Analytics Reference](/sdk/analytics/requests) for more info
</Tip>

### Usage Class Removed

The `pinata.usage` class in the `pinata-web3` package has been removed in `pinata@2.x`.


# Agents CLI
Source: https://docs.pinata.cloud/tools/cli/agents

Create, manage, and interact with AI agents from the command line

<Warning>
  The Agents feature is currently in beta.
</Warning>

The `pinata agents` commands let you manage your [Pinata Agents](/agents) directly from the terminal. Create agents, start chat sessions, manage skills and secrets, configure channels, and more.

<Card icon="rocket" href="/agents" title="New to Agents?">
  Learn about the Pinata Agents platform and get started with hosted AI agents.
</Card>

```
NAME:
   pinata agents - Interact with AI agents on Pinata

USAGE:
   pinata agents [command [command options]]

COMMANDS:
   list, l          List all agents
   create, c        Create a new agent
   get, g           Get agent details
   delete, d        Delete an agent
   restart, r       Restart an agent
   logs             Get agent logs
   chat, c          Interactive chat with an agent
   exec             Execute a command in an agent container
   skills, sk       Manage agent skills
   secrets, sec     Manage secrets
   channels, ch     Manage agent channels
   devices, dev     Manage agent devices
   snapshots, snap  Manage agent snapshots
   tasks, t         Manage agent cron jobs/tasks
   ports, p         Manage agent port forwarding
   domains, dom     Manage custom domains (beta)
   files            Agent file operations
   templates, tpl   Browse and manage agent templates
   clawhub, hub     Browse and install skills from ClawHub
   config, cfg      Manage agent configuration
   update, up       Manage agent openclaw updates
   versions, ver    List available agent versions
   engines          List the agent engines enabled for this deployment
   auth             Authenticate with a provider and store the credential as a secret
   feedback         Submit feedback or feature request

OPTIONS:
   --help, -h  show help
```

## `list`

```
NAME:
   pinata agents list - List all agents

USAGE:
   pinata agents list [options]

OPTIONS:
   --help, -h  show help
```

## `create`

```
NAME:
   pinata agents create - Create a new agent

USAGE:
   pinata agents create [options]

OPTIONS:
   --name string, -n string             Name of the agent (required)
   --description string, -d string      Agent personality description
   --vibe string                        Agent vibe/tagline
   --emoji string                       Agent emoji
   --engine string                      Container engine: openclaw (default) or hermes
   --skill string [ --skill string ]    Skill CIDs to attach (can be specified multiple times)
   --secret string [ --secret string ]  Secret IDs to attach (can be specified multiple times)
   --template string, -t string         Template ID to deploy from (uses template snapshot, skills, and defaults)
   --user-name string                   Owner display name (written into workspace/USER.md)
   --user-email string                  Owner email (written into workspace/USER.md)
   --channels string                    Channel config as JSON (for Hermes agents; set at creation to avoid a restart), e.g. '{"telegram":{"botToken":"...","dmPolicy":"open"}}'
   --help, -h                           show help
```

`--engine` selects the container engine. It defaults to `openclaw` server-side; pass `hermes` for the opinionated engine. Run [`pinata agents engines`](#engines) to see which engines your deployment allows. See [Concepts → Engine](/agents/concepts#engine) for the differences.

**Example with template:**

```bash theme={null}
# List available templates
pinata agents templates list

# Create agent from template
pinata agents create --name "My IPFS Agent" --template tpchchgg
```

**Example with a Hermes engine and channels:**

Pass `--channels` as a JSON object to configure Telegram, Slack, or Discord at creation for engines that support create-time channel bootstrap. This is especially useful for Hermes agents because the channel can come up with the agent and avoid a post-create restart. The per-channel fields (`botToken`, `appToken`, `dmPolicy`, `allowFrom`) match the [Channels API](/agents/api#channels) and the [`channels` block in `manifest.json`](/agents/manifest#channels).

```bash theme={null}
# Create a Hermes agent
pinata agents create --name "My Hermes Agent" --engine hermes

# Create a Hermes agent with Telegram configured at creation
pinata agents create \
  --name "My Hermes Agent" \
  --engine hermes \
  --user-name "Ada Lovelace" \
  --user-email "ada@example.com" \
  --channels '{"telegram":{"botToken":"123:abc","dmPolicy":"open"}}'
```

## `get`

```
NAME:
   pinata agents get - Get agent details

USAGE:
   pinata agents get [options] [agent ID]

OPTIONS:
   --help, -h  show help
```

The response includes the agent's `engine` so you can tell OpenClaw, Hermes, and other enabled engines apart.

## `delete`

```
NAME:
   pinata agents delete - Delete an agent

USAGE:
   pinata agents delete [options] [agent ID]

OPTIONS:
   --help, -h  show help
```

## `restart`

```
NAME:
   pinata agents restart - Restart an agent

USAGE:
   pinata agents restart [options] [agent ID]

OPTIONS:
   --help, -h  show help
```

## `logs`

```
NAME:
   pinata agents logs - Get agent logs

USAGE:
   pinata agents logs [options] [agent ID]

OPTIONS:
   --help, -h  show help
```

## `chat`

Start an interactive chat session with an agent. Supports multiple output modes for different use cases, and works across supported agent engines.

```
NAME:
   pinata agents chat - Interactive chat with an agent

USAGE:
   pinata agents chat [options] [agent ID] [optional prompt]

DESCRIPTION:
   Start an interactive chat session with an agent.

   The gateway URL and token are automatically fetched from the agent's configuration.

   Output modes:
     - TTY stdout:     Interactive TUI with markdown rendering
     - Non-TTY stdout: JSONL streaming (machine-readable, default for pipes)
     - --text:         Plain text streaming (simpler alternative to JSONL)
     - --conversation: Multi-turn mode (read messages from stdin line-by-line)

   Examples:
     # Interactive TUI mode
     pinata agents chat <agent-id>

     # Single message with plain text response (for agents/scripts)
     echo "Hello" | pinata agents chat <agent-id> --text

     # JSONL output (machine-readable, default when piped)
     echo "Hello" | pinata agents chat <agent-id>

     # Multi-turn conversation (each line is a message)
     echo -e "Hello\nHow are you?" | pinata agents chat <id> -C --text

     # Interactive conversation from a file
     pinata agents chat <id> --conversation --text < messages.txt

     # Filter JSONL with jq
     echo "hi" | pinata agents chat <id> | jq -c 'select(.type=="content_delta")'

OPTIONS:
   --model string      Model override
   --json              Force JSONL output (auto-enabled when stdout is not a TTY)
   --text              Force plain text output (simpler alternative to JSONL for pipes)
   --conversation, -C  Multi-turn conversation mode (read messages from stdin line-by-line)
   --session string    Session key for conversation context (default: agent:main:cli)
   --help, -h          show help
```

**Output Modes:**

| Mode                  | When                     | Use Case                      |
| --------------------- | ------------------------ | ----------------------------- |
| Interactive TUI       | TTY stdout               | Human users in terminal       |
| JSONL streaming       | Non-TTY stdout (default) | Machine parsing, other agents |
| Plain text (`--text`) | Explicit flag            | Simple scripts, easy piping   |
| Multi-turn (`-C`)     | Explicit flag            | Conversations with context    |

**Examples:**

```bash theme={null}
# Interactive TUI mode
pinata agents chat <agent-id>

# Single message with plain text response
echo "Hello" | pinata agents chat --text <agent-id>

# Multi-turn conversation (each line is a message)
echo -e "Hello\nWhat is 2+2?" | pinata agents chat --text -C <agent-id>

# JSONL output for machine parsing
echo "Hello" | pinata agents chat <agent-id>

# Filter JSONL to extract just the text
echo "Hello" | pinata agents chat <agent-id> | jq -r 'select(.type=="content_delta") | .delta.text' | tr -d '\n'
```

## `exec`

Execute a command inside an agent's container.

```
NAME:
   pinata agents exec - Execute a command in an agent container

USAGE:
   pinata agents exec [options] [agent ID] [command]

OPTIONS:
   --cwd string  Working directory for the command
   --help, -h    show help
```

**Examples:**

```bash theme={null}
# Run a simple command
pinata agents exec <agent-id> 'echo hello'

# List files in workspace
pinata agents exec <agent-id> 'ls -la /app'

# Run with specific working directory
pinata agents exec <agent-id> --cwd /app 'cat config.json'
```

## `files`

Read files from an agent's container.

```
NAME:
   pinata agents files - Agent file operations

USAGE:
   pinata agents files [command [command options]]

COMMANDS:
   read, r  Read a file from agent container

OPTIONS:
   --help, -h  show help
```

**Examples:**

```bash theme={null}
# Read a file from the agent
pinata agents files read <agent-id> /app/config.json
```

## `devices`

Manage device pairing for agents (used for mobile/external device connections).

```
NAME:
   pinata agents devices - Manage agent devices

USAGE:
   pinata agents devices [command [command options]]

COMMANDS:
   list, l      List pending and paired devices
   approve, a   Approve a device pairing request
   approve-all  Approve all pending device requests

OPTIONS:
   --help, -h  show help
```

**Examples:**

```bash theme={null}
# List devices for an agent
pinata agents devices list <agent-id>

# Approve a specific device request
pinata agents devices approve <agent-id> <request-id>

# Approve all pending requests
pinata agents devices approve-all <agent-id>
```

## `skills`

```
NAME:
   pinata agents skills - Manage agent skills

USAGE:
   pinata agents skills [command [command options]]

COMMANDS:
   list, l    List available skills in library
   create, c  Create a new skill
   delete, d  Delete a skill from library
   attach, a  Attach skills to an agent
   detach     Detach a skill from an agent

OPTIONS:
   --help, -h  show help
```

**`create` options:**

```
OPTIONS:
   --cid string                     Content ID of the skill (required)
   --name string, -n string         Skill name (required)
   --description string, -d string  Skill description
   --env string [ --env string ]    Required environment variable names
   --file-id string                 Pinata v3 file ID
   --help, -h                       show help
```

**Examples:**

```bash theme={null}
# List skills in your library
pinata agents skills list

# Create a skill from a CID
pinata agents skills create --cid <skill-cid> --name "My Skill" --env API_KEY

# Attach skills to an agent
pinata agents skills attach <agent-id> <skill-cid> <skill-cid>

# Detach a skill from an agent
pinata agents skills detach <agent-id> <skill-id>
```

## `secrets`

```
NAME:
   pinata agents secrets - Manage secrets

USAGE:
   pinata agents secrets [command [command options]]

COMMANDS:
   list, l    List all secrets
   create, c  Create a new secret
   update, u  Update a secret value
   delete, d  Delete a secret
   attach, a  Attach secrets to an agent
   detach     Detach a secret from an agent

OPTIONS:
   --help, -h  show help
```

**`create` options:**

```
OPTIONS:
   --name string, -n string   Secret name (e.g. ANTHROPIC_API_KEY)
   --value string, -v string  Secret value
   --help, -h                 show help
```

**Examples:**

```bash theme={null}
# Create a secret
pinata agents secrets create --name ANTHROPIC_API_KEY --value sk-ant-...

# Update a secret's value
pinata agents secrets update <secret-id> --value sk-ant-...

# Attach secrets to an agent
pinata agents secrets attach <agent-id> <secret-id> <secret-id>
```

## `channels`

```
NAME:
   pinata agents channels - Manage agent channels

USAGE:
   pinata agents channels [command [command options]]

COMMANDS:
   status, s     Get channel configuration status
   configure, c  Configure a channel (telegram, slack, discord, whatsapp)
   remove, r     Remove a channel configuration

OPTIONS:
   --help, -h  show help
```

**`configure` options:**

```
OPTIONS:
   --bot-token string                           Bot token
   --app-token string                           App token (Slack only)
   --dm-policy string                           DM policy: open or pairing
   --allow-from string [ --allow-from string ]  Allowed user IDs/phone numbers
   --enabled                                    Enable or disable the channel
   --skip-restart                               Skip restarting the agent after configuring
   --help, -h                                   show help
```

Pass `--skip-restart` to batch multiple channel changes and avoid a gateway restart per call; the new configuration takes effect on the next restart.

**Examples:**

```bash theme={null}
# Get channel status
pinata agents channels status <agent-id>

# Configure Telegram
pinata agents channels configure <agent-id> telegram --bot-token <token>

# Configure a channel without restarting the agent
pinata agents channels configure <agent-id> telegram --bot-token <token> --skip-restart

# Disable a channel without removing its configuration
pinata agents channels configure <agent-id> telegram --enabled=false

# Remove a channel
pinata agents channels remove <agent-id> telegram
```

## `snapshots`

```
NAME:
   pinata agents snapshots - Manage agent snapshots

USAGE:
   pinata agents snapshots [command [command options]]

COMMANDS:
   list, l    List agent snapshots
   create, c  Create a snapshot
   status, s  Get sync status
   reset, r   Reset to a snapshot

OPTIONS:
   --help, -h  show help
```

**Examples:**

```bash theme={null}
# List snapshots
pinata agents snapshots list <agent-id>

# Check sync status
pinata agents snapshots status <agent-id>

# Create a snapshot
pinata agents snapshots create <agent-id>

# Reset to a snapshot
pinata agents snapshots reset <agent-id> <snapshot-cid>
```

## `tasks`

```
NAME:
   pinata agents tasks - Manage agent cron jobs/tasks

USAGE:
   pinata agents tasks [command [command options]]

COMMANDS:
   list, l    List tasks
   create, c  Create a new task
   update, u  Update an existing task
   delete, d  Delete a task
   toggle     Enable or disable a task
   run        Run a task immediately
   history    View task run history

OPTIONS:
   --help, -h  show help
```

### `create`

```
NAME:
   pinata agents tasks create - Create a new task

USAGE:
   pinata agents tasks create [options] [agent ID]

DESCRIPTION:
   Create a new cron job for an agent.

   Schedule types:
     --at       Run once at a specific time (ISO 8601 format)
     --every    Run at intervals (e.g., "1h", "30m", "24h")
     --cron     Run on a cron schedule (e.g., "0 9 * * *")

   Payload types (choose one):
     --system-event  System event text (triggers heartbeat-style execution)
     --agent-turn    Agent turn message (triggers conversational response)

   Examples:
     # Run every hour with a system event
     pinata agents tasks create <agent-id> --name "hourly-check" --every 1h --system-event "Check for updates"

     # Run daily at 9am UTC with agent turn
     pinata agents tasks create <agent-id> --name "daily-report" --cron "0 9 * * *" --agent-turn "Generate daily report"

     # Run once at a specific time
     pinata agents tasks create <agent-id> --name "one-time" --at "2026-04-01T12:00:00Z" --system-event "Do task"

OPTIONS:
   --name string, -n string           Task name (required)
   --description string               Task description
   --disabled                         Create task in disabled state
   --at string                        Run once at this time (ISO 8601)
   --every string                     Run every interval (e.g., 1h, 30m)
   --cron string                      Cron expression (e.g., '0 9 * * *')
   --tz string                        Timezone for cron schedule
   --system-event string              System event payload text
   --agent-turn string                Agent turn message
   --model string                     Model override for agent turn
   --timeout int                      Timeout in seconds (default: 0)
   --session string                   Session target: main or isolated (default: "main")
   --skill string [ --skill string ]  Skill CIDs to make available to the task (can be specified multiple times)
   --help, -h                         show help
```

### `update`

```
NAME:
   pinata agents tasks update - Update an existing task

USAGE:
   pinata agents tasks update [options] [agent ID] [job ID]

DESCRIPTION:
   Update an existing cron job. Only specified fields are changed.

   Examples:
     # Change task name
     pinata agents tasks update <agent-id> <job-id> --name "new-name"

     # Update schedule to run every 2 hours
     pinata agents tasks update <agent-id> <job-id> --every 2h

     # Update payload message
     pinata agents tasks update <agent-id> <job-id> --system-event "New message"

OPTIONS:
   --name string, -n string           New task name
   --description string               New task description
   --at string                        Run once at this time (ISO 8601)
   --every string                     Run every interval (e.g., 1h, 30m)
   --cron string                      Cron expression (e.g., '0 9 * * *')
   --tz string                        Timezone for cron schedule
   --system-event string              System event payload text
   --agent-turn string                Agent turn message
   --model string                     Model override for agent turn
   --timeout int                      Timeout in seconds (default: 0)
   --skill string [ --skill string ]  Skill CIDs to make available to the task (can be specified multiple times)
   --help, -h                         show help
```

**Scoping skills to a task:**

`create` and `update` accept `--skill` (repeatable) to make specific skill CIDs available only while the task runs:

```bash theme={null}
# Create a task with task-scoped skills
pinata agents tasks create --name "nightly-report" --cron "0 0 * * *" \
  --agent-turn "Summarize today" --skill <skill-cid> --skill <skill-cid> <agent-id>

# Add or change task-scoped skills on an existing task
pinata agents tasks update <agent-id> <task-id> --skill <skill-cid>
```

## `templates`

Browse and manage agent templates. Includes browsing published templates and submitting your own from a GitHub/GitLab repository.

```
NAME:
   pinata agents templates - Browse and manage agent templates

USAGE:
   pinata agents templates [command [command options]]

COMMANDS:
   list, l      List available templates
   get, g       Get template details by slug
   mine         List templates you have submitted
   validate, v  Validate a git repo for template submission
   submit, s    Submit a new template from a git repo
   update, u    Update an existing template submission (re-pull from repo)
   delete, d    Archive a template submission
   branches     List branches for a git repository
   refs         List branches and tags (with the default branch) for a git repository
   search-refs  Search branches and tags by name for a git repository

OPTIONS:
   --help, -h  show help
```

**Browsing templates:**

```bash theme={null}
# List all templates
pinata agents templates list

# List featured templates only
pinata agents templates list --featured

# Filter by category
pinata agents templates list --category ai

# Get template details
pinata agents templates get ipfs-expert
```

**Submitting templates:**

Your git repo must contain a valid `manifest.json`, a `README.md`, and a `workspace/` directory. See [Creating Templates](/agents/templates#creating-templates) for the full specification.

`submit`, `update`, and `validate` share the `--ref` and `--path` flags:

```
OPTIONS:
   --ref string, -r string, --branch string, -b string  Git ref to submit from (e.g. refs/heads/main, refs/tags/v1.0.0, or a bare branch name; default: main)
   --path string, -p string                             Subdirectory within the repo to use as the template root (for monorepos)
   --name string                                        Override the template name from manifest.json
   --slug string                                        Override the template slug from manifest.json (lowercase, hyphens only)
   --help, -h                                           show help
```

<Note>
  `--branch`/`-b` are kept as aliases for `--ref`. You can pass a full ref (`refs/heads/main`, `refs/tags/v1.0.0`) or a bare branch name. Use `--path` to point at a subdirectory when your template lives inside a monorepo. `--name`/`--slug` are available on `submit` and `update` only.
</Note>

```bash theme={null}
# Validate your repo before submitting
pinata agents templates validate https://github.com/user/my-agent-template

# Validate a specific ref (branch or tag)
pinata agents templates validate https://github.com/user/my-agent-template --ref refs/tags/v1.0.0

# Validate a template that lives in a subdirectory (monorepo)
pinata agents templates validate https://github.com/user/monorepo --path packages/my-agent

# Submit the template
pinata agents templates submit https://github.com/user/my-agent-template

# Submit from a specific branch with name/slug overrides
pinata agents templates submit https://github.com/user/my-agent-template \
  --ref develop --name "My Agent" --slug my-agent
```

**Discovering refs:**

```bash theme={null}
# List branches for a repo
pinata agents templates branches https://github.com/user/my-agent-template

# List branches and tags (with the default branch)
pinata agents templates refs https://github.com/user/my-agent-template

# Search branches and tags by name
pinata agents templates search-refs https://github.com/user/my-agent-template v1
```

**Managing your submissions:**

```bash theme={null}
# List your submitted templates
pinata agents templates mine

# Update an existing submission (re-pull from repo)
pinata agents templates update <template-id>

# Update with a new repo URL or ref
pinata agents templates update <template-id> --git-url https://github.com/user/new-repo --ref main

# Archive a submission
pinata agents templates delete <template-id>
```

## `clawhub`

Browse and install skills from ClawHub, the community skills marketplace.

```
NAME:
   pinata agents clawhub - Browse and install skills from ClawHub

USAGE:
   pinata agents clawhub [command [command options]]

COMMANDS:
   list, l     Browse ClawHub skills
   get, g      Get ClawHub skill details by slug
   install, i  Install a ClawHub skill to your library

OPTIONS:
   --help, -h  show help
```

**`list` options:**

```
OPTIONS:
   --category string, -c string  Filter by category
   --sort string, -s string      Sort by: popular, newest, name
   --featured                    Show only featured skills
   --cursor string               Pagination cursor
   --help, -h                    show help
```

**Examples:**

```bash theme={null}
# Browse available skills
pinata agents clawhub list

# Browse featured skills, newest first
pinata agents clawhub list --featured --sort newest

# Get skill details by slug
pinata agents clawhub get clawdhub

# Install a skill to your library
pinata agents clawhub install <hub-skill-id>
```

## `domains`

Manage custom domains for your agents (beta feature).

```
NAME:
   pinata agents domains - Manage custom domains (beta)

USAGE:
   pinata agents domains [command [command options]]

COMMANDS:
   list, l    List custom domains
   add, a     Register a custom domain
   update, u  Update a custom domain
   delete, d  Remove a custom domain

OPTIONS:
   --help, -h  show help
```

**`add` options:**

```
OPTIONS:
   --subdomain string  Subdomain name (e.g., 'myapp' for myapp.apps.pinata.cloud)
   --domain string     Custom domain (e.g., 'api.example.com')
   --port int          Target container port
   --protected         Require authentication
   --help, -h          show help
```

**`update` options:**

```
OPTIONS:
   --subdomain string  New subdomain name
   --domain string     New custom domain
   --port int          New target port (default: 0)
   --protected         Enable authentication
   --no-protected      Disable authentication
   --help, -h          show help
```

**Examples:**

```bash theme={null}
# List domains for an agent
pinata agents domains list <agent-id>

# Add a subdomain (myapp.apps.pinata.cloud)
pinata agents domains add <agent-id> --subdomain myapp --port 8080

# Add a custom domain
pinata agents domains add <agent-id> --domain api.example.com --port 3000 --protected

# Update domain port
pinata agents domains update <agent-id> <domain-id> --port 8081

# Disable authentication on a domain
pinata agents domains update <agent-id> <domain-id> --no-protected

# Remove domain
pinata agents domains delete <agent-id> <domain-id>
```

## `ports`

Manage agent port forwarding rules.

```
NAME:
   pinata agents ports - Manage agent port forwarding

USAGE:
   pinata agents ports [command [command options]]

COMMANDS:
   list, l  List port forwarding rules
   set, s   Set port forwarding rules

OPTIONS:
   --help, -h  show help
```

**Examples:**

```bash theme={null}
# List current port mappings
pinata agents ports list <agent-id>

# Set port forwarding (port:pathPrefix format, up to 10 rules)
pinata agents ports set <agent-id> 8080:/api 3000:/app

# Clear all port forwarding rules
pinata agents ports set <agent-id>
```

## `config`

Manage agent openclaw configuration.

```
NAME:
   pinata agents config - Manage agent configuration

USAGE:
   pinata agents config [command [command options]]

COMMANDS:
   get, g       Get agent openclaw config
   set, s       Set agent openclaw config (JSON)
   validate, v  Validate agent openclaw config

OPTIONS:
   --help, -h  show help
```

**Examples:**

```bash theme={null}
# Read current config
pinata agents config get <agent-id>

# Validate config
pinata agents config validate <agent-id>

# Set config (JSON string)
pinata agents config set <agent-id> '{"key": "value"}'
```

## `update`

Manage openclaw version updates.

```
NAME:
   pinata agents update - Manage agent openclaw updates

USAGE:
   pinata agents update [command [command options]]

COMMANDS:
   check, c  Check for openclaw updates
   apply, a  Apply openclaw update

OPTIONS:
   --help, -h  show help
```

**`apply` options:**

```
OPTIONS:
   --tag string  Specific version or tag to install (e.g., 'latest', '0.3.0', 'beta')
   --help, -h    show help
```

**Examples:**

```bash theme={null}
# Check for updates
pinata agents update check <agent-id>

# Apply latest update
pinata agents update apply <agent-id>

# Apply specific version
pinata agents update apply <agent-id> --tag 2026.3.8
```

## `versions`

List available agent image versions.

```
NAME:
   pinata agents versions - List available agent versions

USAGE:
   pinata agents versions [options] [agent ID]

OPTIONS:
   --help, -h  show help
```

**Example:**

```bash theme={null}
pinata agents versions <agent-id>
```

For agents on an engine that publishes versions per engine (such as Hermes), the output includes `availableVersionsPerEngine` (grouped by `openclaw`/`hermes`) alongside the agent's current `engine` and version.

## `engines`

List the agent engines enabled for the current deployment. Use this to discover which values are valid for `agents create --engine`.

```
NAME:
   pinata agents engines - List the agent engines enabled for this deployment

USAGE:
   pinata agents engines [options]

OPTIONS:
   --help, -h  show help
```

**Example:**

```bash theme={null}
pinata agents engines
```

Returns the enabled engines, each with an `id` (the value you pass to `--engine`) and a human-readable `label`:

```json theme={null}
[
  { "id": "openclaw", "label": "OpenClaw" },
  { "id": "hermes", "label": "Hermes" }
]
```

## `auth`

Authenticate with AI providers and store credentials as secrets for your agents.

```
NAME:
   pinata agents auth - Authenticate with a provider and store the credential as a secret

USAGE:
   pinata agents auth [options] [provider: anthropic, openai, openrouter, venice]

OPTIONS:
   --oauth     Use OAuth browser flow instead of API key (openai only)
   --help, -h  show help
```

**Supported Providers:**

| Provider     | Secret Name                              | Options                        |
| ------------ | ---------------------------------------- | ------------------------------ |
| `anthropic`  | `ANTHROPIC_API_KEY`                      | -                              |
| `openai`     | `OPENAI_API_KEY` or `OPENAI_OAUTH_TOKEN` | `--oauth` for Codex OAuth flow |
| `openrouter` | `OPENROUTER_API_KEY`                     | -                              |
| `venice`     | `VENICE_API_KEY`                         | -                              |

**Examples:**

```bash theme={null}
# Store Anthropic API key
pinata agents auth anthropic

# Store OpenAI API key
pinata agents auth openai

# Authenticate with OpenAI via OAuth (for Codex CLI)
pinata agents auth openai --oauth

# Store OpenRouter API key
pinata agents auth openrouter

# Store Venice API key
pinata agents auth venice
```

## `feedback`

Submit feedback or a feature request from the terminal.

```
NAME:
   pinata agents feedback - Submit feedback or feature request

USAGE:
   pinata agents feedback [options] [message]

OPTIONS:
   --help, -h  show help
```

**Example:**

```bash theme={null}
pinata agents feedback "It would be great to..."
```


# IPFS
Source: https://docs.pinata.cloud/tools/cli/ipfs

Upload and manage files, groups, gateways, keys, and swaps

The Pinata CLI is equipped with the majority of features on both the Public IPFS API and Private IPFS API.

## `upload`

```
NAME:
   pinata upload - Upload a file to Pinata

USAGE:
   pinata upload [options] [path to file]

OPTIONS:
   --group string, -g string       Upload a file to a specific group by passing in the groupId
   --name string, -n string        Add a name for the file you are uploading. By default it will use the filename on your system. (default: "nil")
   --verbose                        Show upload progress
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                       show help
```

## `files`

```
NAME:
   pinata files - Interact with your files on Pinata

USAGE:
   pinata files [command [command options]]

COMMANDS:
   delete, d  Delete a file by ID
   get, g     Get file info by ID
   update, u  Update a file by ID
   list, l    List most recent files

OPTIONS:
   --help, -h  show help
```

### `get`

```
NAME:
   pinata files get - Get file info by ID

USAGE:
   pinata files get [options] [ID of file]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `list`

```
NAME:
   pinata files list - List most recent files

USAGE:
   pinata files list [options]

OPTIONS:
   --name string, -n string                                             Filter by name of the target file
   --cid string, -c string                                              Filter results by CID
   --group string, -g string                                            Filter results by group ID
   --mime string, -m string                                             Filter results by file mime type
   --amount string, -a string                                           The number of files you would like to return
   --token string, -t string                                            Paginate through file results using the pageToken
   --cidPending                                                         Filter results based on whether or not the CID is pending
   --keyvalues string, --kv string [ --keyvalues string, --kv string ]  Filter results by metadata keyvalues (format: key=value)
   --network string, --net string                                       Specify the network (public or private). Uses default if not specified
   --help, -h                                                           show help
```

### `update`

```
NAME:
   pinata files update - Update a file by ID

USAGE:
   pinata files update [options] [ID of file]

OPTIONS:
   --name string, -n string        Update the name of a file
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `delete`

```
NAME:
   pinata files delete - Delete a file by ID

USAGE:
   pinata files delete [options] [ID of file]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

## `groups`

```
NAME:
   pinata groups - Interact with file groups

USAGE:
   pinata groups [command [command options]]

COMMANDS:
   create, c  Create a new group
   list, l    List groups on your account
   update, u  Update a group
   delete, d  Delete a group by ID
   get, g     Get group info by ID
   add, a     Add a file to a group
   remove, r  Remove a file from a group

OPTIONS:
   --help, -h  show help
```

### `create`

```
NAME:
   pinata groups create - Create a new group

USAGE:
   pinata groups create [options] [name of group]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `get`

```
NAME:
   pinata groups get - Get group info by ID

USAGE:
   pinata groups get [options] [ID of group]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `list`

```
NAME:
   pinata groups list - List groups on your account

USAGE:
   pinata groups list [options]

OPTIONS:
   --amount string, -a string      The number of groups you would like to return (default: "10")
   --name string, -n string        Filter groups by name
   --token string, -t string       Paginate through results using the pageToken
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `update`

```
NAME:
   pinata groups update - Update a group

USAGE:
   pinata groups update [options] [ID of group]

OPTIONS:
   --name string, -n string        Update the name of a group
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `add`

```
NAME:
   pinata groups add - Add a file to a group

USAGE:
   pinata groups add [options] [group id] [file id]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `remove`

```
NAME:
   pinata groups remove - Remove a file from a group

USAGE:
   pinata groups remove [options] [group id] [file id]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

## `gateways`

```
NAME:
   pinata gateways - Interact with your gateways on Pinata

USAGE:
   pinata gateways [command [command options]]

COMMANDS:
   set, s   Set your default gateway to be used by the CLI
   open, o  Open a file in the browser
   link, l  Get either an IPFS link for a public file or a temporary access link for a Private IPFS file

OPTIONS:
   --help, -h  show help
```

### `set`

<Tip>
  Use the command with no arguments and get a list of your gateways to choose from!
</Tip>

```
NAME:
   pinata gateways set - Set your default gateway to be used by the CLI

USAGE:
   pinata gateways set [options] [domain of the gateway]

OPTIONS:
   --help, -h  show help
```

### `link`

```
NAME:
   pinata gateways link - Get either an IPFS link for a public file or a temporary access link for a Private IPFS file

USAGE:
   pinata gateways link [options] [cid of the file, seconds the url is valid for]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `open`

```
NAME:
   pinata gateways open - Open a file in the browser

USAGE:
   pinata gateways open [options] [CID of the file]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

## `keys`

```
NAME:
   pinata keys - Create and manage generated API keys

USAGE:
   pinata keys [command [command options]]

COMMANDS:
   create, c  Create an API key with admin or scoped permissions
   list, l    List and filter API key
   revoke, r  Revoke an API key

OPTIONS:
   --help, -h  show help
```

### `create`

```
NAME:
   pinata keys create - Create an API key with admin or scoped permissions

USAGE:
   pinata keys create [options]

OPTIONS:
   --name string, -n string                                         Name of the API key
   --admin, -a                                                      Set the key as Admin
   --uses int, -u int                                               Max uses a key can use (default: 0)
   --endpoints string, -e string [ --endpoints string, -e string ]  Optional array of endpoints the key is allowed to use
   --help, -h                                                       show help
```

### `list`

```
NAME:
   pinata keys list - List and filter API key

USAGE:
   pinata keys list [options]

OPTIONS:
   --name string, -n string    Name of the API key
   --revoked, -r               Set the key as Admin
   --exhausted, -e             Filter keys that are exhausted or not
   --uses, -u                  Filter keys that do or don't have limited uses
   --offset string, -o string  Offset the number of results to paginate
   --help, -h                  show help
```

### `revoke`

```
NAME:
   pinata keys revoke - Revoke an API key

USAGE:
   pinata keys revoke [options] [key]

OPTIONS:
   --help, -h  show help
```

## `swaps`

```
NAME:
   pinata swaps - Interact and manage hot swaps on Pinata

USAGE:
   pinata swaps [command [command options]]

COMMANDS:
   list, l    List swaps for a given gateway domain or for your config gateway domain
   add, a     Add a swap for a CID
   delete, d  Remeove a swap for a CID

OPTIONS:
   --help, -h  show help
```

### `list`

```
NAME:
   pinata swaps list - List swaps for a given gateway domain or for your config gateway domain

USAGE:
   pinata swaps list [options] [cid] [optional gateway domain]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `add`

```
NAME:
   pinata swaps add - Add a swap for a CID

USAGE:
   pinata swaps add [options] [cid] [swap cid]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```

### `delete`

```
NAME:
   pinata swaps delete - Remeove a swap for a CID

USAGE:
   pinata swaps delete [options] [cid]

OPTIONS:
   --network string, --net string  Specify the network (public or private). Uses default if not specified
   --help, -h                      show help
```


# CLI Overview
Source: https://docs.pinata.cloud/tools/cli/overview

The official CLI for Pinata written in Go

![cover](https://dweb.mypinata.cloud/ipfs/QmNcdx9t48z7RQUXUZZHmuc4zBfyBxKLjDfEgmfhiop7j7?img-format=webp)

The Pinata CLI is a tool you can use alongside your account to upload and manage files through your terminal. It also provides access to Pinata's [Agents platform (beta)](/tools/cli/agents), allowing you to create, manage, and interact with hosted agents directly from the command line. The source code can be found in the link below.

<Card icon="github" href="https://github.com/PinataCloud/ipfs-cli" title="Source Code" />

## Installation

<Note>
  If you are on Windows please use WSL when installing. If you get an error that it was not able to resolve the github host run `git config --global --unset http.proxy`
</Note>

### Install Script

The easiest way to install is to copy and paste this script into your terminal

```bash theme={null}
curl -fsSL https://cli.pinata.cloud/install | bash
```

### Homebrew

If you are on MacOS and have homebrew installed you can run the command below to install the CLI

```
brew install PinataCloud/ipfs-cli/ipfs-cli
```

### Building from Source

To build and install from source make sure you have [Go](https://go.dev/) installed on your computer and the following command returns a version:

```
go version
```

Then paste and run the following into your terminal:

```
git clone https://github.com/PinataCloud/ipfs-cli && cd ipfs-cli && go install .
```

### Linux Binary

As versions are released you can visit the [Releases](https://github.com/PinataCloud/ipfs-cli/releases) page and download the appropriate binary for your system, them move it into your bin folder.

For example, this is how I install the CLI for my Raspberry Pi

```
wget https://github.com/PinataCloud/ipfs-cli/releases/download/v0.1.0/ipfs-cli_Linux_arm64.tar.gz

tar -xzf ipfs-cli_Linux_arm64.tar.gz

sudo mv pinata /usr/bin
```

## Authentication

With the CLI installed you will first need to authenticate it with your [Pinata JWT](https://docs.pinata.cloud/account-management/api-keys). Run this command and follow the steps to setup the CLI!

```
pinata auth
```

## Configuration

Set a default IPFS network, can be either `public` or `private`. You can always change this at any time or override in individual commands. If none is set the default is `public`.

```
NAME:
   pinata config - Configure Pinata CLI settings

USAGE:
   pinata config [command [command options]]

COMMANDS:
   network, net  Set default network (public or private)

OPTIONS:
   --help, -h  show help
```

## Next Steps

<CardGroup>
  <Card title="IPFS" icon="cube" href="/tools/cli/ipfs">
    Upload files, manage groups, gateways, keys, and swaps
  </Card>

  <Card title="Agents CLI" icon="robot" href="/tools/cli/agents">
    Manage your Pinata Agents from the terminal (beta)
  </Card>
</CardGroup>

## Contact

If you have any questions please feel free to reach out to us!

[team@pinata.cloud](mailto:team@pinata.cloud)


# Community SDKs
Source: https://docs.pinata.cloud/tools/community-sdks



## IPFS Gateway Tools SDK

This is a convenience SDK that allows developers to take an IPFS URL of any kind and convert it to a Gateway URL of their choosing. This is especially useful when you want to load IPFS content through your own [Dedicated Gateway](/gateways/dedicated-ipfs-gateways).

[https://www.npmjs.com/package/@pinata/ipfs-gateway-tool](https://www.npmjs.com/package/@pinata/ipfs-gateway-tool)

## Community SDKs

These SDKs have been written by members of the Pinata community. They have not been audited by anyone at Pinata, nor can we vouch for their security or viability.

### Python

[https://github.com/Vourhey/pinatapy](https://github.com/Vourhey/pinatapy)

[https://github.com/edmondyu/pinata-python-util](https://github.com/edmondyu/pinata-python-util)

### Go

[https://gitlab.com/benoit74/pinata-cli](https://gitlab.com/benoit74/pinata-cli)

### Rust

[https://github.com/perfectmak/pinata-sdk](https://github.com/perfectmak/pinata-sdk)

### .NET

[https://www.nuget.org/packages/Pinata.Client/](https://www.nuget.org/packages/Pinata.Client/)

### Elixir

[https://github.com/m1ome/ex\_pinata](https://github.com/m1ome/ex_pinata)


# Quickstart
Source: https://docs.pinata.cloud/tools/erc-8004/quickstart

Register your first AI agent on-chain using ERC-8004 and Pinata

## What is ERC-8004?

ERC-8004 is an open standard for AI agent identity and trust. It gives your agent:

* **A portable identity** — Your agent exists as an NFT on Ethereum/L2s. It's yours, not tied to any platform.
* **Discoverable endpoints** — Other agents and applications can find your MCP server, A2A endpoint, wallet, and more.
* **Composable reputation** — Feedback from clients builds a track record that follows your agent everywhere.

### The Three Registries

| Registry       | Purpose                                                 |
| -------------- | ------------------------------------------------------- |
| **Identity**   | Stores who agents are and how to reach them             |
| **Reputation** | Records feedback and ratings from clients               |
| **Validation** | Provides independent verification for high-stakes tasks |

This guide focuses on the **Identity Registry** — registering your agent so it can be discovered.

## Quick Start

For example agent cards and ready-to-run scripts, check out [PinataCloud/erc8004](https://github.com/PinataCloud/erc8004). For an interactive experience, try the [wizard](https://github.com/PinataCloud/erc8004/tree/main/wizard):

```bash theme={null}
npx @pinata/erc8004-wizard
```

## Prerequisites

1. **Pinata account** — Sign up at [pinata.cloud](https://pinata.cloud), create an [API key](/account-management/api-keys) and a [gateway](/gateways/dedicated-ipfs-gateways)
2. **Native tokens for gas** — Get free testnet tokens from faucets (links in table below)
3. **Wallet private key** — The wallet that will own your agent NFT

## Supported Networks

ERC-8004 is deployed on multiple chains with consistent addresses:

| Network          | Chain ID | Identity Registry                            | Faucet                                                      |
| ---------------- | -------- | -------------------------------------------- | ----------------------------------------------------------- |
| Ethereum Mainnet | 1        | `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` | —                                                           |
| Ethereum Sepolia | 11155111 | `0x8004A818BFB912233c491871b3d84c89A494BD9e` | [Alchemy](https://www.alchemy.com/faucets/ethereum-sepolia) |
| Base Mainnet     | 8453     | `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` | —                                                           |
| Base Sepolia     | 84532    | `0x8004A818BFB912233c491871b3d84c89A494BD9e` | [Alchemy](https://www.alchemy.com/faucets/base-sepolia)     |
| Polygon Mainnet  | 137      | `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` | —                                                           |
| Polygon Amoy     | 80002    | `0x8004A818BFB912233c491871b3d84c89A494BD9e` | [Polygon](https://faucet.polygon.technology/)               |
| Monad Mainnet    | 143      | `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` | —                                                           |
| Monad Testnet    | 10143    | `0x8004A818BFB912233c491871b3d84c89A494BD9e` | [Monad](https://faucet.monad.xyz)                           |
| BSC Mainnet      | 56       | `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432` | —                                                           |
| BSC Testnet      | 97       | `0x8004A818BFB912233c491871b3d84c89A494BD9e` | [BNB Chain](https://www.bnbchain.org/en/testnet-faucet)     |

<Note>
  All mainnets share the same address: `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432`

  All testnets share the same address: `0x8004A818BFB912233c491871b3d84c89A494BD9e`
</Note>

## Using the Scripts

Clone the repo and install dependencies:

```bash theme={null}
git clone https://github.com/PinataCloud/erc8004
cd erc8004
bun install
cp .env.example .env  # fill in your values
```

### Available Scripts

```bash theme={null}
bun run register <agent-card.json>              # Register new agent
bun run register <agent-card.json> --update 3   # Update agent #3
bun run verify <agent-id>                       # Verify registration
bun run check-registration <agent-id>           # Check registrations[] matches on-chain
bun run set-wallet <agent-id>                   # Set payment wallet
bun run unset-wallet <agent-id>                 # Clear payment wallet
```

### Network Selection

By default, scripts use **Ethereum Sepolia**. To use a different network:

```bash theme={null}
# Via environment variable
NETWORK=base-mainnet bun run register my-agent.json

# Via CLI flag
bun run register my-agent.json --network base-mainnet
bun run verify 1 --network polygon-mainnet
```

## Registration Flow

Registration has four steps:

1. **Register on-chain** (without URI) → Get your `agentId`
2. **Build agent card** with `registrations[]` containing your new `agentId`
3. **Upload to IPFS** via Pinata
4. **Set agent URI** on-chain to point to your IPFS content

### Code Example

```typescript theme={null}
import { PinataSDK } from "pinata"
import { createPublicClient, createWalletClient, http, parseAbi, parseEventLogs } from "viem"
import { privateKeyToAccount } from "viem/accounts"
import { sepolia } from "viem/chains"

const REGISTRY = "0x8004A818BFB912233c491871b3d84c89A494BD9e"
const AGENT_REGISTRY = `eip155:${sepolia.id}:${REGISTRY}`

const abi = parseAbi([
  "function register() external returns (uint256 agentId)",
  "function setAgentURI(uint256 agentId, string newURI) external",
  "event Registered(uint256 indexed agentId, string agentURI, address indexed owner)",
])

async function registerAgent(name: string, description: string) {
  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
  const publicClient = createPublicClient({ chain: sepolia, transport: http() })
  const walletClient = createWalletClient({ account, chain: sepolia, transport: http() })
  const pinata = new PinataSDK({
    pinataJwt: process.env.PINATA_JWT!,
    pinataGateway: process.env.PINATA_GATEWAY!,
  })

  // Step 1: Register on-chain to get agent ID
  const registerHash = await walletClient.writeContract({
    address: REGISTRY,
    abi,
    functionName: "register",
    args: [],
  })
  const receipt = await publicClient.waitForTransactionReceipt({ hash: registerHash })
  const logs = parseEventLogs({ abi, logs: receipt.logs, eventName: "Registered" })
  const agentId = logs[0].args.agentId

  // Step 2: Build agent card with registrations
  const agentCard = {
    type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
    name,
    description,
    image: "ipfs://bafkreiaims435hmzeg3l6ixlrlvnei7wept5kmfd6c2ncz3ucl466xhucu",
    services: [],
    registrations: [{ agentId: Number(agentId), agentRegistry: AGENT_REGISTRY }],
    supportedTrust: ["reputation"],
  }

  // Step 3: Upload to Pinata
  const blob = new Blob([JSON.stringify(agentCard, null, 2)], { type: "application/json" })
  const file = new File([blob], `agent-${agentId}.json`, { type: "application/json" })
  const uploadResult = await pinata.upload.public.file(file)
  const tokenUri = `ipfs://${uploadResult.cid}`

  // Step 4: Set agent URI on-chain
  const setUriHash = await walletClient.writeContract({
    address: REGISTRY,
    abi,
    functionName: "setAgentURI",
    args: [agentId, tokenUri],
  })
  await publicClient.waitForTransactionReceipt({ hash: setUriHash })

  return { agentId, tokenUri }
}
```

## Agent Card Schema

| Field            | Required | Description                                                       |
| ---------------- | -------- | ----------------------------------------------------------------- |
| `type`           | Yes      | Always `https://eips.ethereum.org/EIPS/eip-8004#registration-v1`  |
| `name`           | Yes      | Your agent's display name                                         |
| `description`    | Yes      | What it does, capabilities, pricing                               |
| `image`          | Yes      | Avatar URL (IPFS or HTTPS)                                        |
| `services`       | No       | Agent endpoints (see [Services & IPFS](/tools/erc-8004/services)) |
| `registrations`  | No       | Registry references (populated after registration)                |
| `x402Support`    | No       | Accepts [x402 payments](/files/x402/intro) (default: false)       |
| `active`         | No       | Currently running (default: true)                                 |
| `supportedTrust` | No       | Trust models (see below)                                          |

### Trust Models

The `supportedTrust` array declares how clients can verify your agent:

| Model             | Description                                       |
| ----------------- | ------------------------------------------------- |
| `reputation`      | Trust based on historical feedback from clients   |
| `crypto-economic` | Trust backed by staked tokens that can be slashed |
| `tee-attestation` | Trust verified by Trusted Execution Environment   |

Most agents use `["reputation"]`.

### Example Agent Card

```json theme={null}
{
  "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
  "name": "My Agent",
  "description": "A helpful AI assistant.",
  "image": "ipfs://bafkreiaims435hmzeg3l6ixlrlvnei7wept5kmfd6c2ncz3ucl466xhucu",
  "services": [
    { "name": "web", "endpoint": "https://myagent.example.com" },
    { "name": "MCP", "endpoint": "https://mcp.myagent.example.com", "version": "2025-06-18" }
  ],
  "registrations": [
    { "agentId": 5, "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e" }
  ],
  "supportedTrust": ["reputation"]
}
```

## Endpoint Verification

Agents can prove they control their advertised endpoints by serving a `.well-known` file.

**How it works:**

1. Agent lists an HTTPS endpoint in their card
2. Verifier fetches `https://{domain}/.well-known/agent-registration.json`
3. File must contain a `registrations[]` entry matching the agent

Create `/.well-known/agent-registration.json` on your domain:

```json theme={null}
{
  "registrations": [
    {
      "agentId": 5,
      "agentRegistry": "eip155:11155111:0x8004A818BFB912233c491871b3d84c89A494BD9e"
    }
  ]
}
```

<Note>
  Verification isn't needed if your agent URI and endpoint share the same domain — domain control is already demonstrated.
</Note>

## Payment Wallet

ERC-8004 stores the agent's payment wallet **on-chain** (not in the agent card). The wallet address must sign an EIP-712 message proving consent.

```solidity theme={null}
// Contract functions
function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes signature)
function getAgentWallet(uint256 agentId) returns (address)
function unsetAgentWallet(uint256 agentId)
```

Read the payment wallet:

```bash theme={null}
cast call 0x8004A818BFB912233c491871b3d84c89A494BD9e \
  "getAgentWallet(uint256)(address)" <agent-id> \
  --rpc-url https://ethereum-sepolia-rpc.publicnode.com
```

Or use the script:

```bash theme={null}
bun run set-wallet <agent-id>    # Set payment wallet
bun run unset-wallet <agent-id>  # Clear payment wallet
```

## Updating an Agent

To update metadata, re-upload the agent card and call `setAgentURI`:

```typescript theme={null}
const uploadResult = await pinata.upload.public.file(updatedAgentCardFile)
await walletClient.writeContract({
  address: REGISTRY,
  abi,
  functionName: "setAgentURI",
  args: [agentId, `ipfs://${uploadResult.cid}`],
})
```

Or use the script:

```bash theme={null}
bun run register my-agent.json --update 3  # Update agent #3
```

## Verifying Agents

The `verify` script fetches agent cards from IPFS via your Pinata gateway and validates them against the ERC-8004 spec:

```bash theme={null}
bun run verify <agent-id>
bun run check-registration <agent-id>  # Check registrations[] matches on-chain
```

<Note>
  To verify agents you didn't register, set up [Gateway Access Controls](/gateways/gateway-access-controls) and add a gateway key to your `.env` or pass it via CLI: `--gateway-key your_token`
</Note>

## Resources

* [ERC-8004 Specification](https://eips.ethereum.org/EIPS/eip-8004)
* [ERC-8004 Website](https://www.8004.org)
* [PinataCloud/erc8004 Examples](https://github.com/PinataCloud/erc8004)


# Services & IPFS
Source: https://docs.pinata.cloud/tools/erc-8004/services

Service endpoint types and IPFS usage in ERC-8004 agents

ERC-8004 agents can advertise multiple service endpoints. This guide covers all 7 service types, when to use each, and how Pinata fits in.

## Overview

Services are listed in the `services[]` array of your agent card. Each service has:

* `name` — Service type identifier (required)
* `endpoint` — Where to reach the service (required)
* `version` — Protocol version (recommended)

```json theme={null}
{
  "services": [
    { "name": "web", "endpoint": "https://myagent.com" },
    { "name": "MCP", "endpoint": "https://mcp.myagent.com", "version": "2025-06-18" }
  ]
}
```

## Web

**Purpose:** Agent's public website or landing page.

```json theme={null}
{
  "name": "web",
  "endpoint": "https://myagent.com"
}
```

**When to use:** Every agent should have a web endpoint for human discovery. This is where users learn about your agent, see pricing, and find documentation.

**Requirements:**

* Must be HTTPS
* Should be human-readable

## MCP (Model Context Protocol)

**Purpose:** Expose tools, prompts, and resources to LLM applications.

```json theme={null}
{
  "name": "MCP",
  "endpoint": "https://mcp.myagent.com",
  "version": "2025-06-18"
}
```

**When to use:** Your agent provides tools that other AI systems can call — search, calculation, data retrieval, actions.

**Requirements:**

* Must be HTTPS
* Implements MCP server protocol
* Version should match MCP spec date

**Resources:**

* [MCP Specification](https://modelcontextprotocol.io)

## A2A (Agent-to-Agent)

**Purpose:** Direct communication between agents via the A2A protocol.

```json theme={null}
{
  "name": "A2A",
  "endpoint": "https://myagent.com/.well-known/agent-card.json",
  "version": "0.3.0"
}
```

**When to use:** Your agent can receive tasks from other agents, advertise skills, and participate in multi-agent workflows.

**Requirements:**

* Must be HTTPS
* Points to A2A agent card (often at `.well-known/agent-card.json`)
* Implements A2A protocol for task lifecycle

**Resources:**

* [A2A Specification](https://github.com/google/A2A)

## OASF (Open Agent Services Framework)

**Purpose:** Standardized skills and domain classification.

```json theme={null}
{
  "name": "OASF",
  "endpoint": "ipfs://bafkreioasfmanifest...",
  "version": "0.8",
  "skills": ["data-analysis", "summarization"],
  "domains": ["finance", "research"]
}
```

**When to use:** You want your agent discoverable by skill or domain, using the OASF taxonomy.

**Requirements:**

* Can be IPFS or HTTPS
* Optional `skills[]` and `domains[]` arrays for classification

<Note>
  **Use Pinata for OASF manifests.** OASF manifests are static JSON documents — perfect for IPFS. Upload via Pinata and use the CID as your endpoint.
</Note>

**Resources:**

* [OASF Specification](https://github.com/agntcy/oasf)

## ENS (Ethereum Name Service)

**Purpose:** Human-readable name for your agent.

```json theme={null}
{
  "name": "ENS",
  "endpoint": "myagent.eth",
  "version": "v1"
}
```

**When to use:** You've registered an ENS name for your agent and want it discoverable.

**Requirements:**

* Must be a valid `.eth` name
* You should control the ENS name

## DID (Decentralized Identifier)

**Purpose:** W3C-standard decentralized identity.

```json theme={null}
{
  "name": "DID",
  "endpoint": "did:web:myagent.com",
  "version": "v1"
}
```

**When to use:** Your agent has a DID for cross-platform identity verification.

**Common DID methods:**

* `did:web:domain.com` — Web-based DID
* `did:ethr:0x...` — Ethereum-based DID
* `did:key:z...` — Key-based DID

**Resources:**

* [W3C DID Specification](https://www.w3.org/TR/did-core/)

## Email

**Purpose:** Contact email for the agent operator.

```json theme={null}
{
  "name": "email",
  "endpoint": "contact@myagent.com"
}
```

**When to use:** You want to provide a human contact point for support, business inquiries, or abuse reports.

## Service Selection Guide

| If your agent...       | Include these services |
| ---------------------- | ---------------------- |
| Has a landing page     | `web`                  |
| Provides tools to LLMs | `MCP`                  |
| Talks to other agents  | `A2A`                  |
| Uses OASF taxonomy     | `OASF`                 |
| Has an ENS name        | `ENS`                  |
| Has a DID              | `DID`                  |
| Needs human contact    | `email`                |

Most agents should include at least `web` and one protocol endpoint (MCP or A2A).

## Using Pinata with ERC-8004

Pinata handles IPFS storage for several parts of your agent registration. The ERC-8004 spec explicitly recommends IPFS: *"we suggest using IPFS for full data"*

### Agent URI (Token URI)

The most important use — your agent's identity document lives on IPFS, making it censorship-resistant and permanently verifiable.

```typescript theme={null}
const result = await pinata.upload.public.file(agentCardFile)
const agentURI = `ipfs://${result.cid}`

await contract.setAgentURI(agentId, agentURI)
```

### Agent Image

The agent's avatar. Images don't change often, making them perfect for content-addressing.

```json theme={null}
{
  "image": "ipfs://bafkreiaims435hmzeg3l6ixlrlvnei7wept5kmfd6c2ncz3ucl466xhucu"
}
```

### OASF Manifests

Static skill/domain definitions are ideal for IPFS:

```typescript theme={null}
const manifest = { skills: ["data-analysis"], domains: ["finance"] }
const result = await pinata.upload.public.json(manifest)
// Use ipfs://${result.cid} as your OASF endpoint
```

### Reputation Feedback

When storing feedback details on IPFS, the `feedbackHash` parameter is **optional** because the CID already guarantees integrity:

```typescript theme={null}
await contract.giveFeedback(
  agentId,
  100,                        // value
  0,                          // decimals
  "quality",                  // tag1
  "",                         // tag2
  "",                         // endpoint
  `ipfs://${result.cid}`,     // feedbackURI
  "0x00...00"                 // hash NOT needed for IPFS
)
```

### .well-known via IPFS Gateway

Serve your domain verification file from IPFS using a Cloudflare Worker:

```javascript theme={null}
if (url.pathname === '/.well-known/agent-registration.json') {
  const response = await fetch('https://your-gateway.mypinata.cloud/ipfs/bafkrei...')
  return new Response(response.body, {
    headers: { 'Content-Type': 'application/json' }
  })
}
```

## About agentWallet (On-Chain Metadata)

Payment wallet is **not** a service endpoint — it's on-chain metadata managed by the Identity Registry contract.

**How it works:**

1. When you register, `agentWallet` is initially set to your owner address
2. To change it, call `setAgentWallet()` with an EIP-712 signature (EOA) or ERC-1271 signature (smart contract wallet)
3. When the agent NFT is transferred, `agentWallet` is automatically cleared

**Contract functions:**

```solidity theme={null}
function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes signature)
function getAgentWallet(uint256 agentId) returns (address)
function unsetAgentWallet(uint256 agentId)
```

This design ensures the payment wallet is cryptographically verified on-chain, not just declared in a JSON file.


# Overview
Source: https://docs.pinata.cloud/tools/mcp/overview

Use the Pinata MCP server to give AI assistants access to IPFS file storage

## What is MCP?

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that allows AI assistants like Claude to interact with external tools and services. MCP servers expose capabilities that AI models can use to perform actions on your behalf.

The **Pinata MCP server** gives Claude (and other MCP-compatible assistants) the ability to:

* Upload and manage files on IPFS (public and private)
* Search, retrieve, and delete files
* Organize files into groups
* Create gateway links for sharing content
* Set up content monetization with x402 payment instructions
* Vectorize files for semantic search
* Pin existing CIDs from the IPFS network

## Prerequisites

* Node.js 18+ installed
* A [Pinata account](https://app.pinata.cloud) with an [API key (JWT)](/account-management/api-keys)
* A [Pinata Gateway](/gateways/dedicated-ipfs-gateways) URL

## Installation

The Pinata MCP server is available on npm and can be run via `npx`. Installation depends on which Claude client you're using.

### Claude Code

Run `claude mcp add` and follow the prompts:

```
Server Name: pinata
Server Scope: Project or Global
Server Command: npx
Command Arguments: pinata-mcp /path/to/allowed/directory
Environment Variables: PINATA_JWT=<YOUR_JWT>,GATEWAY_URL=example.mypinata.cloud
```

### Claude Desktop

Add the following to your `claude_desktop_config.json`:

```json theme={null}
{
  "mcpServers": {
    "pinata": {
      "command": "npx",
      "args": [
        "pinata-mcp",
        "/path/to/allowed/directory",
        "/another/allowed/directory"
      ],
      "env": {
        "PINATA_JWT": "<YOUR_JWT>",
        "GATEWAY_URL": "example.mypinata.cloud"
      }
    }
  }
}
```

<Note>
  The directory arguments are optional and control which local directories the MCP server can access for file uploads. If not provided, the server only allows access to the current working directory. You can specify multiple directories.
</Note>

## Configuration

| Environment Variable | Required | Description                                                                                |
| -------------------- | -------- | ------------------------------------------------------------------------------------------ |
| `PINATA_JWT`         | Yes      | Your Pinata API JWT from the [API Keys page](https://app.pinata.cloud/developers/api-keys) |
| `GATEWAY_URL`        | Yes      | Your Pinata gateway domain (e.g., `example.mypinata.cloud`)                                |

## Verifying the Connection

Once installed, ask Claude to test the connection:

```
Test my Pinata authentication to make sure everything is working
```

Claude will use the `testAuthentication` tool to verify your JWT is valid.

## Next Steps

See the [Tools Reference](/tools/mcp/tools) for a complete list of available tools with parameters and example prompts.


# Tools Reference
Source: https://docs.pinata.cloud/tools/mcp/tools

Complete reference for all Pinata MCP server tools

This page documents all tools available in the Pinata MCP server. Each tool includes its parameters and example prompts you can use with Claude.

## Authentication

### testAuthentication

Verify that your Pinata JWT is valid and working.

**Parameters:** None

**Example Prompt:**

```
Test my Pinata authentication to make sure everything is working
```

***

## File Operations

### uploadFile

Upload a file to Pinata IPFS. Provide either a file path or base64-encoded content.

| Parameter     | Type                      | Required | Description                                                               |
| ------------- | ------------------------- | -------- | ------------------------------------------------------------------------- |
| `resourceUri` | string                    | No       | File URI to upload (e.g., `file:///path/to/file.jpg`)                     |
| `fileContent` | string                    | No       | Base64-encoded file content (alternative to resourceUri)                  |
| `fileName`    | string                    | No       | Name for the uploaded file (auto-detected from path if using resourceUri) |
| `mimeType`    | string                    | No       | MIME type (auto-detected if not provided)                                 |
| `network`     | `"public"` \| `"private"` | No       | Upload to public or private IPFS (default: `"public"`)                    |
| `group_id`    | string                    | No       | ID of a group to add the file to                                          |
| `keyvalues`   | object                    | No       | Metadata key-value pairs                                                  |

**Example Prompts:**

```
Upload the file at ~/Documents/report.pdf to Pinata as a private file

Upload this image to my Pinata account as a public file named "My Logo"

Upload ~/data/config.json to Pinata and add it to group abc123
```

### searchFiles

Search for files in your Pinata account by name, CID, or MIME type.

| Parameter   | Type                      | Required | Description                                            |
| ----------- | ------------------------- | -------- | ------------------------------------------------------ |
| `network`   | `"public"` \| `"private"` | No       | Search in public or private IPFS (default: `"public"`) |
| `name`      | string                    | No       | Filter by filename                                     |
| `cid`       | string                    | No       | Filter by content ID (CID)                             |
| `mimeType`  | string                    | No       | Filter by MIME type                                    |
| `limit`     | number                    | No       | Maximum number of results                              |
| `pageToken` | string                    | No       | Token for pagination                                   |

**Example Prompts:**

```
Search my Pinata account for all PNG files

Find all files named "report" in my private storage

List my JSON files on Pinata
```

### getFileById

Retrieve detailed information about a specific file by its ID.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `id`      | string                    | Yes      | The unique ID of the file                    |

**Example Prompt:**

```
Get details for file ID abc123 from my Pinata account
```

### updateFile

Update metadata for an existing file including name and key-value pairs.

| Parameter   | Type                      | Required | Description                                  |
| ----------- | ------------------------- | -------- | -------------------------------------------- |
| `network`   | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `id`        | string                    | Yes      | The unique ID of the file                    |
| `name`      | string                    | No       | New name for the file                        |
| `keyvalues` | object                    | No       | Metadata key-value pairs to update           |

**Example Prompt:**

```
Rename file abc123 to "Final Report" and add metadata tag "status": "approved"
```

### deleteFile

Delete a file from your Pinata account.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `id`      | string                    | Yes      | The unique ID of the file                    |

**Example Prompt:**

```
Delete file abc123 from my Pinata account
```

***

## Content Access

### createLink

Create a direct access link for a file. For public files returns a gateway URL, for private files generates a temporary download link.

| Parameter | Type                      | Required | Description                                                 |
| --------- | ------------------------- | -------- | ----------------------------------------------------------- |
| `cid`     | string                    | Yes      | The CID of the file                                         |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`)                |
| `expires` | number                    | No       | Expiration time in seconds for private links (default: 600) |

**Example Prompts:**

```
Create a link for CID bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4

Generate a temporary download link for my private file with CID QmX...
```

### createPrivateDownloadLink

Generate a temporary download link specifically for private IPFS files.

| Parameter | Type   | Required | Description                                            |
| --------- | ------ | -------- | ------------------------------------------------------ |
| `cid`     | string | Yes      | The CID of the private file                            |
| `expires` | number | No       | Expiration time in seconds (default: 600 = 10 minutes) |

**Example Prompt:**

```
Create a 30-minute download link for private CID bafkreih...
```

### fetchFromGateway

Fetch content from IPFS via Pinata gateway and return it.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `cid`     | string                    | Yes      | The CID of the file to fetch                 |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |

**Example Prompts:**

```
Fetch the content with CID QmX... from IPFS

Get the contents of my private file bafkreih...
```

***

## Group Operations

### listGroups

List groups in your Pinata account with optional filtering.

| Parameter   | Type                      | Required | Description                                  |
| ----------- | ------------------------- | -------- | -------------------------------------------- |
| `network`   | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `name`      | string                    | No       | Filter groups by name                        |
| `limit`     | number                    | No       | Maximum number of results                    |
| `pageToken` | string                    | No       | Token for pagination                         |

**Example Prompt:**

```
List all my Pinata groups
```

### createGroup

Create a new group to organize files.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `name`    | string                    | Yes      | Name for the new group                       |

**Example Prompt:**

```
Create a new group called "Project Assets" on Pinata
```

### getGroup

Retrieve detailed information about a specific group.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `id`      | string                    | Yes      | The unique ID of the group                   |

**Example Prompt:**

```
Get details for group abc123
```

### updateGroup

Update metadata for an existing group.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `id`      | string                    | Yes      | The unique ID of the group                   |
| `name`    | string                    | No       | New name for the group                       |

**Example Prompt:**

```
Rename group abc123 to "Marketing Assets"
```

### deleteGroup

Delete a group from your Pinata account.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `id`      | string                    | Yes      | The unique ID of the group                   |

**Example Prompt:**

```
Delete group abc123
```

### addFileToGroup

Add an existing file to a group.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `groupId` | string                    | Yes      | The ID of the group                          |
| `fileId`  | string                    | Yes      | The ID of the file to add                    |

**Example Prompt:**

```
Add file xyz789 to group abc123
```

### removeFileFromGroup

Remove a file from a group.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `groupId` | string                    | Yes      | The ID of the group                          |
| `fileId`  | string                    | Yes      | The ID of the file to remove                 |

**Example Prompt:**

```
Remove file xyz789 from group abc123
```

***

## x402 Payment Instructions

Tools for content monetization using the [x402 protocol](/files/x402/intro). Currently supports USDC on Base and Base Sepolia.

### createPaymentInstruction

Create payment requirements for gated content.

| Parameter     | Type                         | Required | Description                                                   |
| ------------- | ---------------------------- | -------- | ------------------------------------------------------------- |
| `name`        | string                       | Yes      | Name for the payment instruction                              |
| `pay_to`      | string                       | Yes      | Wallet address (0x...) to receive payments                    |
| `amount_usdc` | string                       | Yes      | Price in USD (e.g., `"0.01"` for 1 cent, `"1.50"` for \$1.50) |
| `network`     | `"base"` \| `"base-sepolia"` | No       | Blockchain network (default: `"base"`)                        |
| `description` | string                       | No       | Description of the payment instruction                        |

**Example Prompt:**

```
Create a payment instruction called "Premium Content" that requires 0.01 USDC on Base to access, sending payments to 0x1234...
```

### listPaymentInstructions

List and filter existing payment instructions.

| Parameter   | Type   | Required | Description              |
| ----------- | ------ | -------- | ------------------------ |
| `limit`     | number | No       | Limit results            |
| `pageToken` | string | No       | Token for pagination     |
| `cid`       | string | No       | Filter by associated CID |
| `name`      | string | No       | Filter by name           |
| `id`        | string | No       | Filter by specific ID    |

**Example Prompt:**

```
List all my x402 payment instructions
```

### getPaymentInstruction

Retrieve a specific payment instruction by ID.

| Parameter | Type   | Required | Description                |
| --------- | ------ | -------- | -------------------------- |
| `id`      | string | Yes      | The payment instruction ID |

**Example Prompt:**

```
Get details for payment instruction pi_abc123
```

### updatePaymentInstruction

Modify payment instruction settings.

| Parameter     | Type                         | Required | Description                |
| ------------- | ---------------------------- | -------- | -------------------------- |
| `id`          | string                       | Yes      | The payment instruction ID |
| `name`        | string                       | No       | Updated name               |
| `pay_to`      | string                       | No       | Updated wallet address     |
| `amount_usdc` | string                       | No       | Updated price in USD       |
| `network`     | `"base"` \| `"base-sepolia"` | No       | Updated network            |
| `description` | string                       | No       | Updated description        |

**Example Prompt:**

```
Update payment instruction pi_abc123 to charge $0.05 instead
```

### deletePaymentInstruction

Remove a payment instruction.

| Parameter | Type   | Required | Description                |
| --------- | ------ | -------- | -------------------------- |
| `id`      | string | Yes      | The payment instruction ID |

**Example Prompt:**

```
Delete payment instruction pi_abc123
```

### listPaymentInstructionCids

List CIDs associated with a payment instruction.

| Parameter   | Type   | Required | Description                |
| ----------- | ------ | -------- | -------------------------- |
| `id`        | string | Yes      | The payment instruction ID |
| `limit`     | number | No       | Limit results              |
| `pageToken` | string | No       | Token for pagination       |

**Example Prompt:**

```
List all CIDs associated with payment instruction pi_abc123
```

### addCidToPaymentInstruction

Associate a CID with a payment instruction for monetization.

| Parameter | Type   | Required | Description                |
| --------- | ------ | -------- | -------------------------- |
| `id`      | string | Yes      | The payment instruction ID |
| `cid`     | string | Yes      | The CID to associate       |

**Example Prompt:**

```
Add CID bafkreih... to payment instruction pi_abc123
```

### removeCidFromPaymentInstruction

Remove a CID association from a payment instruction.

| Parameter | Type   | Required | Description                |
| --------- | ------ | -------- | -------------------------- |
| `id`      | string | Yes      | The payment instruction ID |
| `cid`     | string | Yes      | The CID to remove          |

**Example Prompt:**

```
Remove CID bafkreih... from payment instruction pi_abc123
```

***

## CID Signatures

Tools for cryptographic content verification using EIP-712 signatures.

### addSignature

Add an EIP-712 cryptographic signature to a CID for content verification.

| Parameter   | Type                      | Required | Description                                   |
| ----------- | ------------------------- | -------- | --------------------------------------------- |
| `network`   | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`)  |
| `cid`       | string                    | Yes      | The CID to sign                               |
| `signature` | string                    | Yes      | The EIP-712 signature                         |
| `address`   | string                    | Yes      | The wallet address that created the signature |

**Example Prompt:**

```
Add signature 0xabc... from wallet 0x123... to CID bafkreih...
```

### getSignature

Get signature details for a specific CID.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `cid`     | string                    | Yes      | The CID to get the signature for             |

**Example Prompt:**

```
Get the signature for CID bafkreih...
```

### deleteSignature

Remove a signature from a CID.

| Parameter | Type                      | Required | Description                                  |
| --------- | ------------------------- | -------- | -------------------------------------------- |
| `network` | `"public"` \| `"private"` | No       | Public or private IPFS (default: `"public"`) |
| `cid`     | string                    | Yes      | The CID to remove the signature from         |

**Example Prompt:**

```
Delete the signature from CID bafkreih...
```

***

## Signed Upload URLs

### createSignedUploadUrl

Create a presigned URL for client-side uploads without exposing your API key.

| Parameter          | Type      | Required | Description                                              |
| ------------------ | --------- | -------- | -------------------------------------------------------- |
| `expires`          | number    | Yes      | How long the URL is valid in seconds                     |
| `max_file_size`    | number    | No       | Max file size in bytes                                   |
| `allow_mime_types` | string\[] | No       | Allowed MIME types (supports wildcards like `"image/*"`) |
| `group_id`         | string    | No       | Group to add the uploaded file to                        |
| `filename`         | string    | No       | Name for the uploaded file                               |
| `keyvalues`        | object    | No       | Metadata key-value pairs                                 |

**Example Prompt:**

```
Create a signed upload URL that expires in 5 minutes and only allows image uploads up to 10MB
```

***

## Pin by CID

### pinByCid

Pin an existing CID from the IPFS network to your Pinata account.

| Parameter    | Type      | Required | Description                 |
| ------------ | --------- | -------- | --------------------------- |
| `cid`        | string    | Yes      | CID to pin                  |
| `name`       | string    | No       | Custom name for the file    |
| `group_id`   | string    | No       | Group to add the file to    |
| `keyvalues`  | object    | No       | Metadata key-value pairs    |
| `host_nodes` | string\[] | No       | Host node IDs to fetch from |

**Example Prompt:**

```
Pin the CID bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4 to my account
```

### queryPinRequests

Query the status of pin by CID requests.

| Parameter   | Type                | Required | Description          |
| ----------- | ------------------- | -------- | -------------------- |
| `order`     | `"ASC"` \| `"DESC"` | No       | Sort by date queued  |
| `status`    | string              | No       | Filter by status     |
| `cid`       | string              | No       | Filter by CID        |
| `limit`     | number              | No       | Limit results        |
| `pageToken` | string              | No       | Token for pagination |

**Example Prompt:**

```
Check the status of my pending pin requests
```

### cancelPinRequest

Cancel a pending pin by CID request.

| Parameter | Type   | Required | Description                     |
| --------- | ------ | -------- | ------------------------------- |
| `id`      | string | Yes      | ID of the pin request to cancel |

**Example Prompt:**

```
Cancel pin request pr_abc123
```

***

## Vectorize (AI/Semantic Search)

### vectorizeFile

Vectorize a file for semantic search capabilities.

| Parameter | Type   | Required | Description                 |
| --------- | ------ | -------- | --------------------------- |
| `file_id` | string | Yes      | ID of the file to vectorize |

**Example Prompt:**

```
Vectorize the file with ID abc123 so I can search it semantically
```

### deleteFileVectors

Delete vectors for a file.

| Parameter | Type   | Required | Description                          |
| --------- | ------ | -------- | ------------------------------------ |
| `file_id` | string | Yes      | ID of the file to delete vectors for |

**Example Prompt:**

```
Delete the vectors for file abc123
```

### queryVectors

Query vectorized files using semantic search.

| Parameter  | Type   | Required | Description                      |
| ---------- | ------ | -------- | -------------------------------- |
| `group_id` | string | Yes      | ID of the group to search        |
| `text`     | string | Yes      | Query string for semantic search |

**Example Prompt:**

```
Search my vectorized files in group xyz for "machine learning concepts"
```

***

## Utilities

### listAllowedDirectories

List directories the MCP server can access for file operations.

**Parameters:** None

**Example Prompt:**

```
What directories can you access for file uploads?
```


# Pinata Expo Hooks
Source: https://docs.pinata.cloud/tools/pinata-expo-hooks

A collection of Expo compatible hooks to use with Pinata

<Warning>
  Pinata Expo Hooks are still under developement and will only work in iOS and Android at the moment. Please [reach out](mailto:steve@pinata.cloud) if you have any issues!
</Warning>

## Installation

Install with your package manager of choice

```bash theme={null}
pnpm i pinata-expo-hooks@latest
```

## Source Code

`pinata-expo-hooks` is MIT open sourced and can be accessed on GitHub with the link below

<Card title="PinataCloud/pinata-expo" icon="github" href="https://github.com/PinataCloud/pinata-expo" />

## Usage

Import at the top of your desired page or component

```typescript theme={null}
import { useUpload } from "pinata-expo-hooks";
```

Inside your page or component use the hook to extract methods and state

```typescript theme={null}
const {
	upload, // Method to upload a file using a presigned URL
	progress, // Progress state as integer
	loading, // Boolean uploading state
	uploadResponse, // File ID used to fetch the file info server side
	error, // Error state
	pause, // Pause upload method
	resume, // Resume upload method
	cancel, // Cancel current upload method
} = useUpload();
```

Return types for `useUpload`

```typescript theme={null}
export type UseUploadReturn = {
	progress: number;
	loading: boolean;
	error: Error | null;
	uploadResponse: string | null;
	upload: (
		fileUri: string,
		network: "public" | "private",
		url: string,
		options?: UploadOptions,
	) => Promise<void>;
	pause: () => void;
	resume: () => void;
	cancel: () => void;
};
```

To upload a file you must already have a server setup that returns a [Presigned URL](/files/presigned-urls). Then you can pass it into the `upload` method like so.

```typescript theme={null}
await upload(fileUri, "public", "presigned_URL", {
	name: fileName || "Upload from Expo",
	keyvalues: {
		app: "Pinata Expo Demo",
		timestamp: Date.now().toString(),
	},
});
```

<Tip>
  [Learn how to send Presigned URLs from your server](/frameworks/hono)
</Tip>

Below are the available `UploadOptions` that can be passed into `upload`

```typescript theme={null}
export type UploadOptions = {
	customHeaders?: Record<string, string>;
	name?: string;
	keyvalues?: Record<string, string>;
	groupId?: string;
	chunkSize?: number; // Defaults to 1014 * 256 * 20 * 10 (~52MB)
};
```

Once a file is uploaded the `uploadResponse` will contain the CID for the file.

Below is a full example of implementing the `useUpload` hook with progress and abilities to pause and resume the upload.

```typescript theme={null}
import { Image } from "expo-image";
import { View, Text, Button, StyleSheet } from "react-native";
import * as ImagePicker from "expo-image-picker";
import * as DocumentPicker from "expo-document-picker";
import { useState } from "react";
import ParallaxScrollView from "@/components/ParallaxScrollView";
import { useUpload } from "pinata-expo-hooks";

const SERVER_URL = "http://localhost:8787"; // Server that returns a presigned_url

export default function HomeScreen() {
	const {
		upload, // Method to upload a file using a presigned URL
		progress, // Progress state as integer
		loading, // Boolean uploading state
		uploadResponse, // CID for the uploaded file
		error, // Error state
		pause, // Pause upload method
		resume, // Resume upload method
		cancel, // Cancel current upload method
	} = useUpload();
	const [fileUri, setFileUri] = useState<string | null>(null);
	const [fileName, setFileName] = useState<string | null>(null);

	const pickImage = async () => {
		const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();

		if (status !== "granted") {
			alert("Sorry, we need camera roll permissions to make this work!");
			return;
		}

		const result = await ImagePicker.launchImageLibraryAsync({
			mediaTypes: ImagePicker.MediaTypeOptions.All,
			allowsEditing: false,
			quality: 1,
		});

		if (!result.canceled && result.assets && result.assets.length > 0) {
			setFileUri(result.assets[0].uri);
			setFileName(result.assets[0].uri.split("/").pop() || "image");
		}
	};

	// Pick document from device
	const pickDocument = async () => {
		try {
			const result = await DocumentPicker.getDocumentAsync();

			if (result.canceled === false) {
				setFileUri(result.assets[0].uri);
				setFileName(result.assets[0].name);
			}
		} catch (err) {
			console.error("Error picking document", err);
		}
	};

	// Start the upload process
	const startUpload = async () => {
		if (!fileUri) {
			alert("Please select a file first");
			return;
		}
		try {
			const urlRes = await fetch(`${SERVER_URL}/presigned_url`);
			if (!urlRes.ok) {
				console.log(urlRes.status);
			}
			const urlData = await urlRes.json();
			await upload(
				fileUri,
				"public", // or 'private' if you want a private upload
				urlData.url,
				{
					name: fileName || "Upload from Expo",
					keyvalues: {
						app: "Pinata Expo Demo",
						timestamp: Date.now().toString(),
					},
				},
			);
		} catch (err) {
			console.error("Failed to start upload:", err);
			alert("Failed to start upload");
		}
	};

	// Progress bar component
	const ProgressBar = ({ value }: { value: number }) => {
		return (
			<View style={styles.progressBarContainer}>
				<View style={[styles.progressBar, { width: `${value}%` }]} />
			</View>
		);
	};

	return (
		<ParallaxScrollView
			headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }}
			headerImage={
				<Image
					source={require("@/assets/images/partial-react-logo.png")}
					style={styles.reactLogo}
				/>
			}
		>
			<View style={styles.container}>
				<Text style={styles.title}>Pinata IPFS Upload</Text>

				<View style={styles.buttonContainer}>
					<Button title="Pick Image" onPress={pickImage} />
					<Button title="Pick Document" onPress={pickDocument} />
				</View>

				{fileUri && (
					<View style={styles.fileInfoContainer}>
						<Text style={styles.fileInfo}>Selected: {fileName}</Text>
						<Button
							title="Upload to IPFS"
							onPress={startUpload}
							disabled={loading}
							color="#FF6AC1" // Pinata pink
						/>
					</View>
				)}

				{loading && (
					<View style={styles.uploadStatusContainer}>
						<Text style={styles.uploadStatusText}>
							Uploading... {Math.round(progress)}%
						</Text>
						<ProgressBar value={progress} />

						<View style={styles.uploadControlsContainer}>
							{progress < 100 && (
								<>
									<Button title="Pause" onPress={pause} color="#FFA15C" />
									<Button title="Resume" onPress={resume} color="#5CB8FF" />
									<Button title="Cancel" onPress={cancel} color="#FF5C5C" />
								</>
							)}
						</View>
					</View>
				)}

				{error && (
					<View style={styles.errorContainer}>
						<Text style={styles.errorTitle}>Upload Error</Text>
						<Text style={styles.errorText}>{error.message}</Text>
					</View>
				)}

				{uploadResponse && (
					<View style={styles.successContainer}>
						<Text style={styles.successTitle}>Upload Complete!</Text>
						<Text style={styles.successText}>File CID: {uploadResponse}</Text>
					</View>
				)}
			</View>
		</ParallaxScrollView>
	);
}

const styles = StyleSheet.create({
	titleContainer: {
		flexDirection: "row",
		alignItems: "center",
		gap: 8,
	},
	stepContainer: {
		gap: 8,
		marginBottom: 8,
	},
	reactLogo: {
		height: 178,
		width: 290,
		bottom: 0,
		left: 0,
		position: "absolute",
	},
	container: {
		flex: 1,
		padding: 20,
		backgroundColor: "#f5f5f5",
	},
	title: {
		fontSize: 24,
		fontWeight: "bold",
		marginBottom: 20,
		textAlign: "center",
	},
	buttonContainer: {
		flexDirection: "row",
		justifyContent: "space-around",
		marginBottom: 20,
	},
	fileInfoContainer: {
		backgroundColor: "#fff",
		padding: 15,
		borderRadius: 8,
		marginBottom: 20,
		elevation: 2,
		shadowColor: "#000",
		shadowOffset: { width: 0, height: 1 },
		shadowOpacity: 0.2,
		shadowRadius: 2,
	},
	fileInfo: {
		marginBottom: 10,
	},
	uploadStatusContainer: {
		backgroundColor: "#fff",
		padding: 15,
		borderRadius: 8,
		marginBottom: 20,
		elevation: 2,
		shadowColor: "#000",
		shadowOffset: { width: 0, height: 1 },
		shadowOpacity: 0.2,
		shadowRadius: 2,
	},
	uploadStatusText: {
		marginBottom: 10,
		fontWeight: "bold",
	},
	progressBarContainer: {
		height: 10,
		backgroundColor: "#e0e0e0",
		borderRadius: 5,
		overflow: "hidden",
		marginBottom: 15,
	},
	progressBar: {
		height: "100%",
		backgroundColor: "#FF6AC1", // Pinata pink
	},
	uploadControlsContainer: {
		flexDirection: "row",
		justifyContent: "space-around",
	},
	errorContainer: {
		backgroundColor: "#FFEBEE",
		padding: 15,
		borderRadius: 8,
		marginBottom: 20,
		borderLeftWidth: 4,
		borderLeftColor: "#FF5C5C",
	},
	errorTitle: {
		fontWeight: "bold",
		color: "#D32F2F",
		marginBottom: 5,
	},
	errorText: {
		color: "#D32F2F",
	},
	successContainer: {
		backgroundColor: "#E8F5E9",
		padding: 15,
		borderRadius: 8,
		marginBottom: 20,
		borderLeftWidth: 4,
		borderLeftColor: "#4CAF50",
	},
	successTitle: {
		fontWeight: "bold",
		color: "#2E7D32",
		marginBottom: 10,
	},
	successText: {
		color: "#2E7D32",
		marginBottom: 5,
	},
	pinningContainer: {
		flexDirection: "row",
		alignItems: "center",
		marginBottom: 5,
	},
	pinningText: {
		color: "#FF6AC1",
		marginRight: 10,
	},
});
```

## Questions

Please [contact us](mailto:steve@pinata.cloud) if you have any issues!


# Add File To Group
Source: https://docs.pinata.cloud/api-reference/endpoint/add-file-to-group

put /groups/{network}/{id}/ids/{file_id}
`org:groups:write`




# Add Signature to CId
Source: https://docs.pinata.cloud/api-reference/endpoint/add-signature-to-cid

post /files/{network}/signature/{cid}
`org:files:write`




# Add Swap
Source: https://docs.pinata.cloud/api-reference/endpoint/add-swap

put /files/{network}/swap/{cid}
`org:files:write`




# Cancel Request
Source: https://docs.pinata.cloud/api-reference/endpoint/cancel-pin-by-cid

DELETE /files/public/pin_by_cid/{id}
`org:files:write`




# Create Group
Source: https://docs.pinata.cloud/api-reference/endpoint/create-group

post /groups/{network}
`org:groups:write`




# Create Signed Upload URL
Source: https://docs.pinata.cloud/api-reference/endpoint/create-signed-upload-url

pinata-api-v3-uploads.yaml post /files/sign
`org:files:write`




# Delete File by ID
Source: https://docs.pinata.cloud/api-reference/endpoint/delete-file-by-id

delete /files/{network}/{id}
`org:files:write`




# Delete File Vectors
Source: https://docs.pinata.cloud/api-reference/endpoint/delete-file-vectors

delete /vectorize/files/{file_id}
`org:write`




# Delete Group
Source: https://docs.pinata.cloud/api-reference/endpoint/delete-group

delete /groups/{network}/{id}
`org:groups:write`




# Get File by ID
Source: https://docs.pinata.cloud/api-reference/endpoint/get-file-by-id

get /files/{network}/{id}
`org:files:read`




# Get Group
Source: https://docs.pinata.cloud/api-reference/endpoint/get-group

get /groups/{network}/{id}
`org:groups:read`




# Get Signature for a CID
Source: https://docs.pinata.cloud/api-reference/endpoint/get-signature-for-a-cid

get /files/{network}/signature/{cid}
`org:files:read`




# Create Download Link
Source: https://docs.pinata.cloud/api-reference/endpoint/get-signed-url

pinata-api-v3.yaml post /files/private/download_link
`org:files:write`




# Get Swap History
Source: https://docs.pinata.cloud/api-reference/endpoint/get-swap-history

get /files/{network}/swap/{cid}
`org:files:read`




# Create Custom Domain for Gateway
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/create-custom-domain-for-gateway

post /gateways/{id}/custom_domain
`org:gateways:write`




# Create Gateway
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/create-gateway

post /gateways
`org:gateways:write`




# Create Gateway IP Address Restriction
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/create-gateway-ip-address-restriction

post /gateways/{id}/ips
`org:gateways:write`




# Create Gateway Key Restriction
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/create-gateway-key-restriction

post /gateways/{id}/access_tokens
`org:gateways:write`




# Delete Gateway
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/delete-gateway

delete /gateways/{id}
`org:gateways:write`




# Delete Gateway Custom Domain
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/delete-gateway-custom-domain

delete /gateways/{id}/custom_domain/{custom_domain_id}
`org:gateways:write`




# Gateway Custom Domain Details
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/gateway-custom-domain-details

get /gateways/{id}/custom_domain/{custom_domain_id}
`org:gateways:read`




# Gateway Details
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/gateway-details

get /gateways/{id}
`org:gateways:read`




# Gateway Domain Available
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/gateway-domain-available

get /gateways/exists/{domain}
`org:gateways:read`




# List Gateways
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/list-gateways

get /gateways
`org:gateways:read`




# List Files
Source: https://docs.pinata.cloud/api-reference/endpoint/list-files

get /files/{network}
`org:files:read`




# List Groups
Source: https://docs.pinata.cloud/api-reference/endpoint/list-groups

get /groups/{network}
`org:groups:read`




# Pin by CID
Source: https://docs.pinata.cloud/api-reference/endpoint/pin-by-cid

post /files/public/pin_by_cid
`org:files:write`




# Query File Vectors
Source: https://docs.pinata.cloud/api-reference/endpoint/query-file-vectors

post /vectorize/groups/{group_id}/query
`org:write`




# Query Pin Requests
Source: https://docs.pinata.cloud/api-reference/endpoint/query-pin-requests

get /files/public/pin_by_cid
`org:files:read`




# Remove File From Group
Source: https://docs.pinata.cloud/api-reference/endpoint/remove-file-from-group

delete /groups/{network}/{id}/ids/{file_id}
`org:groups:write`




# Remove Signature from CID
Source: https://docs.pinata.cloud/api-reference/endpoint/remove-signature-from-cid

delete /files/{network}/signature/{cid}
`org:files:write`




# Remove Swap
Source: https://docs.pinata.cloud/api-reference/endpoint/remove-swap

delete /files/{network}/swap/{cid}
`org:files:write`




# Update File
Source: https://docs.pinata.cloud/api-reference/endpoint/update-file

put /files/{network}/{id}
`org:files:write`




# Update Group
Source: https://docs.pinata.cloud/api-reference/endpoint/update-group

put /groups/{network}/{id}
`org:groups:write`




# Upload a File
Source: https://docs.pinata.cloud/api-reference/endpoint/upload-a-file

post /files
`org:files:write`


<Note>
  The V3 Upload endpoint currently does not support folder uploads. Please use the legacy [pinFileToIPFS endpoint](/api-reference/endpoint/ipfs/pin-file-to-ipfs)
</Note>


# Create API Key
Source: https://docs.pinata.cloud/api-reference/endpoint/v3-create-api-key

post /api_keys
`org:write`




# List API Keys
Source: https://docs.pinata.cloud/api-reference/endpoint/v3-list-api-keys

get /api_keys
`org:write`




# Revoke API Key
Source: https://docs.pinata.cloud/api-reference/endpoint/v3-revoke-api-key

delete /api_keys/{key}
`org:write`




# Vectorize a File
Source: https://docs.pinata.cloud/api-reference/endpoint/vectorize-file

post /vectorize/files/{file_id}
`org:write`




# Introduction
Source: https://docs.pinata.cloud/api-reference/introduction

Getting started with the Pinata API

Welcome to the Pinata API Reference! Before you can do anything in the API, you'll need some API Keys.

## API Keys

Visit the [Pinata API Keys](https://app.pinata.cloud/developers/api-keys) page to generate new keys.

<img />

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, so be sure to give it a name to keep track of it. Once you have that filled out, click "Generate API 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.

<Warning>
  Once API keys have been created, you will not be able to see the secret or JWT
  again
</Warning>

Once you have created your keys you can go ahead and try testing them! Try to paste this into your terminal with your `JWT`

```bash 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!"
}
```


# bandwidth
Source: https://docs.pinata.cloud/sdk/analytics/bandwidth

`org:analytics:read`

Get analytics on bandwidth for multiple properties

## Usage

The `analytics` class is unique in that it very flexible, but also can require more queries to be used well. Be sure to real the [Parameters](#parameters) in detail to understand how it can be used.

The `bandwidth` method will sort results by highest number of bandwidth, but will also include `requests` values.

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const clicks = await pinata.analytics.bandwidth
  .days(30)
  .limit(10)
  .cid("<CID>")
```

## Returns

What is returned in `value` will depend on they property or query used. For instance, using `cid()` will return CIDs, `country()` will return Countries, etc.

```typescript theme={null}
type TopAnalyticsResponse = {
	data: TopAnalyticsItem[];
};

type TopAnalyticsItem = {
	value: string;
	requests: number;
	bandwidth: number;
};
```

## Parameters

Filter response with the following additional methods. It does require at least one property, such as `cid`, `fileName`, `userAgent`, `country`, `region`, or `referrer`.

### cid

* Type: `string`

Returns bandwidth for all CIDs

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .cid()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .cid("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
```

### fileName

* Type: `string`

Returns bandwidth for all file names

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .fileName()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .fileName("pinnie.png")
```

### userAgent

* Type: `string`

Returns bandwidth for user agents

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .userAgent()
```

Filter by passing an argument

```typescript {3-5} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .userAgent(
    "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0"
  )
```

### country

* Type: `string`

Returns bandwidth for countries

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .country()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .country("us")
```

### region

* Type: `string`

Returns bandwidth for regions inside of countries

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .region()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .region("us - VA")
```

### referer

* Type: `string`

Returns bandwidth for referers

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .referer()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .referer("https://docs.pinata.cloud/")
```

### days

* Type: `number`

Number of days to query. Starts with current date and then goes back by provided number.

```typescript {2} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .cid()
```

### sort

* Type: `"asc" | "desc"`

Order results either ascending or descending by created date

```typescript {4} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .cid()
  .sort("asc")
```

### limit

* Type: `number`

Limit the number of results

```typescript {4} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .cid()
  .limit(10)
```

### customDates

* Type: `string, string`

Custom dates to query using a start and end date with the format `YYYY-MM-DD`

```typescript {2} theme={null}
const files = await pinata.analytics.bandwidth
  .customDates("2024-11-01", "2024-11-20")
  .cid()
```

### from

* Type: `string`

Provide an alternate Gateway domain to query instead of the default one found in the Pinata SDK Config

```typescript {4} theme={null}
const files = await pinata.analytics.bandwidth
  .days(7)
  .cid()
  .from("example-2.mypinata.cloud")
```


# requests
Source: https://docs.pinata.cloud/sdk/analytics/requests

`org:analytics:read`

Get analytics on requests for multiple properties

## Usage

The `analytics` class is unique in that it very flexible, but also can require more queries to be used well. Be sure to real the [Parameters](#parameters) in detail to understand how it can be used.

The `requests` method will sort results by highest number of requests, but will also include `bandwidth` values.

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const clicks = await pinata.analytics.requests
  .days(30)
  .limit(10)
  .cid("<CID>")
```

## Returns

What is returned in `value` will depend on they property or query used. For instance, using `cid()` will return CIDs, `country()` will return Countries, etc.

```typescript theme={null}
type TopAnalyticsResponse = {
	data: TopAnalyticsItem[];
};

type TopAnalyticsItem = {
	value: string;
	requests: number;
	bandwidth: number;
};
```

## Parameters

Filter response with the following additional methods. It does require at least one property, such as `cid`, `fileName`, `userAgent`, `country`, `region`, or `referrer`.

### cid

* Type: `string`

Returns requests for all CIDs

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .cid()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .cid("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
```

### fileName

* Type: `string`

Returns requests for all file names

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .fileName()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .fileName("pinnie.png")
```

### userAgent

* Type: `string`

Returns requests for user agents

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .userAgent()
```

Filter by passing an argument

```typescript {3-5} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .userAgent(
    "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0"
  )
```

### country

* Type: `string`

Returns requests for countries

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .country()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .country("us")
```

### region

* Type: `string`

Returns requests for regions inside of countries

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .region()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .region("us - VA")
```

### referer

* Type: `string`

Returns requests for referers

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .referer()
```

Filter by passing an argument

```typescript {3} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .referer("https://docs.pinata.cloud/")
```

### days

* Type: `number`

Number of days to query. Starts with current date and then goes back by provided number.

```typescript {2} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .cid()
```

### sort

* Type: `"asc" | "desc"`

Order results either ascending or descending by created date

```typescript {4} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .cid()
  .sort("asc")
```

### limit

* Type: `number`

Limit the number of results

```typescript {4} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .cid()
  .limit(10)
```

### customDates

* Type: `string, string`

Custom dates to query using a start and end date with the format `YYYY-MM-DD`

```typescript {2} theme={null}
const files = await pinata.analytics.requests
  .customDates("2024-11-01", "2024-11-20")
  .cid()
```

### from

* Type: `string`

Provide an alternate Gateway domain to query instead of the default one found in the Pinata SDK Config

```typescript {4} theme={null}
const files = await pinata.analytics.requests
  .days(7)
  .cid()
  .from("example-2.mypinata.cloud")
```


# deleteSwap
Source: https://docs.pinata.cloud/sdk/files/private/delete-swap

`org:files:write`

Remove a CID swap for [Hot Swaps](/gateways/plugins/hot-swaps) plugin

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const deleteSwap = await pinata.files.private.deleteSwap(
  "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske"
)
```

## Returns

```typescript theme={null}
OK
```

## Parameters

Pass in the required parameters below to remove a CID swap

### cid

* Type: `string`

This would be the original CID that was swapped

```typescript theme={null}
const deleteSwap = await pinata.files.private.deleteSwap(
  "bafkreibbvdqf5ekc2crouowv7vtjbcmbjoiysegslmmqc6jrxbaa43xske"
)
```


# createAccessLink
Source: https://docs.pinata.cloud/sdk/gateways/private/create-signed-url

`org:files:write`

Creates a temporary access link for a file on private IPFS

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const url = await pinata.gateways.private.createAccessLink({
	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
	expires: 30,
});
```

## Returns

* Type: `string`

The full signed URL

```
https://example-gateway.mypinata.cloud/files/bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq?X-Algorithm=PINATA1&X-Date=1724943397&X-Expires=30&X-Method=GET&X-Signature=<SIGNATURE>
```

## Parameters

### cid

* Type: `string`

Accepts CID of the file you are trying to create an access link for

```typescript {2} theme={null}
const url = await pinata.gateways.private.createAccessLink({
  cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
	expires: 30,
});
```

### expires

* Type: `number`

The number of seconds the access link should be valid for

```typescript {3} theme={null}
const url = await pinata.gateways.private.createAcceslink({
	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
	expires: 30,
});
```

### date (Optional)

* Type: `number`

A UNIX timestamp of the date the link is created

```typescript {1-2,7} theme={null}
const date = Math.floor(new Date().getTime() / 1000);
//date: 1724943711

const url = await pinata.gateways.private.createAccessLink({
	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
	expires: 30,
	date: date
});
```

### gateway (Optional)

* Type: `string`

Use a Gateway domain other than the default domain from the config

```typescript {4} theme={null}
const url = await pinata.gateways.private.createAccessLink({
	cid: "bafkreib4pqtikzdjlj4zigobmd63lig7u6oxlug24snlr6atjlmlza45dq",
	expires: 30,
	gateway: "discordpinnie.mypinata.cloud"
});
```

### optimizeImage (Optional)

* Type: [OptimizeImageOptions](/sdk/types#optimizeimageoptions)

```typescript theme={null}
type OptimizeImageOptions = {
  width?: number;
  height?: number;
  dpr?: number;
  fit?: "scaleDown" | "contain" | "cover" | "crop" | "pad";
  gravity?: "auto" | "side" | string;
  quality?: number;
  format?: "auto" | "webp";
  animation?: boolean;
  sharpen?: number;
  onError?: boolean;
  metadata?: "keep" | "copyright" | "none";
};
```

If the content being fetched is an image you can apply image optimizations to the image.

```typescript {3-7} theme={null}
const data = await pinata.gateways.private
  .createAccessLink("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
  .optimizeImage({
    width: 500,
    height: 500,
    format: "webp"
  })
```


# get
Source: https://docs.pinata.cloud/sdk/gateways/private/get

`org:files:write`

Retrieve a file through the config's `pinataGateway`

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const file = await pinata.gateways.private.get("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
```

## Returns

Returns the data in the form of `JSON`, `string`, or `Blob` as well as the `ContentType`

```typescript theme={null}
type GetCIDResponse = {
  data?: JSON | string | Blob | null;
  contentType: ContentType;
};

type ContentType =
  | "application/json"
  | "application/xml"
  | "text/plain"
  | "text/html"
  | "text/css"
  | "text/javascript"
  | "application/javascript"
  | "image/jpeg"
  | "image/png"
  | "image/gif"
  | "image/svg+xml"
  | "audio/mpeg"
  | "audio/ogg"
  | "video/mp4"
  | "application/pdf"
  | "application/octet-stream"
  | string;
```

## Parameters

### cid

* Type: `string`

Accepts CID of the file you are trying to fetch

```typescript theme={null}
const data = await pinata.gateways.private.get(
	"bafybeibo5zcqeorhqxczodrx52rn7byyrwfvwthz5dspnjlbkd7zkugefi",
);
```

### optimizeImage (Optional)

* Type: [OptimizeImageOptions](/sdk/types#optimizeimageoptions)

```typescript theme={null}
type OptimizeImageOptions = {
  width?: number;
  height?: number;
  dpr?: number;
  fit?: "scaleDown" | "contain" | "cover" | "crop" | "pad";
  gravity?: "auto" | "side" | string;
  quality?: number;
  format?: "auto" | "webp";
  animation?: boolean;
  sharpen?: number;
  onError?: boolean;
  metadata?: "keep" | "copyright" | "none";
};
```

If the content being fetched is an image you can apply image optimizations to the image.

```typescript {3-7} theme={null}
const data = await pinata.gateways.private
  .get("bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4")
  .optimizeImage({
    width: 500,
    height: 500,
    format: "webp"
  })
```


# convert
Source: https://docs.pinata.cloud/sdk/gateways/public/convert



Convert an IPFS link into one that uses your Dedicated Gateway

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata-web3";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const url = await pinata.gateways.convert(
  "ipfs://QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
);
```

## Returns

```typescript theme={null}
string
```

## Parameters

### url

* Type: `string`

Accepts CID or IPFS gateway link.

```typescript theme={null}
const url = await pinata.gateways.convert(
	"bafybeibo5zcqeorhqxczodrx52rn7byyrwfvwthz5dspnjlbkd7zkugefi/hello-1.txt",
);

const url = await pinata.gateways.convert(
	"https://ipfs.io/ipfs/bafybeibo5zcqeorhqxczodrx52rn7byyrwfvwthz5dspnjlbkd7zkugefi/hello-1.txt",
);

const url = await pinata.gateways.convert(
	"https://bafyreibroegmxohcbvvs3rziqsp3osyn7t5rzot34y6pn5xtewffhtsl4e.ipfs.nftstorage.link/metadata.json",
);
```

### prefix (Optional)

* Type: `string`

Use a different gateway prefix than the config default

```typescript theme={null}
const url = await pinata.gateways.convert(
	"QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng",
	"https://ipfs.io"
);
```


# get
Source: https://docs.pinata.cloud/sdk/gateways/public/get



Retrieve a file from IPFS through the config's `pinataGateway`

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata-web3";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const file = await pinata.gateways.public.get("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
```

## Returns

Returns the data from the CID in the form of `JSON`, `string`, or `Blob` as well as the `ContentType`

```typescript theme={null}
type GetCIDResponse = {
  data?: JSON | string | Blob | null;
  contentType: ContentType;
};

type ContentType =
  | "application/json"
  | "application/xml"
  | "text/plain"
  | "text/html"
  | "text/css"
  | "text/javascript"
  | "application/javascript"
  | "image/jpeg"
  | "image/png"
  | "image/gif"
  | "image/svg+xml"
  | "audio/mpeg"
  | "audio/ogg"
  | "video/mp4"
  | "application/pdf"
  | "application/octet-stream"
  | string;
```

## Parameters

### cid

* Type: `string`

Accepts CID or IPFS gateway link.

```typescript theme={null}
const data = await pinata.gateways.public.get(
	"bafybeibo5zcqeorhqxczodrx52rn7byyrwfvwthz5dspnjlbkd7zkugefi/hello-1.txt",
);

const data = await pinata.gateways.public.get(
	"https://ipfs.io/ipfs/bafybeibo5zcqeorhqxczodrx52rn7byyrwfvwthz5dspnjlbkd7zkugefi/hello-1.txt",
);

const data = await pinata.gateways.public.get(
	"https://bafyreibroegmxohcbvvs3rziqsp3osyn7t5rzot34y6pn5xtewffhtsl4e.ipfs.nftstorage.link/metadata.json",
);
```

## Methods

### optimizeImage (Optional)

* Type: [OptimizeImageOptions](/sdk/types#optimizeimageoptions)

```typescript theme={null}
type OptimizeImageOptions = {
  width?: number;
  height?: number;
  dpr?: number;
  fit?: "scaleDown" | "contain" | "cover" | "crop" | "pad";
  gravity?: "auto" | "side" | string;
  quality?: number;
  format?: "auto" | "webp";
  animation?: boolean;
  sharpen?: number;
  onError?: boolean;
  metadata?: "keep" | "copyright" | "none";
};
```

If the content being fetched is an image you can apply image optimizations to the image.

```typescript theme={null}
const data = await pinata.gateways.public
  .get("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
  .optimizeImage({
    width: 500,
    height: 500,
    format: "webp"
  })
```


# addFiles
Source: https://docs.pinata.cloud/sdk/groups/private/add-files

`org:groups:write`

Add private files to a private group

## Usage

```typescript 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.private.addFiles({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

## Returns

```typescript theme={null}
type UpdateGroupFilesResponse[] = {
	id: string;
	status: string;
};
```

## Parameters

### groupId

* Type: `string`

ID of the target Group to add files to

```typescript {2} theme={null}
const group = await pinata.groups.private.addFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

### files

* Type: `string[]`

An array of file IDs as strings that you want to add to the group

```typescript {3-6} theme={null}
const group = await pinata.groups.private.addFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```


# create
Source: https://docs.pinata.cloud/sdk/groups/private/create

`org:groups:write`

Create a private group

## Usage

```typescript 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.private.create({
	name: "My New Group",
});
```

## Returns

```typescript theme={null}
type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

### name

* Type: `string`

Requires a name for the group to be created

```typescript {2} theme={null}
const group = await pinata.groups.private.create({
	name: "My New Group",
});
```


# delete
Source: https://docs.pinata.cloud/sdk/groups/private/delete

`org:groups:write`

Delete a private group

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.private.delete({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```

## Returns

```typescript theme={null}
OK
```

## Parameters

### groupId

* Type: `string`

ID for the target Group

```typescript {2} theme={null}
const groups = await pinata.groups.private.delete({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```


# get
Source: https://docs.pinata.cloud/sdk/groups/private/get

`org:groups:read`

Get info for an existing private group

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.private.get({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```

## Returns

```typescript theme={null}
type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

### groupId

* Type: `string`

ID for the target Group

```typescript {2} theme={null}
const groups = await pinata.groups.private.get({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```


# list
Source: https://docs.pinata.cloud/sdk/groups/private/list

`org:groups:read`

List and filter through all private groups

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.private
  .list()
```

## Returns

```typescript theme={null}
type GroupListResponse = {
  groups: GroupResponseItem[];
  next_page_token: string;
};

type GroupResponseItem = {
    id: string;
    is_public: boolean;
    name: string;
    created_at: string;
};
```

## Parameters

Filter response with the following additional methods. All filters are optional.

### name

* Type: `string`

Filters groups based the group name

```typescript {3} theme={null}
const groups = await pinata.groups.private
    .list()
    .name("SDK")
```

### limit

* Type: `number`

Limits the number of results

```typescript {3} theme={null}
const groups = await pinata.groups.private
    .list()
    .limit(10)
```

### pageToken

* Type: `string`

Paginates through groups based on a provided page token

```typescript {3} theme={null}
const groups = await pinata.groups.private
  .list()
  .pageToken("MDE5MWIzZWYtM2U0Zi03YTY5LWE3OTQtOTRhZDE5NjQxMTk0")
```

## Auto Paginate

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.gateways.private.list()) {
  console.log(item.name);
}
```


# removeFiles
Source: https://docs.pinata.cloud/sdk/groups/private/remove-files

`org:groups:write`

Remove private files from a private group

## Usage

```typescript 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.private.removeFiles({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

## Returns

```typescript theme={null}
type UpdateGroupFilesResponse[] = {
	id: string;
	status: string;
};
```

## Parameters

### groupId

* Type: `string`

ID of the target Group to remove files from

```typescript {2} theme={null}
const group = await pinata.groups.private.removeFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

### files

* Type: `string[]`

An array of file IDs as strings that you want to remove from the group

```typescript {3-6} theme={null}
const group = await pinata.groups.private.removeFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```


# update
Source: https://docs.pinata.cloud/sdk/groups/private/update

`org:groups:write`

Update the name of a private group

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.private.update({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	name: "My New Group 2",
});
```

## Returns

```typescript theme={null}
type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

### groupId

* Type: `string`

ID for the target Group

```typescript {2} theme={null}
const groups = await pinata.groups.private.update({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	name: "My New Group 2",
});
```

### name (Optional)

* Type: `string`

Updated name for the target group

```typescript {2} theme={null}
const groups = await pinata.groups.private.update({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	name: "My Group Again",
});
```


# addFiles
Source: https://docs.pinata.cloud/sdk/groups/public/add-files

`org:groups:write`

Add public files to a public group

## Usage

```typescript 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.addFiles({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

## Returns

```typescript theme={null}
type UpdateGroupFilesResponse[] = {
	id: string;
	status: string;
};
```

## Parameters

### groupId

* Type: `string`

ID of the target Group to add files to

```typescript {2} theme={null}
const group = await pinata.groups.public.addFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

### files

* Type: `string[]`

An array of file IDs as strings that you want to add to the group

```typescript {3-6} theme={null}
const group = await pinata.groups.public.addFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```


# create
Source: https://docs.pinata.cloud/sdk/groups/public/create

`org:groups:write`

Create a public group

## Usage

```typescript 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",
});
```

## Returns

```typescript theme={null}
type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

### name

* Type: `string`

Requires a name for the group to be created

```typescript {2} theme={null}
const group = await pinata.groups.public.create({
	name: "My New Group",
});
```


# delete
Source: https://docs.pinata.cloud/sdk/groups/public/delete

`org:groups:write`

Delete a public group

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.public.delete({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```

## Returns

```typescript theme={null}
OK
```

## Parameters

### groupId

* Type: `string`

ID for the target Group

```typescript {2} theme={null}
const groups = await pinata.groups.public.delete({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```


# get
Source: https://docs.pinata.cloud/sdk/groups/public/get

`org:groups:read`

Get info for an existing public group

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.public.get({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```

## Returns

```typescript theme={null}
type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

### groupId

* Type: `string`

ID for the target Group

```typescript {2} theme={null}
const groups = await pinata.groups.public.get({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```


# list
Source: https://docs.pinata.cloud/sdk/groups/public/list

`org:groups:read`

List and filter through all public groups

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.public
  .list()
```

## Returns

```typescript theme={null}
type GroupListResponse = {
  groups: GroupResponseItem[];
  next_page_token: string;
};

type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

Filter response with the following additional methods. All filters are optional.

### name

* Type: `string`

Filters groups based the group name

```typescript {3} theme={null}
const groups = await pinata.groups.public
    .list()
    .name("SDK")
```

### limit

* Type: `number`

Limits the number of results

```typescript {3} theme={null}
const groups = await pinata.groups.public
    .list()
    .limit(10)
```

### pageToken

* Type: `string`

Paginates through groups based on a provided page token

```typescript {3} theme={null}
const groups = await pinata.groups.public
  .list()
  .pageToken("MDE5MWIzZWYtM2U0Zi03YTY5LWE3OTQtOTRhZDE5NjQxMTk0")
```

## Auto Paginate

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.gateways.public.list()) {
  console.log(item.name);
}
```


# removeFiles
Source: https://docs.pinata.cloud/sdk/groups/public/remove-files

`org:groups:write`

Remove public files from a public group

## Usage

```typescript 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.removeFiles({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

## Returns

```typescript theme={null}
type UpdateGroupFilesResponse[] = {
	id: string;
	status: string;
};
```

## Parameters

### groupId

* Type: `string`

ID of the target Group to remove files from

```typescript {2} theme={null}
const group = await pinata.groups.public.removeFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```

### files

* Type: `string[]`

An array of file IDs as strings that you want to remove from the group

```typescript {3-6} theme={null}
const group = await pinata.groups.public.removeFiles({
  groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	files: [
	  "7e18c4a4-9501-44de-8f81-403db7de0e39",
		"a606ef7e-70a0-40ad-9b8a-60563e009655"
	],
});
```


# update
Source: https://docs.pinata.cloud/sdk/groups/public/update

`org:groups:write`

Update the name of a public group

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const groups = await pinata.groups.public.update({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
	name: "My New Group 2",
});
```

## Returns

```typescript theme={null}
type GroupResponseItem = {
    id: string;
    name: string;
    created_at: string;
};
```

## Parameters

### groupId

* Type: `string`

ID for the target Group

```typescript {2} theme={null}
const groups = await pinata.groups.public.update({
	groupId: "3778c10d-452e-4def-8299-ee6bc548bdb0",
});
```

### name (Optional)

* Type: `string`

Updated name for the target group

```typescript {2} theme={null}
const groups = await pinata.groups.public.update({
	name: "My Group Again",
});
```


# create
Source: https://docs.pinata.cloud/sdk/keys/create

`org:write`

Create an API key

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const key = await pinata.keys.create({
  keyName: "user 1",
  permissions: {
    resources:  [
      "org:files:read",
      "org:groups:read"
    ]
  },
  maxUses: 1,
});
```

## Returns

```typescript theme={null}
type KeyResponse = {
  JWT: string;
  pinata_api_key: string;
  pinata_api_secret: string;
};
```

## Parameters

### keyName

* Type: `string`

Name for the API key

```typescript {2} theme={null}
const key = await pinata.keys.create({
  keyName: "user 1",
  permissions: {
    admin: true,
  },
  maxUses: 1,
});
```

### maxUses (Optional)

* Type: `number`

Limit the number of uses a key is valid for

```typescript {6} theme={null}
const key = await pinata.keys.create({
  keyName: "user 1",
  permissions: {
    admin: true,
  },
  maxUses: 1,
});
```

### permissions

* Type: [KeyPermissions](../types#keypermissions)

#### admin (Optional)

* Type: `boolean`

Grants the key admin access to all endpoints

```typescript {4} theme={null}
const key = await pinata.keys.create({
  keyName: "user 1",
  permissions: {
    admin: true,
  }
});
```

#### resources (Optional)

* Type: `ResourcePermission[]`

```typescript theme={null}
export type ResourcePermission =
	| "org:read"
	| "org:write"
	| "org:files:read"
	| "org:files:write"
	| "org:groups:read"
	| "org:groups:write"
	| "org:gateways:read"
	| "org:gateways:write"
	| "org:analytics:read"
	| "org:analytics:write";
```

Scope the API key by ResourcePermission

```typescript {4-7} theme={null}
const key = await pinata.keys.create({
  keyName: "user 1",
  permissions: {
    resources: [
      "org:files:read",
      "org:groups:read",
    ]
  }
});
```

#### endpoints (Optional)

* Type [Endpoints](../types#endpoints)

```typescript {3-20} theme={null}
const key = await pinata.keys.create({
  keyName: "user 1",
  permissions: {
    endpoints: {
      data: {
        pinList: true,
        userPinnedDataTotal: false
      },
      pinning: {
        hashMetadata: true,
        hashPinPolicy: false,
        pinByHash: true,
        pinFileToIPFS: true,
        pinJSONToIPFS: true,
        pinJobs: false,
        unpin: false,
        userPinPolicy: false
      }
    }
  }
});
```


# list
Source: https://docs.pinata.cloud/sdk/keys/list

`org:write`

List and filter through Keys

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const keys = await pinata.keys
  .list()
  .name("Admin")
  .revoked(false)
```

## Returns

```typescript theme={null}
type KeyListItem = {
  id: string;
  name: string;
  key: string;
  secret: string;
  max_uses: number;
  uses: number;
  user_id: string;
  scopes: KeyScopes;
  revoked: boolean;
  createdAt: string;
  updatedAt: string;
};
```

## Parameters

Filter response with the following additional methods. All filters are optional.

### name

* Type: `string`

Filter by name, uses "contains" matching

```typescript theme={null}
const keys = await pinata.keys
  .list()
  .name("Greetings");
```

### revoked

* Type: `boolean`

Filter keys by whether or not they have been revoked

```typescript theme={null}
const keys = await pinata.keys
  .list()
  .revoked(false);
```

### exhausted

* Type: `boolean`

Filter keys based on whether they had limited uses that were exhuasted

```typescript theme={null}
const keys = await pinata.keys
  .list()
  .exhausted(false);
```

### offset

* Type: `number`

Offset the number of keys returned to paginate

```typescript theme={null}
const keys = await pinata.keys
  .list()
  .offset(5);
```

## Auto Paginate

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.keys.list()) {
  console.log(item.name);
}
```


# revoke
Source: https://docs.pinata.cloud/sdk/keys/revoke

`org:write`

Revoke an API Key

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const revoke = await pinata.keys.revoke([
 "94566af5e63833e260be"
]);
```

## Returns

```typescript theme={null}
type RevokeKeyResponse[] = {
  key: string;
  status: string;
};
```

## Parameters

### keys

* Type: `string[]`

An array of API Keys to revoke. This is the `key` found in the response of `keys.list`

```typescript theme={null}
const revoke = await pinata.keys.revoke([
 "94566af5e63833e260be"
]);
```


# add
Source: https://docs.pinata.cloud/sdk/signatures/private/add

`org:files:write`

Add an EIP-712 signature to a CID

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures).
</Note>

## Usage

```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.private.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```

## Returns

```typescript theme={null}
type SignatureResponse = {
  cid: string;
  signature: string;
};
```

## Parameters

### cid

* Type: `string`

Target CID that you want to add a signature to.

```typescript {2} theme={null}
const signature = await pinata.signatures.private.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```

### signature

* Type: `0x${string}`

EIP-712 Signature to be assigned to target CID.

```typescript {3} theme={null}
const signature = await pinata.signatures.private.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```

### address

* Type: `0x${string}`

Wallet address that made the signature

```typescript {4} theme={null}
const signature = await pinata.signatures.private.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```


# delete
Source: https://docs.pinata.cloud/sdk/signatures/private/delete

`org:files:write`

Delete an EIP-712 signature from a CID

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures).
</Note>

## Usage

```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.delete(
  "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU"
);
```

## Returns

```typescript theme={null}
OK
```

## Parameters

### cid

* Type: `string`

Target CID that you want to add a signature to.

```typescript theme={null}
const signature = await pinata.signatures.delete(
  "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU"
);
```


# get
Source: https://docs.pinata.cloud/sdk/signatures/private/get

`org:files:read`

Get signature for CID

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures).
</Note>

## Usage

```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.get("QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU");
```

## Returns

```typescript theme={null}
type SignatureResponse = {
  cid: string;
  signature: string;
};
```

## Parameters

### cid

* Type: `string`

Target CID that you want to add a signature to.

```typescript theme={null}
const signature = await pinata.signatures.get("QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU");
```


# add
Source: https://docs.pinata.cloud/sdk/signatures/public/add

`org:files:write`

Add an EIP-712 signature to a CID

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures).
</Note>

## Usage

```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"
});
```

## Returns

```typescript theme={null}
type SignatureResponse = {
  cid: string;
  signature: string;
};
```

## Parameters

### cid

* Type: `string`

Target CID that you want to add a signature to.

```typescript {2} theme={null}
const signature = await pinata.signatures.public.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```

### signature

* Type: `0x${string}`

EIP-712 Signature to be assigned to target CID.

```typescript {3} theme={null}
const signature = await pinata.signatures.public.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```

### address

* Type: `0x${string}`

Wallet address that made the signature

```typescript {4} theme={null}
const signature = await pinata.signatures.public.add({
  cid: "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU",
  signature: "0x1b...911b",
  address: "0xB3899AA8E13172E48D44CE411b0c4c2f08730Dc6"
});
```


# delete
Source: https://docs.pinata.cloud/sdk/signatures/public/delete

`org:files:write`

Delete an EIP-712 signature from a CID

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures).
</Note>

## Usage

```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.delete(
  "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU"
);
```

## Returns

```typescript theme={null}
OK
```

## Parameters

### cid

* Type: `string`

Target CID that you want to add a signature to.

```typescript theme={null}
const signature = await pinata.signatures.delete(
  "QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU"
);
```


# get
Source: https://docs.pinata.cloud/sdk/signatures/public/get

`org:files:read`

Get signature for CID

<Note>
  For more information about adding signatures to CIDs on IPFS please read the [Signatures Guide](/files/signatures).
</Note>

## Usage

```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.get("QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU");
```

## Returns

```typescript theme={null}
type SignatureResponse = {
  cid: string;
  signature: string;
};
```

## Parameters

### cid

* Type: `string`

Target CID that you want to add a signature to.

```typescript theme={null}
const signature = await pinata.signatures.get("QmXGeVy9dVwfuFJmvbzz8y4dYK1TdxXbDGzwbNuyZ5xXSU");
```


# Analytics
Source: https://docs.pinata.cloud/sdk/types/analytics



### AnalyticsQuery

```typescript theme={null}
export type AnalyticsQuery = {
	gateway_domain: string;
	start_date: string;
	end_date: string;
	cid?: string;
	file_name?: string;
	user_agent?: string;
	country?: string;
	region?: string;
	referer?: string;
	limit?: number;
	sort_order?: "asc" | "desc";
};
```

### TopAnalyticsQuery

```typescript theme={null}
export type TopAnalyticsQuery = AnalyticsQuery & {
	sort_by: "requests" | "bandwidth";
	attribute:
		| "cid"
		| "country"
		| "region"
		| "user_agent"
		| "referer"
		| "file_name";
};
```

### TopAnalyticsResponse

```typescript theme={null}
export type TopAnalyticsResponse = {
	data: TopAnalyticsItem[];
};
```

### TopAnalyticsItem

```typescript theme={null}
export type TopAnalyticsItem = {
	value: string;
	requests: number;
	bandwidth: number;
};
```

### TimeIntervalAnalyticsQuery

```typescript theme={null}
export type TimeIntervalAnalyticsQuery = AnalyticsQuery & {
	sort_by?: "requests" | "bandwidth";
	date_interval: "day" | "week";
};
```

### TimePeriodItem

```typescript theme={null}
export type TimePeriodItem = {
	period_start_time: string;
	requests: number;
	bandwidth: number;
};
```

### TimeIntervalAnalyticsResponse

```typescript theme={null}
export type TimeIntervalAnalyticsResponse = {
	total_requests: number;
	total_bandwidth: number;
	time_periods: TimePeriodItem[];
};
```

### UserPinnedDataResponse

```typescript theme={null}
export type UserPinnedDataResponse = {
	pin_count: number;
	pin_size_total: number;
	pin_size_with_replications_total: number;
};
```


# Config
Source: https://docs.pinata.cloud/sdk/types/config



### PinataConfig

```typescript theme={null}
export type PinataConfig = {
	pinataJwt?: string;
	pinataGateway?: string;
	pinataGatewayKey?: string;
	customHeaders?: Record<string, string>;
	endpointUrl?: string;
	uploadUrl?: string;
};
```


# Files
Source: https://docs.pinata.cloud/sdk/types/files



### FileObject

```typescript theme={null}
export type FileObject = {
	name: string;
	size: number;
	type: string;
	lastModified: number;
	arrayBuffer: () => Promise<ArrayBuffer>;
};
```

### JsonBody

```typescript theme={null}
export type JsonBody = Record<string, unknown>;
```

### PinataMetadata

```typescript theme={null}
export type PinataMetadata = {
	name?: string;
	keyvalues?: Record<string, string>;
};
```

### UpdateFileOptions

```typescript theme={null}
export type UpdateFileOptions = {
	id: string;
	name?: string;
	keyvalues?: Record<string, string>;
};
```

### DeleteResponse

```typescript theme={null}
export type DeleteResponse = {
	id: string;
	status: string;
};
```

### FileListItem

```typescript theme={null}
export type FileListItem = {
	id: string;
	name: string | null;
	cid: "pending" | string;
	size: number;
	number_of_files: number;
	mime_type: string;
	keyvalues: Record<string, string>;
	group_id: string | null;
	created_at: string;
};
```

### FileListResponse

```typescript theme={null}
export type FileListResponse = {
	files: FileListItem[];
	next_page_token: string;
};
```

### FileListQuery

```typescript theme={null}
export type FileListQuery = {
	name?: string;
	group?: string;
	noGroup?: boolean;
	mimeType?: string;
	cid?: string;
	cidPending?: boolean;
	metadata?: Record<string, string>;
	order?: "ASC" | "DESC";
	limit?: number;
	pageToken?: string;
};
```

### PinQueueQuery

```typescript theme={null}
export type PinQueueQuery = {
	sort?: "ASC" | "DSC";
	status?:
		| "prechecking"
		| "retrieving"
		| "expired"
		| "backfilled
		| "over_free_limit"
		| "over_max_size"
		| "invalid_object"
		| "bad_host_node";
	ipfs_pin_hash?: string;
	limit?: number;
	pageToken?: string;
};
```

### PinQueueItem

```typescript theme={null}
export type PinQueueItem = {
	id: string;
	cid?: string;
	ipfs_pin_hash?: string;
	date_queued: string;
	name: string;
	status: string;
	keyvalues: any;
	host_nodes: string[];
	pin_policy: {
		regions: {
			id: string;
			desiredReplicationCount: number;
		}[];
		version: number;
	};
};
```

### PinQueueResponse

```typescript theme={null}
export type PinQueueResponse = {
	rows: PinQueueItem[];
	next_page_token: string;
};
```

### SwapCidOptions

```typescript theme={null}
export type SwapCidOptions = {
	cid: string;
	swapCid: string;
};
```

### SwapHistoryOptions

```typescript theme={null}
export type SwapHistoryOptions = {
	cid: string;
	domain: string;
};
```

### SwapCidResponse

```typescript theme={null}
export type SwapCidResponse = {
	mapped_cid: string;
	created_at: string;
};
```

### VectorizeFileResponse

```typescript theme={null}
export type VectorizeFileResponse = {
	status: boolean;
};
```

### VectorizeQuery

```typescript theme={null}
export type VectorizeQuery = {
	groupId: string;
	query: string;
	returnFile?: boolean;
};
```

### VectorQueryMatch

```typescript theme={null}
export type VectorQueryMatch = {
	file_id: string;
	cid: string;
	score: number;
};
```

### VectorizeQueryResponse

```typescript theme={null}
export type VectorizeQueryResponse = {
	count: number;
	matches: VectorQueryMatch[];
};
```

***


# Gateways
Source: https://docs.pinata.cloud/sdk/types/gateways



### ContentType

```typescript theme={null}
export type ContentType =
	| "application/json"
	| "application/xml"
	| "text/plain"
	| "text/html"
	| "text/css"
	| "text/javascript"
	| "application/javascript"
	| "image/jpeg"
	| "image/png"
	| "image/gif"
	| "image/svg+xml"
	| "audio/mpeg"
	| "audio/ogg"
	| "video/mp4"
	| "application/pdf"
	| "application/octet-stream"
	| string
	| null; // Allow for other MIME types
```

### GetCIDResponse

```typescript theme={null}
export type GetCIDResponse = {
	data?: JSON | string | Blob | null;
	contentType: ContentType;
};
```

### OptimizeImageOptions

```typescript theme={null}
export type OptimizeImageOptions = {
	width?: number;
	height?: number;
	dpr?: number;
	fit?: "scaleDown" | "contain" | "cover" | "crop" | "pad";
	gravity?: "auto" | "side" | string;
	quality?: number;
	format?: "auto" | "webp";
	animation?: boolean;
	sharpen?: number;
	onError?: boolean;
	metadata?: "keep" | "copyright" | "none";
};
```

### AccessLinkOptions

```typescript theme={null}
export type AccessLinkOptions = {
	cid: string;
	date?: number;
	expires: number;
	gateway?: string;
};
```

### ContainsCIDResponse

```typescript theme={null}
export type ContainsCIDResponse = {
	containsCid: boolean;
	cid: string | null;
};
```


# Groups
Source: https://docs.pinata.cloud/sdk/types/groups



### GroupOptions

```typescript theme={null}
export type GroupOptions = {
	name: string;
	isPublic?: boolean;
};
```

### UpdateGroupOptions

```typescript theme={null}
export type UpdateGroupOptions = {
	groupId: string;
	name?: string;
	isPublic?: boolean;
};
```

### GetGroupOptions

```typescript theme={null}
export type GetGroupOptions = {
	groupId: string;
};
```

### GroupListResponse

```typescript theme={null}
export type GroupListResponse = {
	groups: GroupResponseItem[];
	next_page_token: string;
};
```

### GroupResponseItem

```typescript theme={null}
export type GroupResponseItem = {
	id: string;
	is_public: boolean;
	name: string;
	createdAt: string;
};
```

### GroupQueryOptions

```typescript theme={null}
export type GroupQueryOptions = {
	name?: string;
	limit?: number;
	pageToken?: string;
	isPublic?: boolean;
};
```

### GroupCIDOptions

```typescript theme={null}
export type GroupCIDOptions = {
	groupId: string;
	files: string[];
};
```

### UpdateGroupFilesResponse

```typescript theme={null}
export type UpdateGroupFilesResponse = {
	id: string;
	status: string;
};
```


# Keys
Source: https://docs.pinata.cloud/sdk/types/keys



### KeyPermissions

```typescript theme={null}
export type KeyPermissions = {
	admin?: boolean;
	endpoints?: Endpoints;
};
```

### Endpoints

```typescript theme={null}
export type Endpoints = {
	data?: DataEndponts;
	pinning?: PinningEndpoints;
};
```

### DataEndponts

```typescript theme={null}
export type DataEndponts = {
	pinList?: boolean;
	userPinnedDataTotal?: boolean;
};
```

### PinningEndpoints

```typescript theme={null}
export type PinningEndpoints = {
	hashMetadata?: boolean;
	hashPinPolicy?: boolean;
	pinByHash?: boolean;
	pinFileToIPFS?: boolean;
	pinJSONToIPFS?: boolean;
	pinJobs?: boolean;
	unpin?: boolean;
	userPinPolicy?: boolean;
};
```

### KeyOptions

```typescript theme={null}
export type KeyOptions = {
	keyName: string;
	permissions: KeyPermissions;
	maxUses?: number;
};
```

### KeyResponse

```typescript theme={null}
export type KeyResponse = {
	JWT: string;
	pinata_api_key: string;
	pinata_api_secret: string;
};
```

### KeyListQuery

```typescript theme={null}
export type KeyListQuery = {
	revoked?: boolean;
	limitedUse?: boolean;
	exhausted?: boolean;
	name?: string;
	offset?: number;
};
```

### KeyListItem

```typescript theme={null}
export type KeyListItem = {
	id: string;
	name: string;
	key: string;
	secret: string;
	max_uses: number;
	uses: number;
	user_id: string;
	scopes: KeyScopes;
	revoked: boolean;
	createdAt: string;
	updatedAt: string;
};
```

### KeyScopes

```typescript theme={null}
type KeyScopes = {
	endpoints: {
		pinning: {
			pinFileToIPFS: boolean;
			pinJSONToIPFS: boolean;
		};
	};
	admin: boolean;
};
```

### KeyListResponse

```typescript theme={null}
export type KeyListResponse = {
	keys: KeyListItem[];
	count: number;
};
```

### RevokeKeyResponse

```typescript theme={null}
export type RevokeKeyResponse = {
	key: string;
	status: string;
};
```


# Signatures
Source: https://docs.pinata.cloud/sdk/types/signatures



### SignatureOptions

```typescript theme={null}
export type SignatureOptions = {
	cid: string;
	signature: string;
	address: string;
};
```

### SignatureResponse

```typescript theme={null}
export type SignatureResponse = {
	cid: string;
	signature: string;
};
```


# Uploads
Source: https://docs.pinata.cloud/sdk/types/uploads



### UploadResponse

```typescript theme={null}
export type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	user_id: string;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

### UploadOptions

```typescript theme={null}
export type UploadOptions = {
	metadata?: PinataMetadata;
	//pinType?: "async" | "sync" | "cidOnly";
	keys?: string;
	groupId?: string;
	vectorize?: boolean;
	url?: string;
};
```

### SignedUploadUrlOptions

```typescript theme={null}
export type SignedUploadUrlOptions = {
	date?: number;
	expires: number;
	groupId?: string;
	name?: string;
	keyvalues?: Record<string, string>;
	vectorize?: boolean;
};
```

### UploadCIDOptions

```typescript theme={null}
export type UploadCIDOptions = {
	metadata?: PinataMetadata;
	peerAddresses?: string[];
	keys?: string;
	groupId?: string;
};
```

### PinByCIDResponse

```typescript theme={null}
export type PinByCIDResponse = {
	id: string;
	cid: string;
	status: "prechecking" | "retrieving";
	name: string;
};
```


# x402
Source: https://docs.pinata.cloud/sdk/types/x402



Types for x402 monetization features

## PaymentRequirement

Defines a single payment requirement within a payment instruction.

```typescript theme={null}
type PaymentRequirement = {
  asset: string;
  pay_to: string;
  network: "base" | "base-sepolia" | "eip155:8453" | "eip155:84532";
  amount: string;
  description?: string;
};
```

| Property      | Type                                                                | Description                                                                                                   |
| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `asset`       | `string`                                                            | The USDC token contract address for the network (e.g., `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` for Base) |
| `pay_to`      | `string`                                                            | The wallet address to receive payments                                                                        |
| `network`     | `"base"` \| `"base-sepolia"` \| `"eip155:8453"` \| `"eip155:84532"` | The blockchain network for transactions. Accepts friendly names or EIP-155 chain IDs                          |
| `amount`      | `string`                                                            | The amount required in USDC smallest units (6 decimals). Example: `"10000"` = \$0.01                          |
| `description` | `string`                                                            | Optional description for this requirement                                                                     |

## PaymentInstruction

Represents a complete payment instruction with all its requirements.

```typescript theme={null}
type PaymentInstruction = {
  id: string;
  version: number;
  payment_requirements: PaymentRequirement[];
  name: string;
  description?: string;
  created_at: string;
};
```

| Property               | Type                   | Description                                   |
| ---------------------- | ---------------------- | --------------------------------------------- |
| `id`                   | `string`               | Unique identifier for the payment instruction |
| `version`              | `number`               | Version number of the instruction             |
| `payment_requirements` | `PaymentRequirement[]` | Array of payment requirements                 |
| `name`                 | `string`               | Name of the payment instruction               |
| `description`          | `string`               | Optional description                          |
| `created_at`           | `string`               | ISO timestamp of creation                     |

## CreatePaymentInstructionRequest

Request body for creating a new payment instruction.

```typescript theme={null}
type CreatePaymentInstructionRequest = {
  name: string;
  payment_requirements: PaymentRequirement[];
  description?: string;
};
```

## UpdatePaymentInstructionRequest

Request body for updating an existing payment instruction.

```typescript theme={null}
type UpdatePaymentInstructionRequest = {
  name?: string;
  payment_requirements?: PaymentRequirement[];
  description?: string;
};
```

## PaymentInstructionListQuery

Query parameters for listing payment instructions.

```typescript theme={null}
type PaymentInstructionListQuery = {
  limit?: number;
  pageToken?: string;
  cid?: string;
  name?: string;
  id?: string;
};
```

| Property    | Type     | Description                         |
| ----------- | -------- | ----------------------------------- |
| `limit`     | `number` | Maximum number of results to return |
| `pageToken` | `string` | Pagination token for next page      |
| `cid`       | `string` | Filter by associated CID            |
| `name`      | `string` | Filter by instruction name          |
| `id`        | `string` | Filter by instruction ID            |

## PaymentInstructionListResponse

Response structure for listing payment instructions.

```typescript theme={null}
type PaymentInstructionListResponse = {
  data: {
    payment_instructions: PaymentInstruction[];
    next_page_token?: string;
  };
};
```

## PaymentInstructionResponse

Response structure for single payment instruction operations.

```typescript theme={null}
type PaymentInstructionResponse = {
  data: PaymentInstruction;
};
```

## PaymentInstructionDeleteResponse

Response structure for delete operations.

```typescript theme={null}
type PaymentInstructionDeleteResponse = {
  data: {};
};
```

## CidListQuery

Query parameters for listing CIDs associated with a payment instruction.

```typescript theme={null}
type CidListQuery = {
  limit?: number;
  pageToken?: string;
};
```

## CidListResponse

Response structure for listing CIDs.

```typescript theme={null}
type CidListResponse = {
  data: {
    cids: string[];
    next_page_token?: string;
  };
};
```

## CidAssociation

Represents a CID associated with a payment instruction.

```typescript theme={null}
type CidAssociation = {
  payment_instruction_id: string;
  cid: string;
  created_at: string;
};
```

| Property                 | Type     | Description                                       |
| ------------------------ | -------- | ------------------------------------------------- |
| `payment_instruction_id` | `string` | ID of the associated payment instruction          |
| `cid`                    | `string` | The IPFS CID                                      |
| `created_at`             | `string` | ISO timestamp of when the association was created |

## CidAssociationResponse

Response structure for adding a CID to a payment instruction.

```typescript theme={null}
type CidAssociationResponse = {
  data: CidAssociation;
};
```

## CidRemoveResponse

Response structure for removing a CID from a payment instruction.

```typescript theme={null}
type CidRemoveResponse = {
  data: {};
};
```


# base64
Source: https://docs.pinata.cloud/sdk/upload/private/base64

`org:files:write`

Upload a base64 string to Private IPFS

## Usage

```typescript 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.private.base64("SGVsbG8gV29ybGQh")
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### base64

* Type: `string`

Accepts a string encoded in base64

```typescript theme={null}
const upload = await pinata.upload.private.base64("SGVsbG8gV29ybGQh")
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .base64("SGVsbG8gV29ybGQh")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional metadata to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .base64("SGVsbG8gV29ybGQh")
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .base64("SGVsbG8gV29ybGQh")
  .name("hello.txt")
```

### vectorize (Optional)

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

* Type: `null`

Vectorize a file on upload, requires a group to be used as well.

```typescript {4} theme={null}
const upload = await pinata.upload.private
  .base64("SGVsbG8gV29ybGQh")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
  .vectorize()
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .base64("SGVsbG8gV29ybGQh")
  .key("GENERATED_API_JWT")
```


# createSignedURL
Source: https://docs.pinata.cloud/sdk/upload/private/create-signed-url

`org:files:write`

Create a signed upload URL for a file upload. This method is ideal when you want to authorize server side but upload on the client; check out [this doc](/files/uploading-files#client-side-uploads) for more details.

<Note>
  All the information about the upload such as the name, group, keyvalues, etc. must be part of the signed URL in order for those values to go through. If you have a basic signed URL with no extra parameters and then try to add fields like name, group, etc. they will not go through.
</Note>

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const signedURL = await pinata.upload.private.createSignedURL({
  expires: 30
});

const upload = await pinata.upload.private
  .file(file)
  .url(signedURL)
```

## Returns

* Type: `string`

The full signed URL

```
https://uploads.pinata.cloud/v3/files/209bb44c-e987-47c4-a9e3-a88e02d4d8e9?X-Algorithm=PINATA2&X-Date=1734379351&X-Expires=300&X-Method=%5B%22HEAD%22%2C%22PATCH%22%2C%22POST%22%5D&X-User-ID=eb73db21-10fa-4005-b97c-639e19e82d60&X-Signature=<SIGNATURE>
```

## Parameters

### expires

* Type: `number`

The number of seconds the signed URL should be valid for

```typescript {2} theme={null}
const url = await pinata.upload.private.createSignedURL({
	expires: 30,
});
```

### name (Optional)

* Type: `string`

Name for the file to be uploaded

```typescript {3} theme={null}
const url = await pinata.upload.private.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.private.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.private.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.private.createSignedURL({
	expires: 30,
	groupId: "ad4bc3bf-8794-49e7-94ff-fea1ce745779"
});
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Keyvalue pairs for the uploaded file

```typescript {3-5} theme={null}
const url = await pinata.upload.private.createSignedURL({
	expires: 30,
	keyvalues: {
	  env: "prod"
	}
});
```

### vectorize (Optional)

* Type: `boolean`

Vectorize the file on upload

```typescript {3} theme={null}
const url = await pinata.upload.private.createSignedURL({
	expires: 30,
	vectorize: true
});
```

### 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.private.createSignedURL({
	expires: 30,
	date: date
});
```


# file
Source: https://docs.pinata.cloud/sdk/upload/private/file

`org:files:write`

Upload a single file to Private IPFS

## Usage

```typescript 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 world!"], "hello.txt", { type: "text/plain" })
const upload = await pinata.upload.private.file(file)
```

### 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`.

```typescript {10-12} 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.private.file(file);
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### file

* Type: `File` object

Accepts a File object in accordance with the [W3C File API](https://w3c.github.io/FileAPI/#file-section).

```typescript {3} theme={null}
const blob = new Blob(["hello world!"], { type: "text/plain" })
const file = new File([blob], "hello.txt", { type: "text/plain" })
const upload = await pinata.upload.private.file(file)
```

In most environments you can also pass a Blob here as well.

```typescript {2} theme={null}
const blob = new Blob(["hello world!"], { type: "text/plain" })
const upload = await pinata.upload.private.file(blob)
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .name("hello.txt")
```

### vectorize (Optional)

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

* Type: `null`

Vectorize a file on upload, requires a group to be used as well.

```typescript {4} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
  .vectorize()
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .key("GENERATED_API_JWT")
```

### expires (Optional)

* Type: `number`

Set an expiration date for the file using a Unix timestamp. The timestamp must be in the future. Once the expiration time is reached, the file will be automatically deleted.

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .file(file)
  .expires(1735689600)
```


# json
Source: https://docs.pinata.cloud/sdk/upload/private/json

`org:files:write`

Upload a JSON object to Private IPFS

## Usage

```typescript 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.json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
})
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### JSON

* Type: `Record<string, unknown>`

Accepts an object that is turned into JSON

```typescript theme={null}
const upload = await pinata.upload.private.json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
})
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {7} theme={null}
const upload = await pinata.upload.private
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .name("metadata.json")
```

### vectorize (Optional)

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

* Type: `null`

Vectorize a file on upload, requires a group to be used as well.

```typescript {8} theme={null}
const upload = await pinata.upload.private
  .json({
      content: "console.log('hello world!)",
      name: "helloworld.ts",
      lang: "ts"
  })
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
  .vectorize()
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {7} theme={null}
const upload = await pinata.upload.private
    .json({
        content: "console.log('hello world!)",
        name: "helloworld.ts",
        lang: "ts"
    })
  .key("GENERATED_API_JWT")
```


# url
Source: https://docs.pinata.cloud/sdk/upload/private/url

`org:files:write`

Upload the contents of a URL to Private IPFS

## Usage

```typescript 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.private.url("https://i.imgur.com/u4mGk5b.gif")
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### url

* Type: `string`

Accepts a URL in the form of a string. The mimetype of the URL body provided in the headers typically determines the resulting file.

```typescript theme={null}
const upload = await pinata.upload.private.url("https://i.imgur.com/u4mGk5b.gif")
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .url("https://i.imgur.com/u4mGk5b.gif")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .url("https://i.imgur.com/u4mGk5b.gif")
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.private
  .url("https://i.imgur.com/u4mGk5b.gif")
  .name("pinnie.gif")
```

### vectorize (Optional)

<Warning>
  The file vectors feature is still in beta. Please contact the team at <a href="mailto:team@pinata.cloud">[team@pinata.cloud](mailto:team@pinata.cloud)</a> if you have any issues.
</Warning>

* Type: `null`

Vectorize a file on upload, requires a group to be used as well.

```typescript {4} theme={null}
const upload = await pinata.upload.private
  .url("https://i.imgur.com/u4mGk5b.gif")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
  .vectorize()
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .url(url)
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.private
  .url("https://i.imgur.com/u4mGk5b.gif")
  .key("GENERATED_API_JWT")
```


# base64
Source: https://docs.pinata.cloud/sdk/upload/public/base64

`org:files:write`

Upload a base64 string to Pinata

## Usage

```typescript 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")
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### base64

* Type: `string`

Accepts a string encoded in base64

```typescript theme={null}
const upload = await pinata.upload.public.base64("SGVsbG8gV29ybGQh")
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .base64("SGVsbG8gV29ybGQh")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .base64("SGVsbG8gV29ybGQh")
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .base64("SGVsbG8gV29ybGQh")
  .name("hello.txt")
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .base64("SGVsbG8gV29ybGQh")
  .key("GENERATED_API_JWT")
```


# cid
Source: https://docs.pinata.cloud/sdk/upload/public/cid

`org:files:write`

Pin an existing CID on IPFS to your Pinata account

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const pin = await pinata.upload.public.cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
```

## Returns

```typescript theme={null}
type PinByCIDResponse = {
	id: string;
	cid: string;
	status: "prechecking" | "retrieving";
	name: string;
};
```

## Parameters

### cid

* Type: `string`

Target CID already on IPFS

```typescript theme={null}
const pin = await pinata.upload.public.cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
  .name("hello.txt")
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript theme={null}
const pin = await pinata.upload.public
  .cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript theme={null}
const pin = await pinata.upload.public
  .cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
  .key("GENERATED_API_JWT")
```

### peerAddress (Optional)

* Type: `string[]`

Accepts an array of host node IDs to peer with

```typescript theme={null}
const pin = await pinata.upload
  .cid("QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng")
  .peerAddress([
    "/ip4/hostNode1ExternalIP/tcp/4001/ipfs/hostNode1PeerId",
    "/ip4/hostNode2ExternalIP/tcp/4001/ipfs/hostNode2PeerId"
  ])
```


# createSignedURL
Source: https://docs.pinata.cloud/sdk/upload/public/create-signed-url

`org:files:write`

Create a signed upload URL for a file upload. This method is ideal when you want to authorize server side but upload on the client; check out [this doc](/files/uploading-files#client-side-uploads) for more details.

<Note>
  All the information about the upload such as the name, group, keyvalues, etc. must be part of the signed URL in order for those values to go through. If you have a basic signed URL with no extra parameters and then try to add fields like name, group, etc. they will not go through.
</Note>

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const signedURL = await pinata.upload.public
  .createSignedURL({
    expires: 30
  });

const upload = await pinata.upload.public
  .file(file)
  .url(signedURL)
```

## Returns

* Type: `string`

The full signed URL

```
https://uploads.pinata.cloud/v3/files/209bb44c-e987-47c4-a9e3-a88e02d4d8e9?X-Algorithm=PINATA2&X-Date=1734379351&X-Expires=300&X-Method=%5B%22HEAD%22%2C%22PATCH%22%2C%22POST%22%5D&X-User-ID=eb73db21-10fa-4005-b97c-639e19e82d60&X-Signature=<SIGNATURE>
```

## Parameters

### 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<string, string>`

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
});
```


# file
Source: https://docs.pinata.cloud/sdk/upload/public/file

`org:files:write`

Upload a single file to Pinata

## Usage

```typescript 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 world!"], "hello.txt", { type: "text/plain" })
const upload = await pinata.upload.public.file(file)
```

### 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`.

```typescript {10-12} 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);
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### file

* Type: `File` object

Accepts a File object in accordance with the [W3C File API](https://w3c.github.io/FileAPI/#file-section).

```typescript {3} theme={null}
const blob = new Blob(["hello world!"], { type: "text/plain" })
const file = new File([blob], "hello.txt", { type: "text/plain" })
const upload = await pinata.upload.public.file(file)
```

In most environments you can also pass a Blob here as well.

```typescript {2} theme={null}
const blob = new Blob(["hello world!"], { type: "text/plain" })
const upload = await pinata.upload.public.file(blob)
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .name("hello.txt")
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .key("GENERATED_API_JWT")
```

### car (Optional)

* Type: `boolean`

Upload a file as a raw CAR file

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .car()
```

### expires (Optional)

* Type: `number`

Set an expiration date for the file using a Unix timestamp. The timestamp must be in the future. Once the expiration time is reached, the file will be automatically deleted.

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .file(file)
  .expires(1735689600)
```


# fileArray
Source: https://docs.pinata.cloud/sdk/upload/public/file-array

`org:files:write`

Upload an array of files to Pinata as a folder

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

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])
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### file

* Type: `File[]` object

Accepts an array of File objects in accordance with the [W3C File API](https://w3c.github.io/FileAPI/#file-section).

```typescript theme={null}
const file1 = new File([blob1], "hello1.txt", { type: "text/plain" })
const file2 = new File([blob2], "hello2.txt", { type: "text/plain" })
const upload = await pinata.upload.public.fileArray([file1, file2])
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .fileArray([file1, file2])
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .fileArray([file1, file2])
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .fileArray([file1, file2])
  .name("folder")
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .fileArray([file1, file2])
  .key("GENERATED_API_JWT")
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .fileArray([file1, file2])
  .url(url)
```


# json
Source: https://docs.pinata.cloud/sdk/upload/public/json

`org:files:write`

Upload a JSON object to Pinata

## Usage

```typescript 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({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
})
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### JSON

* Type: `Record<string, unknown>`

Accepts an object that is turned into JSON

```typescript theme={null}
const upload = await pinata.upload.public.json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
})
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {7} theme={null}
const upload = await pinata.upload.public
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .name("metadata.json")
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .json({
    content: "console.log('hello world!)",
    name: "helloworld.ts",
    lang: "ts"
  })
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {7} theme={null}
const upload = await pinata.upload.public
    .json({
        content: "console.log('hello world!)",
        name: "helloworld.ts",
        lang: "ts"
    })
  .key("GENERATED_API_JWT")
```


# url
Source: https://docs.pinata.cloud/sdk/upload/public/url

`org:files:write`

Upload the contents of a URL to Pinata

## Usage

```typescript 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")
```

## Returns

```typescript theme={null}
type UploadResponse = {
	id: string;
	name: string;
	cid: string;
	size: number;
	created_at: string;
	number_of_files: number;
	mime_type: string;
	group_id: string | null;
	keyvalues: {
		[key: string]: string;
	};
	vectorized: boolean;
	network: string;
};
```

## Parameters

### url

* Type: `string`

Accepts a URL in the form of a string. The mimetype of the URL body provided in the headers typically determines the resulting file.

```typescript theme={null}
const upload = await pinata.upload.public.url("https://i.imgur.com/u4mGk5b.gif")
```

### group (Optional)

* Type: `string`

Upload to a specific group by passing in the `groupId`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .url("https://i.imgur.com/u4mGk5b.gif")
  .group("b07da1ff-efa4-49af-bdea-9d95d8881103")
```

### keyvalues (Optional)

* Type: `Record<string, string>`

Add optional keyvalues to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .url("https://i.imgur.com/u4mGk5b.gif")
  .keyvalues({
    env: "prod"
  })
```

### name (Optional)

* Type: `string`

Add optional name to file

```typescript {3-8} theme={null}
const upload = await pinata.upload.public
  .url("https://i.imgur.com/u4mGk5b.gif")
  .name("pinnie.gif")
```

### url (Optional)

* Type: `string`

Pass in a presigned upload URL created with [createSignedURL](./create-signed-url)

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .url(url)
  .url(url)
```

### key (Optional)

* Type: `string`

Upload a file using a secondary API key generated through `keys.create()`

```typescript {3} theme={null}
const upload = await pinata.upload.public
  .url("https://i.imgur.com/u4mGk5b.gif")
  .key("GENERATED_API_JWT")
```


# add
Source: https://docs.pinata.cloud/sdk/x402/cids/add



Associate a CID with a payment instruction for x402 monetization

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const association = await pinata.x402.addCid(
  "01234567-89ab-cdef-0123-456789abcdef",
  "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
);
```

## Returns

```typescript theme={null}
type CidAssociationResponse = {
  data: CidAssociation;
};

type CidAssociation = {
  payment_instruction_id: string;
  cid: string;
  created_at: string;
};
```

## Parameters

### paymentInstructionId (required)

* Type: `string`

The unique identifier of the payment instruction to associate the CID with

```typescript theme={null}
const association = await pinata.x402.addCid(
  "01234567-89ab-cdef-0123-456789abcdef",
  "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
);
```

### cid (required)

* Type: `string`

The IPFS CID to associate with the payment instruction. Once associated, accessing this content will require payment according to the payment instruction.

```typescript theme={null}
const association = await pinata.x402.addCid(
  "01234567-89ab-cdef-0123-456789abcdef",
  "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
);
```


# list
Source: https://docs.pinata.cloud/sdk/x402/cids/list



List all CIDs associated with a payment instruction

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const cids = await pinata.x402.listCids(
  "01234567-89ab-cdef-0123-456789abcdef"
);
```

## Returns

```typescript theme={null}
type CidListResponse = {
  data: {
    cids: string[];
    next_page_token?: string;
  };
};
```

## Parameters

### paymentInstructionId (required)

* Type: `string`

The unique identifier of the payment instruction

```typescript theme={null}
const cids = await pinata.x402.listCids(
  "01234567-89ab-cdef-0123-456789abcdef"
);
```

### options

Optional parameters for pagination.

#### limit

* Type: `number`

Limit the number of results returned

```typescript {3-5} theme={null}
const cids = await pinata.x402.listCids(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    limit: 10,
  }
);
```

#### pageToken

* Type: `string`

Paginate through results using a page token from a previous response

```typescript {3-5} theme={null}
const cids = await pinata.x402.listCids(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    pageToken: "MDE5MWIzZWYtM2U0Zi03YTY5LWE3OTQtOTRhZDE5NjQxMTk0",
  }
);
```


# remove
Source: https://docs.pinata.cloud/sdk/x402/cids/remove



Remove a CID association from a payment instruction

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const result = await pinata.x402.removeCid(
  "01234567-89ab-cdef-0123-456789abcdef",
  "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
);
```

## Returns

```typescript theme={null}
type CidRemoveResponse = {
  data: {};
};
```

## Parameters

### paymentInstructionId (required)

* Type: `string`

The unique identifier of the payment instruction

```typescript theme={null}
const result = await pinata.x402.removeCid(
  "01234567-89ab-cdef-0123-456789abcdef",
  "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
);
```

### cid (required)

* Type: `string`

The IPFS CID to remove from the payment instruction. Once removed, the content will no longer require payment for access through this instruction.

```typescript theme={null}
const result = await pinata.x402.removeCid(
  "01234567-89ab-cdef-0123-456789abcdef",
  "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4"
);
```


# x402 Monetization
Source: https://docs.pinata.cloud/sdk/x402/index



<Note>
  x402 is available on paid Pinata accounts. [Upgrade your account](https://app.pinata.cloud/billing) to access x402 monetization features.
</Note>

Monetize your private IPFS files by receiving USDC payments directly to your wallet using the x402 protocol.

For detailed concepts and workflows, see the [x402 Introduction](/files/x402/intro).

## Quick Example

```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 Access",
  payment_requirements: [
    {
      asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
      pay_to: "0xYourWalletAddress", // YOU receive payments here
      network: "base",
      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}
```

## Network Configuration

**USDC is currently the only supported token.**

| Network                | USDC Token Address                           | Use Case                |
| ---------------------- | -------------------------------------------- | ----------------------- |
| Base (Mainnet)         | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | Production monetization |
| Base Sepolia (Testnet) | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` | Testing payment flows   |

## Payment Amounts

USDC has 6 decimals. To convert USD to token amount, multiply by 1,000,000:

| USD    | `amount`    |
| ------ | ----------- |
| \$0.01 | `"10000"`   |
| \$0.10 | `"100000"`  |
| \$1.00 | `"1000000"` |

## Available Methods

### Payment Instructions

* [list](/sdk/x402/payment-instructions/list) - List all payment instructions
* [create](/sdk/x402/payment-instructions/create) - Create a new payment instruction
* [get](/sdk/x402/payment-instructions/get) - Get a payment instruction by ID
* [update](/sdk/x402/payment-instructions/update) - Update a payment instruction
* [delete](/sdk/x402/payment-instructions/delete) - Delete a payment instruction

### CID Management

* [list](/sdk/x402/cids/list) - List CIDs attached to a payment instruction
* [add](/sdk/x402/cids/add) - Attach a CID to a payment instruction
* [remove](/sdk/x402/cids/remove) - Remove a CID from a payment instruction

## Related Resources

* [x402 Introduction](/files/x402/intro) - Overview of x402 monetization
* [Quick Start Guide](/files/x402/x402-quick-start) - Step-by-step setup guide
* [Payment Instructions Guide](/files/x402/x402-payment-instructions) - Detailed workflow guide
* [Accessing Paid Content](/files/x402/x402-accessing-paid-content) - How requesters access paid content
* [x402 Types](/sdk/types/x402) - TypeScript type definitions


# create
Source: https://docs.pinata.cloud/sdk/x402/payment-instructions/create



Create a new payment instruction for x402 monetization

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const instruction = await pinata.x402.createPaymentInstruction({
  name: "Premium Content Access",
  payment_requirements: [
    {
      asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
      pay_to: "0x1234567890abcdef1234567890abcdef12345678",
      network: "base",
      amount: "10000", // $0.01 in USDC (6 decimals)
    },
  ],
});
```

## Returns

```typescript theme={null}
type PaymentInstructionResponse = {
  data: PaymentInstruction;
};

type PaymentInstruction = {
  id: string;
  version: number;
  payment_requirements: PaymentRequirement[];
  name: string;
  description?: string;
  created_at: string;
};

type PaymentRequirement = {
  asset: string;
  pay_to: string;
  network: "base" | "base-sepolia" | "eip155:8453" | "eip155:84532";
  amount: string;
  description?: string;
};
```

## Parameters

### name (required)

* Type: `string`

The name for the payment instruction

```typescript {2} theme={null}
const instruction = await pinata.x402.createPaymentInstruction({
  name: "Premium Content Access",
  payment_requirements: [
    {
      asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
      pay_to: "0x1234567890abcdef1234567890abcdef12345678",
      network: "base",
      amount: "10000", // $0.01 in USDC (6 decimals)
    },
  ],
});
```

### payment\_requirements (required)

* Type: `PaymentRequirement[]`

An array of payment requirements. Each requirement specifies the asset, recipient address, network, and amount required for access.

```typescript {3-9} theme={null}
const instruction = await pinata.x402.createPaymentInstruction({
  name: "Premium Content Access",
  payment_requirements: [
    {
      asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
      pay_to: "0x1234567890abcdef1234567890abcdef12345678",
      network: "base",
      amount: "10000", // $0.01 in USDC (6 decimals)
    },
  ],
});
```

#### PaymentRequirement Properties

| Property      | Type                                                                | Required | Description                                     |
| ------------- | ------------------------------------------------------------------- | -------- | ----------------------------------------------- |
| `asset`       | `string`                                                            | Yes      | The USDC token contract address for the network |
| `pay_to`      | `string`                                                            | Yes      | The wallet address to receive payments          |
| `network`     | `"base"` \| `"base-sepolia"` \| `"eip155:8453"` \| `"eip155:84532"` | Yes      | The blockchain network                          |
| `amount`      | `string`                                                            | Yes      | The maximum amount required for access          |
| `description` | `string`                                                            | No       | Optional description for this requirement       |

### description

* Type: `string`

An optional description for the payment instruction

```typescript {10} theme={null}
const instruction = await pinata.x402.createPaymentInstruction({
  name: "Premium Content Access",
  payment_requirements: [
    {
      asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
      pay_to: "0x1234567890abcdef1234567890abcdef12345678",
      network: "base",
      amount: "10000", // $0.01 in USDC (6 decimals)
    },
  ],
  description: "Access to exclusive premium content",
});
```


# delete
Source: https://docs.pinata.cloud/sdk/x402/payment-instructions/delete



Delete a payment instruction

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const result = await pinata.x402.deletePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef"
);
```

## Returns

```typescript theme={null}
type PaymentInstructionDeleteResponse = {
  data: {};
};
```

## Parameters

### id (required)

* Type: `string`

The unique identifier of the payment instruction to delete

```typescript theme={null}
const result = await pinata.x402.deletePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef"
);
```


# get
Source: https://docs.pinata.cloud/sdk/x402/payment-instructions/get



Retrieve a specific payment instruction by ID

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const instruction = await pinata.x402.getPaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef"
);
```

## Returns

```typescript theme={null}
type PaymentInstructionResponse = {
  data: PaymentInstruction;
};

type PaymentInstruction = {
  id: string;
  version: number;
  payment_requirements: PaymentRequirement[];
  name: string;
  description?: string;
  created_at: string;
};

type PaymentRequirement = {
  asset: string;
  pay_to: string;
  network: "base" | "base-sepolia" | "eip155:8453" | "eip155:84532";
  amount: string;
  description?: string;
};
```

## Parameters

### id (required)

* Type: `string`

The unique identifier of the payment instruction to retrieve

```typescript theme={null}
const instruction = await pinata.x402.getPaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef"
);
```


# list
Source: https://docs.pinata.cloud/sdk/x402/payment-instructions/list



List and filter payment instructions for x402 monetization

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const instructions = await pinata.x402.listPaymentInstructions();
```

## Returns

```typescript theme={null}
type PaymentInstructionListResponse = {
  data: {
    payment_instructions: PaymentInstruction[];
    next_page_token?: string;
  };
};

type PaymentInstruction = {
  id: string;
  version: number;
  payment_requirements: PaymentRequirement[];
  name: string;
  description?: string;
  created_at: string;
};

type PaymentRequirement = {
  asset: string;
  pay_to: string;
  network: "base" | "base-sepolia" | "eip155:8453" | "eip155:84532";
  amount: string;
  description?: string;
};
```

## Parameters

Filter response with the following optional parameters.

### limit

* Type: `number`

Limit the number of results returned

```typescript {2-4} theme={null}
const instructions = await pinata.x402.listPaymentInstructions({
  limit: 10,
});
```

### pageToken

* Type: `string`

Paginate through results using a page token from a previous response

```typescript {2-4} theme={null}
const instructions = await pinata.x402.listPaymentInstructions({
  pageToken: "MDE5MWIzZWYtM2U0Zi03YTY5LWE3OTQtOTRhZDE5NjQxMTk0",
});
```

### cid

* Type: `string`

Filter payment instructions by associated CID

```typescript {2-4} theme={null}
const instructions = await pinata.x402.listPaymentInstructions({
  cid: "bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4",
});
```

### name

* Type: `string`

Filter payment instructions by name

```typescript {2-4} theme={null}
const instructions = await pinata.x402.listPaymentInstructions({
  name: "Premium Content",
});
```

### id

* Type: `string`

Filter by specific payment instruction ID

```typescript {2-4} theme={null}
const instructions = await pinata.x402.listPaymentInstructions({
  id: "01234567-89ab-cdef-0123-456789abcdef",
});
```


# update
Source: https://docs.pinata.cloud/sdk/x402/payment-instructions/update



Update an existing payment instruction

## Usage

```typescript theme={null}
import { PinataSDK } from "pinata";

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: "example-gateway.mypinata.cloud",
});

const instruction = await pinata.x402.updatePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    name: "Updated Premium Content",
  }
);
```

## Returns

```typescript theme={null}
type PaymentInstructionResponse = {
  data: PaymentInstruction;
};

type PaymentInstruction = {
  id: string;
  version: number;
  payment_requirements: PaymentRequirement[];
  name: string;
  description?: string;
  created_at: string;
};

type PaymentRequirement = {
  asset: string;
  pay_to: string;
  network: "base" | "base-sepolia" | "eip155:8453" | "eip155:84532";
  amount: string;
  description?: string;
};
```

## Parameters

### id (required)

* Type: `string`

The unique identifier of the payment instruction to update

```typescript theme={null}
const instruction = await pinata.x402.updatePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    name: "Updated Name",
  }
);
```

### name

* Type: `string`

Update the name of the payment instruction

```typescript {4} theme={null}
const instruction = await pinata.x402.updatePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    name: "Updated Premium Content",
  }
);
```

### payment\_requirements

* Type: `PaymentRequirement[]`

Update the payment requirements array

```typescript {4-11} theme={null}
const instruction = await pinata.x402.updatePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    payment_requirements: [
      {
        asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
        pay_to: "0xnewaddress1234567890abcdef12345678",
        network: "base",
        amount: "20000", // $0.02 in USDC (6 decimals)
      },
    ],
  }
);
```

#### PaymentRequirement Properties

| Property      | Type                                                                | Required | Description                                     |
| ------------- | ------------------------------------------------------------------- | -------- | ----------------------------------------------- |
| `asset`       | `string`                                                            | Yes      | The USDC token contract address for the network |
| `pay_to`      | `string`                                                            | Yes      | The wallet address to receive payments          |
| `network`     | `"base"` \| `"base-sepolia"` \| `"eip155:8453"` \| `"eip155:84532"` | Yes      | The blockchain network                          |
| `amount`      | `string`                                                            | Yes      | The amount required for access                  |
| `description` | `string`                                                            | No       | Optional description for this requirement       |

### description

* Type: `string`

Update the description of the payment instruction

```typescript {4} theme={null}
const instruction = await pinata.x402.updatePaymentInstruction(
  "01234567-89ab-cdef-0123-456789abcdef",
  {
    description: "Updated description for the payment instruction",
  }
);
```


# Create Gateway Host Origin Restriction
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/create-gateway-host-origin-restriction

post /gateways/{id}/hosts
`org:gateways:write`




# Get Marketplace Plugin Details
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/get-marketplace-plugin-details

get /gateways/plugins/marketplace/{id}
`org:gateways:read`




# Install Gateway Plugin
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/install-gateway-plugin

post /gateways/plugins/{gateway_id}
`org:gateways:write`




# List Gateway Plugins
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/list-gateway-plugins

get /gateways/plugins
`org:gateways:read`




# List Installed Plugins for Gateway
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/list-installed-plugins-for-gateway

get /ipfs/gateway_plugins/{gateway_id}
`org:gateways:read`




# List Marketplace Plugins
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/list-marketplace-plugins

get /gateways/plugins/marketplace
`org:gateways:read`




# Revoke Gateway Key
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/revoke-gateway-key

delete /gateways/{id}/access_tokens/{access_token_id}
`org:gateways:write`




# Revoke Host Origin Restriction
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/revoke-host-origin-restriction

delete /gateways/{id}/hosts/{host_id}
`org:gateways:write`




# Revoke IP Address Restricton
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/revoke-ip-address-restricton

delete /gateways/{id}/ips/{ip_id}
`org:gateways:write:




# Time Interval Gateway Analytics
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/time-interval-gateway-analytics

get /analytics/gateways/time_series
`org:analytics:read`




# Top Gateway Analytics
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/top-gateway-analytics

get /analytics/gateways/top
`org:analytics:read`




# Uninstall Gateway Plugin
Source: https://docs.pinata.cloud/api-reference/endpoint/ipfs/uninstall-gateway-plugin

delete /gateways/plugins/{gateway_id}/plugin/{plugin_id}
`org:gateways:write`




# Add CID to Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/cids-add

put /x402/payment_instructions/{id}/cids/{cid}
Associates a CID with a payment instruction



# List CIDs for Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/cids-list

get /x402/payment_instructions/{id}/cids
Returns a paginated list of CIDs associated with a payment instruction



# Remove CID from Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/cids-remove

delete /x402/payment_instructions/{id}/cids/{cid}
Removes a CID association from a payment instruction



# Create Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/payment-instructions-create

post /x402/payment_instructions
Creates a new payment instruction



# Delete Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/payment-instructions-delete

delete /x402/payment_instructions/{id}
Deletes a payment instruction (soft delete). All CID mappings must be deleted first.



# Get Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/payment-instructions-get

get /x402/payment_instructions/{id}
Retrieves a specific payment instruction by ID



# List Payment Instructions
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/payment-instructions-list

get /x402/payment_instructions
Returns a paginated list of payment instructions



# Update Payment Instruction
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/payment-instructions-update

patch /x402/payment_instructions/{id}
Updates an existing payment instruction



# Upload a File
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/pin

pinata-x402-api-v1.yaml post /pin/{network}
Uploads a file to Pinata's public IPFS network

## Cost

| Price           | Duration           |
| --------------- | ------------------ |
| \$0.10/GB \* 12 | Pins for 12 months |

## Example Usage

In order to access these endpoints you will need to use either [`@x402/axios`](https://www.npmjs.com/package/@x402/axios) or [`@x402/fetch`](https://www.npmjs.com/package/@x402/fetch). Once installed you will also need either [Viem](https://viem.sh) or a Coinbase developer account. From there you can create an `account` locally or through the CDP Wallet API.

When you make a request to one of the Pinata x402 endpoints it will return a 402 error saying payment is required. Then the `fetchWithPayment` method from the fetch or axios library will make a second requst for the requested payment amount. After payment is settled then you can use the returned presigned URL to upload the file to Pinata.

```typescript theme={null}
import { wrapFetchWithPayment, decodeXPaymentResponse } from "@x402/fetch";
import { account } from "./viem";

const fetchWithPayment = wrapFetchWithPayment(fetch, account);

const url = "https://402.pinata.cloud/v1/pin/public";

fetchWithPayment(url, {
  method: "POST",
  body: JSON.stringify({
    fileSize: 5000000,
  }),
})
  .then(async (response) => {
    const body = (await response.json()) as { url: string };
    console.log(body);

    const uuid = crypto.randomUUID();

    const file = new File([`Paid and pinned by 402.pinata.cloud: ${uuid}`], "file.txt");

    const data = new FormData();
    data.append("network", "public");
    data.append("file", file);

    const uploadReq = await fetch(body.url, {
      method: "POST",
      body: data,
    });

    const uploadRes = await uploadReq.json();
    console.log(uploadRes);
  })
  .catch((error) => {
    console.error(error.response?.data?.error);
  });
```

Uploading files to Public IPFS means you can access them through a gateway like `https://ipfs.io/ipfs/:CID`. If you upload a file as `private` then it will not be accessible on public IPFS, so in order to access it you need to create a temporary access URL. This flow is similar to the previous one, except you would provide the CID that you uploded previously that you would like to access. After a successful payment the server will return a URL you can access the file with.

```typescript theme={null}
import { wrapFetchWithPayment, decodeXPaymentResponse } from "@x402/fetch";
import { account } from "./viem";

const fetchWithPayment = wrapFetchWithPayment(fetch, account);

const url =
  "https://402.pinata.cloud/v1/retrieve/private/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4";

fetchWithPayment(url, {
  method: "GET",
})
  .then(async (response) => {
    const body = (await response.json()) as { url: string };
    console.log(body);
  })
  .catch((error) => {
    console.error(error.response?.data?.error);
  });
```


# Retrieve a Private File
Source: https://docs.pinata.cloud/api-reference/endpoint/x402/retrieve

pinata-x402-api-v1.yaml get /retrieve/private/{cid}
Retrieves a private file from IPFS by its CID

## Cost

| Price    | Duration    |
| -------- | ----------- |
| \$0.0001 | Per request |

## Example Usage

If you upload a file as `private` then it will not be accessible on public IPFS, so in order to access it you need to create a temporary access URL. This flow is similar to the previous one, except you would provide the CID that you uploded previously that you would like to access. After a successful payment the server will return a URL you can access the file with.

```typescript theme={null}
import { wrapFetchWithPayment, decodeXPaymentResponse } from "@x402/fetch";
import { account } from "./viem";

const fetchWithPayment = wrapFetchWithPayment(fetch, account);

const url =
	"https://402.pinata.cloud/v1/retrieve/private/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4";

fetchWithPayment(url, {
	method: "GET",
})
	.then(async (response) => {

		const body = (await response.json()) as { url: string };
		console.log(body);

	})
	.catch((error) => {
		console.error(error.response?.data?.error);
	});
```


# Introduction
Source: https://docs.pinata.cloud/api-reference/introduction

Getting started with the Pinata API

Welcome to the Pinata API Reference! Before you can do anything in the API, you'll need some API Keys.

## API Keys

Visit the [Pinata API Keys](https://app.pinata.cloud/developers/api-keys) page to generate new keys.

<img />

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, so be sure to give it a name to keep track of it. Once you have that filled out, click "Generate API 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.

<Warning>
  Once API keys have been created, you will not be able to see the secret or JWT
  again
</Warning>

Once you have created your keys you can go ahead and try testing them! Try to paste this into your terminal with your `JWT`

```bash 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!"
}
```