While building a Farcaster client one of the most common things users will request after the ability to send casts is to add images or media to their casts, and thankfully Pinata makes this a breeze! In this doc we’ll show you how you can add Pinata IPFS uploads to your app no matter what stack you’re using.

The following guide will use Typescript but since this is using the Pinata API you can use whatever language you would like.

API Keys

The first thing you’ll need to do is visit the Pinata API Keys page to generate an API key.

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.

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

Once you have created your keys you can go ahead and try testing them! Try to paste this into your terminal with your JWT

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!

bash
{
  "message": "Congratulations! You are communicating with the Pinata API!"
}

Adding Uploads to Your Client

There are several approaches you can take to keep your API keys safe but the primary two we reccomend are:

  1. Send the file to your owner server via API so the server can upload to Pinata
  2. Create a temporary key using /users/generateApiKey on the server and upload via the client

In this guide we’ll show you second approach as it helps reduce the friction experienced when uploading larger file sizes (e.g. Next.js / Vercel has a limit of 4mb that can be sent via their API routes)

1

Key Generation

In our server code we’ll need a few endpoints with functions that will generate a temporary API key thats only valid for one use, then another to revoke the key just to be extra safe. If you were writing this in an API framework like Hono on a Cloudflare worker it would looke something like this.

// GET route to fetch a key
app.get("/key", async (c) => {
  try {
    const id = uuidv4();
    const body = JSON.stringify({
      keyName: id.toString(),
      permissions: {
        endpoints: {
          pinning: {
            pinFileToIPFS: true
          },
        },
      },
      maxUses: 1,
    });
    const keyRes = await fetch(
      "https://api.pinata.cloud/users/generateApiKey",
      {
        method: "POST",
        body: body,
        headers: {
          accept: "application/json",
          "content-type": "application/json",
          authorization: `Bearer ${c.env.PINATA_JWT}`,
        },
      },
    );
    const keyResJson: any = await keyRes.json();
    console.log(keyResJson);
    const keyData = {
      pinata_api_key: keyResJson.pinata_api_key,
      JWT: keyResJson.JWT,
    };
    return c.json(keyData, { status: 200 });
  } catch (error) {
    console.log(error);
    return c.json({ text: "Error creating API Key:" }, { status: 500 });
  }
});

// PUT route to revoke the key
app.put("/key", async (c) => {
  const keyId = c.req.query("keyId");
  const keyData = JSON.stringify({
    apiKey: keyId
  });
  try {
    const keyDelete = await fetch(
      "https://api.pinata.cloud/users/revokeApiKey",
      {
        method: "PUT",
        body: keyData,
        headers: {
          accept: "application/json",
          "content-type": "application/json",
          authorization: `Bearer ${c.env.PINATA_JWT}`,
        },
      },
    );
    const keyDeleteRes: any = await keyDelete.json();
    console.log(keyDeleteRes);
    return c.json(keyDeleteRes);
  } catch (error) {
    console.log(error);
    return c.json({ text: "Error Deleting API Key:" }, { status: 500 });
  }
});

In this code we have two endpoints, one for creating and one for revoking. In the creation we make an object with the key permissions, which in this case is just to pin a file and will only work once. After creating it we send back the key and the JWT which we’ll see used shortly. Then in the revoking we take that same pinata_api_key as the value for apiKey to identify what key we will be revoking. That’s it!

On the client side code, the functions to create and revoke that key might look something like this.

export const generatePinataKey = async () => {
  try {
    const tempKey = await fetch(`${SERVER_URL}/key`, {
      method: 'GET',
      headers: {
        "Content-Type": "application/json",
      },
    });
    const keyData = await tempKey.json();
    return keyData;
  } catch (error) {
    console.log("error making API key:", error);
    throw error;
  }
};

export async function deleteKey(keyId: string) {
  try {
    const deleteKey = await fetch(`${SERVER_URL}/key?keyId=${keyId}`, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json",
      },
    });
    const deleteJson = await deleteKey.json();
    console.log(deleteJson);
  } catch (error) {
    console.log("Error deleting API key:", error);
  }
}
2

Uploading the File

Now that we have a temporary API key on the client, we can upload our file to IPFS from a client form. There are multiple ways to do different types of file uploads where are documented further here, but in short the Pinata API can accept readableStreams from a local file system or blobs. With the code below we’ll do a simple upload where the file is handled via React useState.

import { useState } from "react";

function App() {

  const [selectedFile, setSelectedFile]: any = useState();

  const changeHandler = (event: any) => {
    setSelectedFile(event.target.files[0]);
  };

  const handleSubmission = async (keyData: any) => {
    try {
      const formData = new FormData();
      formData.append("file", selectedFile);
      const metadata = JSON.stringify({
        name: "File name",
      });
      formData.append("pinataMetadata", metadata);

      const res = await fetch(
        "https://api.pinata.cloud/pinning/pinFileToIPFS",
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${keyData.JWT}`,
          },
          body: formData,
        }
      );
      const resData = await res.json();
      console.log(resData);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <>
      <label className="form-label"> Choose File</label>
      <input type="file" onChange={changeHandler} />
      <button onClick={handleSubmission}>Submit</button>
    </>
  );
}

export default App;

Here we just pass in the keyData we got from our server to access our one time use JWT and send the request to Pinata. Once the upload is complete we’ll get a response that looks like this:

{
  "IpfsHash": "bafkreicnpgfq256elalpa6x6avqti3txb6dphsgqekpdkkxq3frjbph3de",
  "PinSize": 9719,
  "Timestamp": "2024-05-14T18:29:30.541Z",
  "isDuplicate": true
}

The IpfsHash or CID is both the identifier and address for our content which we’ll access soon in sending a cast.

3

Adding the File Link to a Cast

Now that our content is uploaded, we can access it through our Dedicated Gateway using the following pattern:

https://your-gateway-domain.mypinata.cloud/ipfs/:cid

Something else you might want to do is use the ?filename= query at the end of the url to designate a filetype.

You can use the mimetype of the headers to help determine what file extension you want to use! Check out this example

Here is an example of a fully functional Gateway URL

https://dweb.mypinata.cloud/ipfs/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4?filename=pinning.png

With this full image URL you can add it as a url object in the embeds array when sending a cast like so:

{
  "embeds": [
    {
      "url": "https://dweb.mypinata.cloud/ipfs/bafkreih5aznjvttude6c3wbvqeebb6rlx5wkbzyppv7garjiubll2ceym4?filename=pinning.png"
    }
  ]
}

If you would like to see a fully completed client example using this method check out our open source Lite Client repo!