· Nacho Coll · Tutorials  · 10 min read

Python IPFS Upload: Complete Guide with Code Examples

Upload files to IPFS from Python using the IPFS.NINJA REST API. Complete guide with requests library examples.

Upload files to IPFS from Python using the IPFS.NINJA REST API. Complete guide with requests library examples.

Python developers looking to upload files to IPFS don’t need to run a local IPFS node or deal with complex library dependencies. While IPFS.NINJA doesn’t provide a dedicated Python SDK, its REST API is perfectly suited for Python applications using the built-in requests library.

This guide shows you how to upload files, images, and JSON data to IPFS from Python, manage your pinned files, and leverage advanced features like signed upload tokens for secure client-side uploads.

IPFS Ninja

Why Choose IPFS.NINJA for Python Development?

Before diving into code examples, let’s understand why IPFS.NINJA is an excellent choice for Python developers:

  • No SDK dependencies: Use Python’s built-in requests library
  • RESTful API: Clean, predictable endpoints that follow HTTP standards
  • JSON-native: Upload JSON data directly without file conversion
  • Multiple authentication methods: API keys, signed tokens, or JWT
  • Built-in analytics: Track upload performance and usage
  • Custom gateways: Branded IPFS access for your applications

If you’re new to IPFS pinning concepts, check out our comprehensive guide on what IPFS pinning is and how to upload files to IPFS.

Setting Up Your Python Environment

First, ensure you have Python 3.6+ and install the required libraries:

pip install requests

For image processing examples later in this guide:

pip install Pillow  # For image handling

Authentication Setup

All IPFS.NINJA API calls require authentication. The simplest method for Python applications is using API keys with the X-Api-Key header.

import requests
import json
import base64

# Your IPFS.NINJA API key (format: bws_ + 32 hex characters)
API_KEY = "bws_1234567890abcdef1234567890abcdef"
BASE_URL = "https://api.ipfs.ninja"

# Default headers for all requests
headers = {
    "X-Api-Key": API_KEY,
    "Content-Type": "application/json"
}

Get your API key from the IPFS.NINJA dashboard after creating your account.

Uploading JSON Data to IPFS

One of IPFS.NINJA’s unique features is native JSON support. You don’t need to convert JSON to files—upload objects directly:

def upload_json_to_ipfs(data, description=None, metadata=None):
    """
    Upload JSON data directly to IPFS
    
    Args:
        data (dict): The JSON data to upload
        description (str, optional): Description for the file
        metadata (dict, optional): Additional metadata
    
    Returns:
        dict: Response containing CID and access URLs
    """
    payload = {
        "content": data,
        "description": description,
        "metadata": metadata
    }
    
    response = requests.post(
        f"{BASE_URL}/upload/new",
        headers=headers,
        json=payload
    )
    
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Upload failed: {response.status_code} - {response.text}")

# Example: Upload a user profile
user_profile = {
    "name": "Alice Smith",
    "bio": "Python developer and IPFS enthusiast",
    "website": "https://alice.dev",
    "social": {
        "github": "alicesmith",
        "twitter": "@alice_dev"
    }
}

try:
    result = upload_json_to_ipfs(
        data=user_profile,
        description="User profile for Alice Smith",
        metadata={"type": "profile", "version": "1.0"}
    )
    
    print(f"✅ Upload successful!")
    print(f"CID: {result['cid']}")
    print(f"Size: {result['sizeMB']} MB")
    print(f"IPFS URL: {result['uris']['ipfs']}")
    print(f"Gateway URL: {result['uris']['url']}")
    
except Exception as e:
    print(f"❌ Upload failed: {e}")

Uploading Files with Base64 Encoding

For binary files like images, documents, or any file type, use base64 encoding:

def upload_file_to_ipfs(file_path, description=None, metadata=None):
    """
    Upload a file to IPFS using base64 encoding
    
    Args:
        file_path (str): Path to the file to upload
        description (str, optional): Description for the file
        metadata (dict, optional): Additional metadata
    
    Returns:
        dict: Response containing CID and access URLs
    """
    try:
        with open(file_path, "rb") as file:
            file_content = base64.b64encode(file.read()).decode('utf-8')
        
        payload = {
            "content": file_content,
            "description": description or f"File upload: {file_path}",
            "metadata": metadata
        }
        
        response = requests.post(
            f"{BASE_URL}/upload/new",
            headers=headers,
            json=payload
        )
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"Upload failed: {response.status_code} - {response.text}")
            
    except FileNotFoundError:
        raise Exception(f"File not found: {file_path}")

# Example: Upload an image
try:
    result = upload_file_to_ipfs(
        file_path="./logo.png",
        description="Company logo",
        metadata={
            "type": "image",
            "format": "png",
            "purpose": "branding"
        }
    )
    
    print(f"✅ Image uploaded successfully!")
    print(f"CID: {result['cid']}")
    print(f"View at: {result['uris']['url']}")
    
except Exception as e:
    print(f"❌ Upload failed: {e}")

Batch Upload Function

For uploading multiple files efficiently:

import os
from pathlib import Path

def batch_upload_files(directory_path, file_extensions=None):
    """
    Upload multiple files from a directory
    
    Args:
        directory_path (str): Directory containing files to upload
        file_extensions (list, optional): Filter by file extensions (e.g., ['.png', '.jpg'])
    
    Returns:
        list: Results from each upload
    """
    results = []
    directory = Path(directory_path)
    
    if not directory.exists():
        raise Exception(f"Directory not found: {directory_path}")
    
    # Get all files or filter by extensions
    if file_extensions:
        files = []
        for ext in file_extensions:
            files.extend(directory.glob(f"*{ext}"))
    else:
        files = [f for f in directory.iterdir() if f.is_file()]
    
    print(f"Found {len(files)} files to upload...")
    
    for file_path in files:
        try:
            print(f"Uploading {file_path.name}...")
            
            result = upload_file_to_ipfs(
                file_path=str(file_path),
                description=f"Batch upload: {file_path.name}",
                metadata={
                    "batch_upload": True,
                    "original_name": file_path.name,
                    "file_size": file_path.stat().st_size
                }
            )
            
            results.append({
                "filename": file_path.name,
                "success": True,
                "cid": result['cid'],
                "url": result['uris']['url']
            })
            
        except Exception as e:
            results.append({
                "filename": file_path.name,
                "success": False,
                "error": str(e)
            })
            print(f"Failed to upload {file_path.name}: {e}")
    
    return results

# Example usage
try:
    results = batch_upload_files("./images", file_extensions=['.png', '.jpg', '.jpeg'])
    
    successful = [r for r in results if r['success']]
    failed = [r for r in results if not r['success']]
    
    print(f"\n✅ Successfully uploaded: {len(successful)} files")
    print(f"❌ Failed uploads: {len(failed)} files")
    
    for result in successful:
        print(f"  - {result['filename']}: {result['url']}")
        
except Exception as e:
    print(f"Batch upload error: {e}")

Managing Your Pinned Files

Retrieve and manage your uploaded files:

def list_pinned_files(limit=50, offset=0):
    """
    List your pinned files
    
    Args:
        limit (int): Number of files to retrieve (max 100)
        offset (int): Number of files to skip
    
    Returns:
        dict: Response containing files list and pagination
    """
    params = {
        "limit": limit,
        "offset": offset
    }
    
    response = requests.get(
        f"{BASE_URL}/files",
        headers=headers,
        params=params
    )
    
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Failed to list files: {response.status_code} - {response.text}")

def get_file_details(cid):
    """
    Get details for a specific file
    
    Args:
        cid (str): The Content Identifier of the file
    
    Returns:
        dict: File details
    """
    response = requests.get(
        f"{BASE_URL}/files/{cid}",
        headers=headers
    )
    
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Failed to get file details: {response.status_code} - {response.text}")

# Example: List recent uploads
try:
    files = list_pinned_files(limit=10)
    
    print(f"Total files: {files.get('total', 0)}")
    print("\nRecent uploads:")
    
    for file_info in files.get('files', []):
        print(f"  - {file_info['cid'][:12]}... | {file_info['sizeMB']} MB | {file_info['description']}")
        
except Exception as e:
    print(f"Failed to list files: {e}")

Pin Existing IPFS Content

Pin content that already exists on IPFS:

def pin_existing_cid(cid, description=None):
    """
    Pin existing IPFS content by CID
    
    Args:
        cid (str): The Content Identifier to pin
        description (str, optional): Description for the pinned content
    
    Returns:
        dict: Response with pin status
    """
    payload = {
        "cid": cid,
        "description": description or f"Pinned existing content: {cid}"
    }
    
    response = requests.post(
        f"{BASE_URL}/pin",
        headers=headers,
        json=payload
    )
    
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Pin failed: {response.status_code} - {response.text}")

# Example: Pin popular NFT metadata
try:
    result = pin_existing_cid(
        cid="QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG",
        description="Popular NFT metadata backup"
    )
    
    print(f"✅ Successfully pinned existing content!")
    print(f"CID: {result['cid']}")
    
except Exception as e:
    print(f"❌ Pin failed: {e}")

Using Signed Upload Tokens

For secure client-side uploads or temporary access, use signed tokens:

def create_upload_token(expires_in_hours=24, max_uses=None):
    """
    Create a signed upload token for temporary uploads
    
    Args:
        expires_in_hours (int): Token expiration time in hours
        max_uses (int, optional): Maximum number of uses (unlimited if None)
    
    Returns:
        dict: Token details including the signed token string
    """
    from datetime import datetime, timedelta
    
    expiry_time = datetime.utcnow() + timedelta(hours=expires_in_hours)
    
    payload = {
        "expiry": expiry_time.isoformat() + "Z",
        "maxUses": max_uses
    }
    
    response = requests.post(
        f"{BASE_URL}/upload-tokens",
        headers=headers,
        json=payload
    )
    
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Token creation failed: {response.status_code} - {response.text}")

def upload_with_token(data, token, description=None):
    """
    Upload using a signed token instead of API key
    
    Args:
        data: Content to upload (dict for JSON, base64 string for files)
        token (str): Signed upload token
        description (str, optional): Description for the upload
    
    Returns:
        dict: Upload response
    """
    token_headers = {
        "Authorization": f"Signed {token}",
        "Content-Type": "application/json"
    }
    
    payload = {
        "content": data,
        "description": description
    }
    
    response = requests.post(
        f"{BASE_URL}/upload/new",
        headers=token_headers,
        json=payload
    )
    
    if response.status_code == 200:
        return response.json()
    else:
        raise Exception(f"Token upload failed: {response.status_code} - {response.text}")

# Example: Create token for client-side uploads
try:
    token_info = create_upload_token(expires_in_hours=1, max_uses=10)
    upload_token = token_info['token']
    
    print(f"Created upload token: {upload_token[:20]}...")
    print(f"Expires: {token_info['expiry']}")
    print(f"Max uses: {token_info.get('maxUses', 'unlimited')}")
    
    # Use the token for an upload
    test_data = {"message": "Hello from signed token!"}
    result = upload_with_token(test_data, upload_token, "Token test upload")
    
    print(f"✅ Token upload successful: {result['cid']}")
    
except Exception as e:
    print(f"❌ Token operation failed: {e}")

Complete Example: NFT Metadata Uploader

Here’s a practical example that combines multiple concepts for NFT metadata management:

class NFTMetadataUploader:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://api.ipfs.ninja"
        self.headers = {
            "X-Api-Key": api_key,
            "Content-Type": "application/json"
        }
    
    def upload_nft_image(self, image_path, name, description=""):
        """Upload NFT image and return IPFS URL"""
        try:
            with open(image_path, "rb") as file:
                image_content = base64.b64encode(file.read()).decode('utf-8')
            
            payload = {
                "content": image_content,
                "description": f"NFT Image: {name}",
                "metadata": {
                    "type": "nft_image",
                    "name": name,
                    "description": description
                }
            }
            
            response = requests.post(
                f"{self.base_url}/upload/new",
                headers=self.headers,
                json=payload
            )
            
            if response.status_code == 200:
                result = response.json()
                return result['uris']['ipfs']  # Return IPFS URI
            else:
                raise Exception(f"Image upload failed: {response.text}")
                
        except Exception as e:
            raise Exception(f"Failed to upload image {image_path}: {e}")
    
    def upload_nft_metadata(self, name, description, image_ipfs_url, attributes=None, external_url=None):
        """Upload complete NFT metadata"""
        metadata = {
            "name": name,
            "description": description,
            "image": image_ipfs_url,
        }
        
        if attributes:
            metadata["attributes"] = attributes
        if external_url:
            metadata["external_url"] = external_url
        
        payload = {
            "content": metadata,
            "description": f"NFT Metadata: {name}",
            "metadata": {
                "type": "nft_metadata",
                "standard": "ERC-721"
            }
        }
        
        response = requests.post(
            f"{self.base_url}/upload/new",
            headers=self.headers,
            json=payload
        )
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"Metadata upload failed: {response.text}")
    
    def create_complete_nft(self, image_path, name, description, attributes=None, external_url=None):
        """Upload image and metadata for a complete NFT"""
        print(f"Creating NFT: {name}")
        
        # Step 1: Upload image
        print("  1. Uploading image...")
        image_ipfs_url = self.upload_nft_image(image_path, name, description)
        print(f"     Image IPFS URL: {image_ipfs_url}")
        
        # Step 2: Upload metadata
        print("  2. Uploading metadata...")
        metadata_result = self.upload_nft_metadata(
            name=name,
            description=description,
            image_ipfs_url=image_ipfs_url,
            attributes=attributes,
            external_url=external_url
        )
        
        print(f"     Metadata CID: {metadata_result['cid']}")
        print(f"     Metadata URL: {metadata_result['uris']['url']}")
        
        return {
            "image_ipfs": image_ipfs_url,
            "metadata_cid": metadata_result['cid'],
            "metadata_url": metadata_result['uris']['url'],
            "metadata_ipfs": metadata_result['uris']['ipfs']
        }

# Example usage
if __name__ == "__main__":
    uploader = NFTMetadataUploader("bws_1234567890abcdef1234567890abcdef")
    
    try:
        nft_result = uploader.create_complete_nft(
            image_path="./nft_artwork.png",
            name="Cosmic Cat #001",
            description="A rare cosmic cat exploring the digital universe",
            attributes=[
                {"trait_type": "Background", "value": "Space"},
                {"trait_type": "Species", "value": "Cosmic Cat"},
                {"trait_type": "Rarity", "value": "Legendary"},
                {"trait_type": "Power Level", "value": 95}
            ],
            external_url="https://myproject.com/nft/1"
        )
        
        print("\n🎉 NFT created successfully!")
        print(f"Use this metadata URI in your smart contract: {nft_result['metadata_ipfs']}")
        
    except Exception as e:
        print(f"❌ NFT creation failed: {e}")

Error Handling and Best Practices

Implement robust error handling for production applications:

import time
from functools import wraps

def retry_on_failure(max_retries=3, delay=1):
    """Decorator to retry failed uploads"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries - 1:
                        raise e
                    print(f"Attempt {attempt + 1} failed: {e}")
                    print(f"Retrying in {delay} seconds...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry_on_failure(max_retries=3, delay=2)
def robust_upload(file_path, description=None):
    """Upload with automatic retry on failure"""
    return upload_file_to_ipfs(file_path, description)

def validate_api_response(response, operation="operation"):
    """Validate API response and provide detailed error info"""
    if response.status_code == 200:
        return response.json()
    elif response.status_code == 401:
        raise Exception(f"Authentication failed for {operation}. Check your API key.")
    elif response.status_code == 413:
        raise Exception(f"File too large for {operation}. Check your plan limits.")
    elif response.status_code == 429:
        raise Exception(f"Rate limit exceeded for {operation}. Please wait and retry.")
    else:
        raise Exception(f"{operation} failed: {response.status_code} - {response.text}")

Next Steps

You now have a complete toolkit for uploading files to IPFS from Python! This covers:

  • JSON and file uploads with the IPFS.NINJA API
  • Batch processing for multiple files
  • File management and pinning existing content
  • Secure uploads with signed tokens
  • Production-ready error handling

For more advanced features, explore our other guides:

The IPFS.NINJA API provides everything you need for Python-based IPFS applications without the complexity of running your own infrastructure.

Ready to start pinning? Create a free account — 500 files, 1 GB storage, dedicated gateway. No credit card required.

Back to Blog

Related Posts

View All Posts »