Python IPFS Upload: Complete Guide with Code Examples
Upload files, images, and JSON to IPFS from Python using the IPFS.NINJA REST API. No SDK required—just the requests library. Complete code examples included.
Nacho founded IPFS.NINJA to make content-addressed storage feel as simple as an S3 PUT — a single API call, a permanent CID, no wallets or peer discovery to reason about.

- Upload files, images, and JSON to IPFS from Python using nothing but the built-in requests library.
- No dedicated Python SDK needed — the REST API works the same from any HTTP-capable language.
- Generate signed upload tokens from Python so browser or mobile clients can upload without your API key.
- Authenticate every call with a single X-Api-Key header — no OAuth flow or SDK installation required.
To upload files to IPFS from Python in 2026, POST to a pinning service’s REST API using requests.post() with an X-Api-Key header — a permanent CID returns in seconds. The legacy ipfshttpclient package for direct Kubo RPC access is unmaintained since 2022, so the REST-to-pinning-service path is what teams ship. This guide covers JSON, binary files, batch uploads, and signed upload tokens.
Last verified: 2026-08-27.

Set Up Python IPFS Uploads in 5 Minutes#
The fastest way to upload to IPFS from Python is a three-step flow: install requests from PyPI, generate an API key from your pinning service dashboard, then call requests.post() with a JSON body containing your content. Signup to first pinned CID typically takes under two minutes — the requests quickstart docs cover every option you will use below.
- Install — add the
requestslibrary withpip install requests(no dedicated SDK needed). - Authenticate — set an
X-Api-Keyheader with your IPFS.NINJA API key. - Make your first upload call — send a
POSTrequest to/upload/newand get back the CID for your file.
The sections below walk through each step in detail, followed by complete code examples for JSON, file, and batch uploads.
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.
IPFS.NINJA advantages for Python developers#
IPFS.NINJA fits Python’s philosophy of readable, dependency-light code. You call one endpoint (POST /upload/new) with the standard requests library — no wheel builds, no C extensions, no Kubo daemon. The service auto-detects JSON, PNG, JPEG, and PDF from the payload, so a single upload function handles every content type your Python app will send.
- No SDK dependencies: Use Python’s built-in
requestslibrary - 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
Setting Up Your Python Environment#
Any Python 3.11+ interpreter works — the code examples in this guide use only requests, base64, and pathlib from the standard library plus Pillow for image handling later. Install requests from PyPI with pip; a virtualenv is recommended so your IPFS scripts do not collide with system packages. Verify with python --version before continuing.
pip install requestsFor image processing examples later in this guide:
pip install Pillow # For image handlingAuthentication 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"\nSuccessfully 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(from_ms=None, to_ms=None):
"""
List your pinned files
Args:
from_ms (int, optional): Start of the time range, Unix ms (defaults to 30 days ago)
to_ms (int, optional): End of the time range, Unix ms (defaults to now)
Returns:
dict: Response containing files list and pagination
"""
import time
if to_ms is None:
to_ms = int(time.time() * 1000)
if from_ms is None:
from_ms = to_ms - 30 * 24 * 60 * 60 * 1000
params = {
"from": from_ms,
"to": to_ms
}
response = requests.get(
f"{BASE_URL}/upload/list",
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}/file/{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()
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, timezone
expiry_time = datetime.now(timezone.utc) + timedelta(hours=expires_in_hours)
payload = {
"expiry": expiry_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
"maxUses": max_uses
}
response = requests.post(
f"{BASE_URL}/upload/signed-url",
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:
- Complete IPFS Upload API Tutorial for API details
- IPFS.NINJA vs Pinata comparison to understand the advantages
- Best IPFS Pinning Services for service comparisons
- IPFS CIDs Explained for the content addressing fundamentals every Python developer should know
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? Start your 7-day trial — 10 GB / 200 files / 20 GB bandwidth / 1 dedicated gateway (trial file cap — Bodhi lifts to unlimited). No credit card required. Then Bodhi $5/mo, Karma $19/mo, or Nirvana $59/mo.
About this article
This article was AI-assisted, human-reviewed, and product-verified against the live IPFS.NINJA platform before publishing. Learn how we use AI in our content .

About the author
Nacho Coll
Founder & Engineer at IPFS.NINJA
Nacho founded IPFS.NINJA to make content-addressed storage feel as simple as an S3 PUT — a single API call, a permanent CID, no wallets or peer discovery to reason about. Writes about IPFS internals, decentralized storage patterns, and the pinning-service landscape from the operator side of the wire.
