Shelpin API Docs
Pin files to the Shelby network with cryptographic proof. One API call, verifiable on-chain.
What is Shelby?
Shelby is a decentralised hot storage protocol built on Aptos. Instead of storing files on one company's servers (like AWS S3), Shelby spreads your data across a network of independent Storage Provider (SP) nodes around the world.
Key properties:
- Decentralised — no single point of failure. Files are erasure-coded across multiple SP nodes.
- On-chain commitments — every file upload is registered on the Aptos blockchain with cryptographic proofs. You can verify your data exists without trusting anyone.
- Hot storage — unlike cold archival systems, files on Shelby are instantly retrievable. Think S3, not Glacier.
- Expiring blobs — storage has a time-to-live. You pay for what you use, not forever.
What is Shelpin?
Shelpin is a managed API layer on top of Shelby. It handles the complexity so you don't have to:
- No wallet needed — Shelpin manages the Aptos account, gas fees, and ShelbyUSD tokens. You just use an API key.
- One API call — upload a file with
POST /filesinstead of dealing with the Shelby SDK, commitment generation, and transaction signing. - User management — accounts, API keys, rate limits, and usage tracking built in.
- Public gateway — serve your files via URL (
/g/pinId/file.jpg) with no auth required. Embed in<img>tags, share links, use as a CDN origin. - Presigned uploads — let frontend users upload directly without exposing your API key.
- Analytics — track downloads, bandwidth, and visitors per file.
Think of it like Pinata for IPFS, but for Shelby — with on-chain proofs built in, not bolted on.
shelby:// Addresses
When you upload a file through Shelpin, you get back a shelby:// address like:
shelby://0x48afe95c5c16...7c81/photo.jpg
This address has two parts:
- Account address (
0x48af...7c81) — the Aptos account that owns the file on-chain. Shelpin manages this for you. - Blob name (
photo.jpg) — the path/name of the file within that account's namespace.
This address is permanent and verifiable. Anyone can check the Aptos blockchain to confirm the file was uploaded and committed. You can view it on the Shelby Explorer.
Quickstart
Get from zero to a pinned file in under a minute.
1. Create an account
# Register and get your API key curl -X POST https://shelpin.forestinfra.com/auth/register \ -H "Content-Type: application/json" \ -d '{"email": "you@example.com"}'
{
"userId": "abc-123",
"email": "you@example.com",
"tier": "free",
"apiKey": "shelpin_a1b2c3d4...",
"message": "Save your API key — it will not be shown again."
}
2. Upload a file
# Pin a file to Shelby curl -X POST https://shelpin.forestinfra.com/files \ -H "Authorization: Bearer shelpin_a1b2c3d4..." \ -F "file=@photo.jpg" \ -F "name=photo.jpg"
{
"id": "pin_f8e7d6c5b4a3",
"shelbyAddress": "shelby://0x48af...7c81/photo.jpg",
"shelbyProof": "shelby://0x48af...7c81/photo.jpg",
"size": 284729,
"status": "pinned",
"name": "photo.jpg",
"created": "2026-03-24 15:28:13"
}
3. Download it back
curl https://shelpin.forestinfra.com/pins/pin_f8e7d6c5b4a3/download \
-H "Authorization: Bearer shelpin_a1b2c3d4..." \
-o photo.jpg
That's it. Your file is stored on the Shelby network with a verifiable shelby:// address.
Authentication
All API requests (except /health and /auth/register) require a Bearer token.
Authorization: Bearer shelpin_your_api_key_here
API keys are generated when you register or via the API Keys endpoint. Keys have read and/or write permissions.
SDK
The Shelpin SDK is a lightweight TypeScript/JavaScript client. Works in Node.js and the browser.
npm install shelpin
Usage
import Shelpin from 'shelpin'; const shelpin = new Shelpin({ apiKey: 'shelpin_your_key' }); // Upload a file const pin = await shelpin.upload( Buffer.from('Hello Shelby!'), 'hello.txt' ); console.log(pin.shelbyAddress); // Upload from URL const pin2 = await shelpin.uploadFromUrl( 'https://example.com/data.json' ); // Upload JSON directly const pin3 = await shelpin.uploadJson( { users: ['alice', 'bob'] }, 'users.json' );
// List your pins const { pins, total } = await shelpin.list({ limit: 10 }); // Download a file const data = await shelpin.download(pins[0].id); // Get public gateway URL (no auth needed) const url = shelpin.gatewayUrl(pins[0].id, 'photo.jpg'); // → https://shelpin.forestinfra.com/g/pin_abc/photo.jpg // Delete a pin await shelpin.delete(pins[0].id);
// Server: create a time-limited upload token const token = await shelpin.createUploadToken({ expiresIn: 300, // 5 minutes maxUses: 1, }); // Client: upload with just the token const form = new FormData(); form.append('file', fileInput.files[0]); await fetch('https://shelpin.forestinfra.com/files/presigned', { method: 'POST', headers: { 'X-Upload-Token': token.uploadToken }, body: form, });
CLI
The Shelpin CLI lets you pin files from your terminal. Install globally or use with npx.
Install
npm install -g shelpin-cli
# or use without installing:
npx shelpin-cli upload ./my-file.jpg
Authenticate
shelpin login shelpin_YOUR_API_KEY
Your key is saved to ~/.shelpin.json. Alternatively, set SHELPIN_API_KEY as an environment variable.
Upload files
# Single file shelpin upload photo.jpg # Multiple files shelpin upload file1.txt file2.png # Entire directory (recursive) shelpin upload ./my-site/
List pins
shelpin list shelpin list --limit 5 --status pinned
Download
shelpin download pin_a1b2c3d4 output.jpg
Get gateway URL
shelpin gateway pin_a1b2c3d4
# → https://shelpin.forestinfra.com/g/pin_a1b2c3d4/photo.jpg
Check health
shelpin health
All commands
| Command | Alias | Description |
|---|---|---|
login <key> | Save API key | |
whoami | Show account info | |
upload <path> | pin | Upload files or folders |
list | ls | List pins |
download <id> | dl | Download a file |
delete <id> | rm | Delete a pin |
gateway <id> | gw | Get public gateway URL |
health | Check service status |
Upload File
POST /files
Upload a file using multipart form data. Max file size: 100MB.
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | The file to upload |
name | string | No | Display name (defaults to original filename) |
metadata | JSON string | No | Arbitrary key-value metadata |
curl -X POST https://shelpin.forestinfra.com/files \ -H "Authorization: Bearer $API_KEY" \ -F "file=@document.pdf" \ -F "name=document.pdf"
const form = new FormData(); form.append("file", fileBlob, "document.pdf"); const res = await fetch("https://shelpin.forestinfra.com/files", { method: "POST", headers: { Authorization: `Bearer ${API_KEY}` }, body: form, }); const pin = await res.json();
Upload Base64 / URL
POST /pins
Pin content from a base64 string or fetch from a URL.
| Field | Type | Required | Description |
|---|---|---|---|
content | string | * | Base64-encoded file content |
url | string | * | URL to fetch and pin |
name | string | No | Display name |
metadata | object | No | Arbitrary key-value metadata |
* Provide either content or url, not both.
curl -X POST https://shelpin.forestinfra.com/pins \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"content": "SGVsbG8gU2hlbGJ5IQ==", "name": "hello.txt"}'
curl -X POST https://shelpin.forestinfra.com/pins \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/data.json", "name": "data.json"}'
const res = await fetch("https://shelpin.forestinfra.com/pins", { method: "POST", headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ content: btoa("Hello Shelby!"), name: "hello.txt", }), }); const pin = await res.json();
List & Query Pins
GET /pins
List all your pins with optional filters.
| Param | Type | Default | Description |
|---|---|---|---|
limit | number | 20 | Results per page (max 100) |
offset | number | 0 | Pagination offset |
status | string | — | Filter: pinned, pinning, failed, unpinned |
name | string | — | Search by name (partial match) |
curl https://shelpin.forestinfra.com/pins?limit=10&status=pinned \
-H "Authorization: Bearer $API_KEY"
{
"pins": [
{
"id": "pin_f8e7d6c5b4a3",
"shelbyAddress": "shelby://0x48af...7c81/photo.jpg",
"shelbyProof": "shelby://0x48af...7c81/photo.jpg",
"size": 284729,
"status": "pinned",
"name": "photo.jpg",
"metadata": null,
"created": "2026-03-24 15:28:13"
}
],
"total": 1,
"limit": 10,
"offset": 0
}
Get Pin
GET /pins/:id
Retrieve metadata for a single pin by its ID.
curl https://shelpin.forestinfra.com/pins/pin_f8e7d6c5b4a3 \
-H "Authorization: Bearer $API_KEY"
{
"id": "pin_f8e7d6c5b4a3",
"shelbyAddress": "shelby://0x48af...7c81/photo.jpg",
"shelbyProof": "shelby://0x48af...7c81/photo.jpg",
"size": 284729,
"status": "pinned",
"name": "photo.jpg",
"metadata": null,
"created": "2026-03-24 15:28:13"
}
Download File
GET /pins/:id/download
Download the actual file content from the Shelby network. Returns the raw file with appropriate headers.
curl https://shelpin.forestinfra.com/pins/pin_f8e7d6c5b4a3/download \
-H "Authorization: Bearer $API_KEY" \
-o photo.jpg
const res = await fetch( "https://shelpin.forestinfra.com/pins/pin_f8e7d6c5b4a3/download", { headers: { Authorization: `Bearer ${API_KEY}` } } ); const blob = await res.blob(); // Save or use the blob
Content-Type: application/octet-stream Content-Disposition: attachment; filename="photo.jpg" Content-Length: 284729
Delete Pin
DELETE /pins/:id
Unpin a file. The file is marked as unpinned and will no longer appear in default listings.
curl -X DELETE https://shelpin.forestinfra.com/pins/pin_f8e7d6c5b4a3 \
-H "Authorization: Bearer $API_KEY"
{
"message": "Pin removed",
"id": "pin_f8e7d6c5b4a3"
}
Presigned Uploads
Let your frontend users upload files directly without exposing your API key. Generate a time-limited upload token, pass it to the client, and they upload straight to Shelpin.
1. Create an upload token (server-side)
POST /auth/upload-token
| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Default filename for uploads |
expiresIn | number | 3600 | Token lifetime in seconds |
maxUses | number | 1 | Max number of uploads allowed |
maxSizeBytes | number | 104857600 | Max file size per upload (100MB) |
curl -X POST https://shelpin.forestinfra.com/auth/upload-token \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"expiresIn": 300, "maxUses": 1}'
{
"uploadToken": "shelpin_upload_a1b2c3...",
"uploadUrl": "/files/presigned",
"expiresAt": "2026-03-24T16:30:00.000Z",
"maxUses": 1,
"maxSizeBytes": 104857600
}
2. Upload from the client (no API key needed)
POST /files/presigned
Pass the token in the X-Upload-Token header. No Authorization header required.
curl -X POST https://shelpin.forestinfra.com/files/presigned \ -H "X-Upload-Token: shelpin_upload_a1b2c3..." \ -F "file=@photo.jpg"
// In the browser — no API key exposed const form = new FormData(); form.append("file", fileInput.files[0]); const res = await fetch("https://shelpin.forestinfra.com/files/presigned", { method: "POST", headers: { "X-Upload-Token": uploadToken }, body: form, }); const pin = await res.json(); console.log(pin.shelbyAddress); // shelby://0x48af.../photo.jpg
Public Gateway
Serve pinned files publicly — no authentication required. Perfect for images, assets, and any content you want shareable via URL.
GET /g/:pinId
GET /g/:pinId/:filename
The second form includes the filename in the URL for friendly links and correct MIME types.
# Direct link — anyone can access, no auth
https://shelpin.forestinfra.com/g/pin_f8e7d6c5b4a3/photo.jpg
Use it in HTML:
<img src="https://shelpin.forestinfra.com/g/pin_f8e7d6c5b4a3/photo.jpg" />
Response headers include:
Cache-Control: public, max-age=3600, immutable— files are cacheableContent-Type— auto-detected from filename extensionX-Shelby-Address— the on-chain shelby:// address
Webhooks
Get notified when pins complete, fail, or are deleted. Register a URL and Shelpin will POST to it with event details.
Events
| Event | Fired when |
|---|---|
pin.completed | A file is successfully pinned to Shelby |
pin.failed | A pin attempt fails |
pin.deleted | A pin is deleted/unpinned |
* | All events (default) |
Create a webhook
curl -X POST https://shelpin.forestinfra.com/webhooks \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.com/hooks/shelpin", "events": ["pin.completed"]}'
Response includes a secret — save it to verify signatures.
Verifying signatures
Every webhook includes an X-Shelpin-Signature header — an HMAC-SHA256 of the request body using your secret:
const crypto = require('crypto'); function verifyWebhook(body, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return signature === expected; }
Payload format
{
"event": "pin.completed",
"timestamp": "2026-03-24T14:30:00.000Z",
"data": {
"pin": {
"id": "pin_a1b2c3d4e5f6",
"shelbyAddress": "shelby://0x...",
"size": 48291,
"status": "pinned",
"name": "photo.jpg"
}
}
}
Other webhook endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /webhooks | List your webhooks |
PATCH | /webhooks/:id | Update URL, events, or active status |
DELETE | /webhooks/:id | Delete a webhook |
POST | /webhooks/:id/test | Send a test event |
Webhooks auto-disable after 10 consecutive delivery failures. Re-enable via PATCH with {"active": true}.
S3-Compatible API
Drop-in replacement for AWS S3 — swap your endpoint URL and your app stores files on Shelby instead of Amazon. Uses the same Shelpin Bearer auth (not AWS SigV4).
Upload an object
curl -X PUT https://shelpin.forestinfra.com/s3/my-bucket/photos/cat.jpg \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: image/jpeg" \ --data-binary @cat.jpg
Download an object
curl https://shelpin.forestinfra.com/s3/my-bucket/photos/cat.jpg \ -H "Authorization: Bearer YOUR_KEY" \ -o cat.jpg
Check if an object exists
curl -I https://shelpin.forestinfra.com/s3/my-bucket/photos/cat.jpg \ -H "Authorization: Bearer YOUR_KEY"
Delete an object
curl -X DELETE https://shelpin.forestinfra.com/s3/my-bucket/photos/cat.jpg \ -H "Authorization: Bearer YOUR_KEY"
List objects in a bucket
curl https://shelpin.forestinfra.com/s3/my-bucket?prefix=photos/ \ -H "Authorization: Bearer YOUR_KEY"
Returns S3-compatible XML with <ListBucketResult>.
Using with the AWS SDK
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3'); const s3 = new S3Client({ endpoint: 'https://shelpin.forestinfra.com/s3', region: 'auto', credentials: { accessKeyId: 'shelpin', // any value secretAccessKey: 'YOUR_KEY', // your Shelpin API key }, }); await s3.send(new PutObjectCommand({ Bucket: 'my-bucket', Key: 'hello.txt', Body: 'Hello Shelby!', }));
Supported operations
| S3 Operation | HTTP | Shelpin Endpoint |
|---|---|---|
| PutObject | PUT | /s3/:bucket/:key |
| GetObject | GET | /s3/:bucket/:key |
| HeadObject | HEAD | /s3/:bucket/:key |
| DeleteObject | DELETE | /s3/:bucket/:key |
| ListObjectsV2 | GET | /s3/:bucket?prefix= |
Note: Shelpin uses Bearer auth, not AWS SigV4. When using the AWS SDK, pass your Shelpin API key as secretAccessKey. The accessKeyId can be any value.
Analytics
GET /analytics
Track downloads, bandwidth, and visitors across your files. All gateway and API downloads are logged.
| Param | Type | Default | Description |
|---|---|---|---|
days | number | 30 | Lookback period (max 90) |
pinId | string | — | Filter to a single file |
curl https://shelpin.forestinfra.com/analytics?days=7 \
-H "Authorization: Bearer $API_KEY"
{
"period": { "days": 7, "since": "2026-03-17T..." },
"summary": {
"totalDownloads": 142,
"totalBandwidth": 48293847,
"uniqueFiles": 8,
"uniqueVisitors": 34
},
"daily": [
{ "date": "2026-03-24", "downloads": 23, "bandwidth": 8294817 }
],
"topFiles": [
{ "pin_id": "pin_f8e7...", "name": "photo.jpg", "downloads": 89, "bandwidth": 25384729 }
]
}
Folders
Organise your pins into folders. A pin can belong to multiple folders.
Create a folder
POST /folders
curl -X POST https://shelpin.forestinfra.com/folders \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "photos"}'
Response:
{
"id": "fld_a1b2c3d4e5f6",
"name": "photos",
"parentId": null
}
List folders
GET /folders
curl https://shelpin.forestinfra.com/folders \ -H "Authorization: Bearer YOUR_KEY"
Returns all folders with pin counts:
{
"folders": [
{ "id": "fld_a1b2c3d4e5f6", "name": "photos", "pin_count": 12 }
]
}
Add pins to a folder
POST /folders/:id/pins
curl -X POST https://shelpin.forestinfra.com/folders/fld_a1b2c3d4e5f6/pins \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"pinIds": ["pin_abc123", "pin_def456"]}'
Remove pins from a folder
DELETE /folders/:id/pins
curl -X DELETE https://shelpin.forestinfra.com/folders/fld_a1b2c3d4e5f6/pins \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"pinIds": ["pin_abc123"]}'
Delete a folder
DELETE /folders/:id
Deleting a folder removes the association but does not delete the pins inside it.
curl -X DELETE https://shelpin.forestinfra.com/folders/fld_a1b2c3d4e5f6 \ -H "Authorization: Bearer YOUR_KEY"
Filter pins by folder
Pass folderId as a query parameter when listing pins:
curl https://shelpin.forestinfra.com/pins?folderId=fld_a1b2c3d4e5f6 \ -H "Authorization: Bearer YOUR_KEY"
Tags
Label your pins with coloured tags for quick filtering. A pin can have multiple tags.
Create a tag
POST /tags
curl -X POST https://shelpin.forestinfra.com/tags \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "production"}'
Response:
{
"id": "tag_x1y2z3",
"name": "production",
"color": "#8BC53F"
}
List tags
GET /tags
curl https://shelpin.forestinfra.com/tags \ -H "Authorization: Bearer YOUR_KEY"
Tag a pin
POST /pins/:pinId/tags
curl -X POST https://shelpin.forestinfra.com/pins/pin_abc123/tags \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"tagIds": ["tag_x1y2z3"]}'
Remove tags from a pin
DELETE /pins/:pinId/tags
curl -X DELETE https://shelpin.forestinfra.com/pins/pin_abc123/tags \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"tagIds": ["tag_x1y2z3"]}'
Filter pins by tag
Pass tagId as a query parameter when listing pins:
curl https://shelpin.forestinfra.com/pins?tagId=tag_x1y2z3 \ -H "Authorization: Bearer YOUR_KEY"
Delete a tag
DELETE /tags/:id
curl -X DELETE https://shelpin.forestinfra.com/tags/tag_x1y2z3 \ -H "Authorization: Bearer YOUR_KEY"
Activity Log
GET /activity
Returns a timeline of all events for your account — downloads, uploads, views. Useful for debugging and auditing.
Query parameters
| Parameter | Default | Description |
|---|---|---|
limit | 50 | Max events to return (max 200) |
offset | 0 | Pagination offset |
Example
curl https://shelpin.forestinfra.com/activity?limit=20 \ -H "Authorization: Bearer YOUR_KEY"
Response
{
"events": [
{
"id": "evt_abc123",
"type": "download",
"pinId": "pin_f8e7d6c5b4a3",
"pinName": "photo.jpg",
"bytes": 48291,
"ip": "192.168.1.1",
"timestamp": "2026-03-25T14:30:00.000Z"
}
],
"total": 156,
"limit": 20,
"offset": 0
}
Event types
| Type | Description |
|---|---|
download | File downloaded via API or gateway |
upload | File uploaded |
view | File viewed via gateway |
Register
POST /auth/register
Create a new account. No authentication required. Returns an API key — save it, it won't be shown again.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Your email address |
curl -X POST https://shelpin.forestinfra.com/auth/register \ -H "Content-Type: application/json" \ -d '{"email": "dev@example.com"}'
API Keys
Create a key
POST /auth/keys
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Label for the key (e.g. "ci-pipeline") |
curl -X POST https://shelpin.forestinfra.com/auth/keys \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "ci-pipeline"}'
List keys
GET /auth/keys
curl https://shelpin.forestinfra.com/auth/keys \
-H "Authorization: Bearer $API_KEY"
Health
GET /health
No authentication required. Check service status and Shelby network connectivity.
curl https://shelpin.forestinfra.com/health
{
"service": "shelpin",
"status": "ok",
"version": "0.1.0",
"shelby": {
"healthy": true,
"networkVersion": "shelbynet",
"mock": false
}
}
Tiers & Limits
Free
Pro
Enterprise
Upload limit per file: 100 MB. Need more? Get in touch.