Skip to content

S3 Compatibility

Use the AWS SDK to upload, download, and manage files on IPFS Ninja with the same code you use for Amazon S3.

Endpoint

https://s3.ipfs.ninja

Credentials

The S3 API uses your IPFS Ninja API key for authentication. Your API key serves as both the access key and the secret key.

How to get your credentials

  1. Go to Dashboard > API Keys
  2. Click Create API key and give it a name (e.g. "S3 access")
  3. Copy the full key immediately — it is only shown once and cannot be retrieved later

Your key looks like this:

bws_628bba35e9e0079d9ff9c392b1b55a7b
├──────────┘└──────────────────────────┘
 prefix (12 chars)    rest of key

Mapping to AWS credentials

AWS ParameterValueExample
accessKeyIdFirst 12 characters of your API keybws_628bba35
secretAccessKeyThe full API key (all 36 characters)bws_628bba35e9e0079d9ff9c392b1b55a7b
regionAlways us-east-1us-east-1

WARNING

The full API key is only shown once when you create it. If you lose it, delete the key and create a new one from the API Keys page.

Quick Start

javascript
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";

const s3 = new S3Client({
  endpoint: "https://s3.ipfs.ninja",
  credentials: {
    accessKeyId: "bws_628bba35",
    secretAccessKey: "bws_628bba35e9e0079d9ff9c392b1b55a7b"
  },
  region: "us-east-1",
  forcePathStyle: true
});

// Upload a file
const put = await s3.send(new PutObjectCommand({
  Bucket: "my-project",
  Key: "hello.json",
  Body: JSON.stringify({ hello: "IPFS" }),
  ContentType: "application/json"
}));

console.log("CID:", put.ETag);
// CID: bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi

Buckets = Folders

S3 buckets map to your IPFS Ninja folders. When you upload a file to a bucket, it's stored in the corresponding folder. When you list objects in a bucket, you see the files in that folder.

S3 OperationIPFS Ninja Equivalent
CreateBucketCreate a new folder
ListBucketsList your folders
DeleteBucketDelete a folder and all files in it
PutObject to bucketUpload file into the folder
ListObjectsV2 on bucketList files in the folder
javascript
import { ListBucketsCommand, CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";

// Create a bucket (= create a folder)
await s3.send(new CreateBucketCommand({ Bucket: "nft-metadata" }));

// Upload a file into the folder
await s3.send(new PutObjectCommand({
  Bucket: "nft-metadata",      // ← folder name
  Key: "token-42.json",        // ← filename within the folder
  Body: JSON.stringify({ name: "My NFT #42" })
}));

// List buckets (= list your folders)
const { Buckets } = await s3.send(new ListBucketsCommand({}));
console.log(Buckets);
// [{ Name: "nft-metadata", CreationDate: "2026-04-13T..." }]

TIP

Folders created via the S3 API are the same folders visible in your Dashboard. You can organize files from either the S3 API, the REST API, or the web interface — they all share the same folder system.

INFO

Unlike Amazon S3, IPFS Ninja folders are flat by default. To create nested structures, use the REST API's folder endpoints with parentFolderId. From the S3 API, use key prefixes (e.g. images/photo.png) to organize within a folder.

Bucket names are globally unique

Bucket names live in a global namespace across all customers, matching AWS S3 semantics. That means:

  • The first user to create a bucket with a given name claims that name globally.
  • Later CreateBucket calls with the same name from any account return BucketAlreadyExists (409).
  • If you try to re-create your own bucket, you get BucketAlreadyOwnedByYou (409).
  • Your folder name in the dashboard is per-account and can still be anything — only the S3-visible bucket name goes through the global namespace.

If a name you want is taken, pick a differently-scoped one (myapp-photos-2026, acme-nft-metadata) — same convention you'd use on Amazon S3.

Supported Operations

PutObject

Upload a file to IPFS. The file is pinned, safety-scanned, and the CID is returned in the ETag and x-amz-meta-cid headers.

To import a CAR file instead of a regular file, add the x-amz-meta-import: car metadata header. See CAR Import for details.

To override the auto-detected fileType label, add an x-amz-meta-filetype metadata header set to one of the values listed under Automatic type detection (e.g. x-amz-meta-filetype: image). This only corrects the stored label — content safety scanning always runs against the actual uploaded bytes. Unknown values are rejected with a 400 InvalidArgument. For multipart uploads, set the header on CreateMultipartUpload (S3 object metadata is fixed at initiation, not at CompleteMultipartUpload).

javascript
import { PutObjectCommand } from "@aws-sdk/client-s3";
import fs from "fs";

const result = await s3.send(new PutObjectCommand({
  Bucket: "my-project",
  Key: "photo.png",
  Body: fs.readFileSync("photo.png"),
  ContentType: "image/png"
}));

console.log("CID:", result.ETag);
bash
# curl equivalent
curl -X PUT "https://s3.ipfs.ninja/my-project/photo.png" \
  --data-binary @photo.png \
  -H "Content-Type: image/png" \
  --aws-sigv4 "aws:amz:us-east-1:s3" \
  --user "bws_628bba35:bws_628bba35e9e0079d9ff9c392b1b55a7b"

GetObject

Download a file by its key (filename) or CID.

javascript
import { GetObjectCommand } from "@aws-sdk/client-s3";

const result = await s3.send(new GetObjectCommand({
  Bucket: "my-project",
  Key: "photo.png"
}));

const body = await result.Body.transformToByteArray();
console.log("Size:", body.length);
console.log("CID:", result.Metadata?.cid);

HeadObject

Get file metadata without downloading the content.

javascript
import { HeadObjectCommand } from "@aws-sdk/client-s3";

const head = await s3.send(new HeadObjectCommand({
  Bucket: "my-project",
  Key: "photo.png"
}));

console.log("Size:", head.ContentLength);
console.log("Type:", head.ContentType);
console.log("CID:", head.Metadata?.cid);

DeleteObject

Unpin a file from IPFS and delete it from your account.

javascript
import { DeleteObjectCommand } from "@aws-sdk/client-s3";

await s3.send(new DeleteObjectCommand({
  Bucket: "my-project",
  Key: "photo.png"
}));

ListObjectsV2

List files in a bucket with optional prefix filtering and pagination.

javascript
import { ListObjectsV2Command } from "@aws-sdk/client-s3";

const list = await s3.send(new ListObjectsV2Command({
  Bucket: "my-project",
  Prefix: "images/",
  MaxKeys: 100
}));

for (const obj of list.Contents ?? []) {
  console.log(obj.Key, obj.Size, obj.ETag); // ETag = CID
}

Multipart Upload

Upload large files (up to 5 GB) using multipart upload. The AWS SDK handles this automatically:

javascript
import { Upload } from "@aws-sdk/lib-storage";
import fs from "fs";

const upload = new Upload({
  client: s3,
  params: {
    Bucket: "my-project",
    Key: "large-dataset.tar.gz",
    Body: fs.createReadStream("large-dataset.tar.gz"),
    ContentType: "application/gzip"
  },
  partSize: 10 * 1024 * 1024, // 10 MB per part
});

upload.on("httpUploadProgress", (progress) => {
  console.log(`Uploaded ${progress.loaded} of ${progress.total} bytes`);
});

const result = await upload.done();
console.log("CID:", result.ETag);

Or manually control parts:

javascript
import {
  CreateMultipartUploadCommand,
  UploadPartCommand,
  CompleteMultipartUploadCommand
} from "@aws-sdk/client-s3";

// 1. Start
const { UploadId } = await s3.send(new CreateMultipartUploadCommand({
  Bucket: "my-project",
  Key: "big-file.bin"
}));

// 2. Upload parts
const part1 = await s3.send(new UploadPartCommand({
  Bucket: "my-project",
  Key: "big-file.bin",
  UploadId,
  PartNumber: 1,
  Body: chunk1
}));

// 3. Complete
const result = await s3.send(new CompleteMultipartUploadCommand({
  Bucket: "my-project",
  Key: "big-file.bin",
  UploadId,
  MultipartUpload: {
    Parts: [{ PartNumber: 1, ETag: part1.ETag }]
  }
}));

Python Example

python
import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.ipfs.ninja",
    aws_access_key_id="bws_628bba35",
    aws_secret_access_key="bws_628bba35e9e0079d9ff9c392b1b55a7b",
    region_name="us-east-1",
    config=Config(s3={"addressing_style": "path"})
)

# Upload
s3.put_object(
    Bucket="my-project",
    Key="data.json",
    Body=b'{"hello": "IPFS"}',
    ContentType="application/json"
)

# List files
response = s3.list_objects_v2(Bucket="my-project")
for obj in response.get("Contents", []):
    print(obj["Key"], obj["Size"])

# Download
result = s3.get_object(Bucket="my-project", Key="data.json")
print(result["Body"].read())

Go Example

go
package main

import (
    "context"
    "fmt"
    "strings"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
    client := s3.New(s3.Options{
        BaseEndpoint: aws.String("https://s3.ipfs.ninja"),
        Region:       "us-east-1",
        Credentials:  credentials.NewStaticCredentialsProvider("bws_628bba35", "bws_628bba35e9e0079d9ff9c392b1b55a7b", ""),
        UsePathStyle: true,
    })

    _, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
        Bucket:      aws.String("my-project"),
        Key:         aws.String("hello.txt"),
        Body:        strings.NewReader("Hello, IPFS!"),
        ContentType: aws.String("text/plain"),
    })
    if err != nil {
        panic(err)
    }
    fmt.Println("Uploaded!")
}

Configure CORS (browser SDK access)

If you're calling the S3 API directly from browser JavaScript (SPA, wallet app, dashboard tool), you need to configure CORS on the bucket first. Otherwise browsers block the preflight and your uploads fail with No 'Access-Control-Allow-Origin' header is present.

Same shape as AWS S3 — PutBucketCors / GetBucketCors / DeleteBucketCors subresources:

PutBucketCors

javascript
import { S3Client, PutBucketCorsCommand } from "@aws-sdk/client-s3";

await s3.send(new PutBucketCorsCommand({
  Bucket: "my-bucket",
  CORSConfiguration: {
    CORSRules: [{
      AllowedOrigins: ["http://localhost:3000", "https://myapp.com"],
      AllowedMethods: ["GET", "HEAD", "PUT", "POST", "DELETE"],
      AllowedHeaders: ["*"],
      ExposeHeaders: ["ETag", "x-amz-meta-cid", "x-amz-request-id"],
      MaxAgeSeconds: 3600,
    }],
  },
}));

GetBucketCors

javascript
import { GetBucketCorsCommand } from "@aws-sdk/client-s3";

const { CORSRules } = await s3.send(new GetBucketCorsCommand({ Bucket: "my-bucket" }));
console.log(CORSRules);

DeleteBucketCors

javascript
import { DeleteBucketCorsCommand } from "@aws-sdk/client-s3";

await s3.send(new DeleteBucketCorsCommand({ Bucket: "my-bucket" }));

Cap and defaults

  • Up to 100 rules per bucket (matches the AWS S3 CORS spec limit).
  • Total config capped at 64 KB serialised.
  • If no CORS config is set, browser preflights are rejected — matching AWS S3's default posture. Configure explicit rules for the origins you actually serve from.

Real-world security note

CORS is a browser-side convenience layer, not a security boundary. Every S3 API call still requires a valid SigV4 signature computed from your API key — a permissive CORS config doesn't let anyone use your bucket without that credential. What CORS does prevent: an unintended origin (e.g. a stale copy of your app on dev.myapp.com) sending signed requests from a browser context.

Dashboard alternative

The Files page has an S3 CORS entry in each folder's action menu. Same underlying storage; if you don't want to write PutBucketCors code, configure it there.

Differences from Amazon S3

FeatureAmazon S3IPFS Ninja S3
Storage modelMutable objectsContent-addressed (immutable CIDs)
Overwrite behaviorReplaces object in-placeCreates new CID, old CID still accessible
VersioningSupportedNot supported (use CIDs for versioning)
Server-side encryptionSupportedNot supported (content is on IPFS)
Lifecycle policiesSupportedNot supported
Bucket policies / ACLsSupportedUse gateway access modes
Presigned URLsSupportedUse signed upload tokens
Max object size5 TB5 GB (multipart), 100 MB (single PUT)
RegionsMulti-regionus-east-1 only
ETag valueMD5 hashIPFS CID
Extra headersStandard S3x-amz-meta-cid (IPFS CID)
CID formatN/AModern CIDv1 (bafy…) for new uploads; legacy Qm… remains valid for input
Bucket namespaceGlobal (AWS-wide)Global (across all IPFS Ninja accounts) — same semantics
CORSPutBucketCors supportedPutBucketCors supported (100-rule cap)

Migrating from Amazon S3

Replace your S3 client configuration:

diff
 const s3 = new S3Client({
+  endpoint: "https://s3.ipfs.ninja",
   credentials: {
-    accessKeyId: "AKIA...",
-    secretAccessKey: "wJalrX..."
+    accessKeyId: "bws_628bba35",
+    secretAccessKey: "bws_628bba35e9e0..."
   },
   region: "us-east-1",
+  forcePathStyle: true
 });

Your existing PutObject, GetObject, ListObjectsV2, and DeleteObject calls work unchanged.

Migrating from Filebase

Replace the endpoint URL:

diff
 const s3 = new S3Client({
-  endpoint: "https://s3.filebase.com",
+  endpoint: "https://s3.ipfs.ninja",
   credentials: {
-    accessKeyId: "FILEBASE_KEY",
-    secretAccessKey: "FILEBASE_SECRET"
+    accessKeyId: "bws_628bba35",
+    secretAccessKey: "bws_628bba35e9e0..."
   },
   region: "us-east-1",
   forcePathStyle: true
 });