Skip to content
PutPut is shutting down on October 21, 2026. New signups, uploads, and paid plans are closed. Please download your files before then.

Code Examples

Complete upload flows in curl, JavaScript/TypeScript, Python, Go, Ruby, and PHP. Each example covers the full presigned upload flow.

curl

Complete shell script — get a token, upload a file, confirm, list, and delete.

bash
#!/bin/bash
# Complete PutPut upload flow — curl
BASE="https://putput-ala.pages.dev"

# Step 1: Get a guest token (reuse across sessions)
GUEST=$(curl -s -X POST $BASE/api/v1/auth/guest)
TOKEN=$(echo $GUEST | jq -r '.token')
CLAIM_URL=$(echo $GUEST | jq -r '.claim_url')

echo "Token: $TOKEN"
echo "Claim URL: $CLAIM_URL"  # Save this — user needs it to upgrade

# Step 2: Presign an upload
FILE="hello.txt"
CONTENT_TYPE="text/plain"
SIZE_BYTES=12

PRESIGN=$(curl -s -X POST $BASE/api/v1/upload/presign \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"filename\": \"$FILE\", \"content_type\": \"$CONTENT_TYPE\", \"size_bytes\": $SIZE_BYTES}")

UPLOAD_URL=$(echo $PRESIGN | jq -r '.presigned_url')
UPLOAD_ID=$(echo $PRESIGN | jq -r '.upload_id')

# Step 3: Upload directly to storage (no auth header!)
echo -n "Hello world!" | curl -s -X PUT "$UPLOAD_URL" \
  -H "Content-Type: $CONTENT_TYPE" \
  --data-binary @-

# Step 4: Confirm the upload
RESULT=$(curl -s -X POST $BASE/api/v1/upload/confirm \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"upload_id\": \"$UPLOAD_ID\"}")

PUBLIC_URL=$(echo $RESULT | jq -r '.file.public_url')
echo "Uploaded: $PUBLIC_URL"

# List files
curl -s $BASE/api/v1/files \
  -H "Authorization: Bearer $TOKEN" | jq .

# Delete a file
FILE_ID=$(echo $RESULT | jq -r '.file.id')
curl -s -X DELETE $BASE/api/v1/files/$FILE_ID \
  -H "Authorization: Bearer $TOKEN" | jq .

JavaScript / TypeScript

Browser or Node.js — reusable upload function with error handling.

typescript
// Complete PutPut upload flow — JavaScript/TypeScript
const BASE = "https://putput-ala.pages.dev";

// Step 1: Get a token (reuse if already stored)
// Namespace the key to avoid collisions on localhost
let token = localStorage.getItem("myapp:putput_token");
if (!token) {
  const res = await fetch(`${BASE}/api/v1/auth/guest`, { method: "POST" });
  if (!res.ok) throw new Error("Failed to create guest token");
  const data = await res.json();
  token = data.token;
  localStorage.setItem("myapp:putput_token", token);
  localStorage.setItem("myapp:putput_claim_url", data.claim_url);
}

// Step 2: Presign — fileBlob is a File or Blob
async function upload(fileBlob: File) {
  const presignRes = await fetch(`${BASE}/api/v1/upload/presign`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      filename: fileBlob.name,
      content_type: fileBlob.type,
      size_bytes: fileBlob.size,
    }),
  });

  if (!presignRes.ok) {
    const { error } = await presignRes.json();
    throw new Error(error.message);
  }

  const { upload_id, presigned_url } = await presignRes.json();

  // Step 3: Upload directly to storage (no auth header)
  const putRes = await fetch(presigned_url, {
    method: "PUT",
    headers: { "Content-Type": fileBlob.type },
    body: fileBlob,
  });

  if (!putRes.ok) throw new Error("Upload to storage failed");

  // Step 4: Confirm — unwrap the { file } envelope
  const confirmRes = await fetch(`${BASE}/api/v1/upload/confirm`, {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ upload_id }),
  });

  if (!confirmRes.ok) {
    const { error } = await confirmRes.json();
    throw new Error(error.message);
  }

  const { file } = await confirmRes.json();
  return file.public_url;
}

// Usage
const url = await upload(myFileInput.files[0]);
console.log("Uploaded:", url);

// Error handling
try {
  await upload(file);
} catch (err) {
  // Check error.code for programmatic handling:
  // UNAUTHORIZED — token invalid, get a new one
  // FILE_TOO_LARGE — exceeds plan limit
  // STORAGE_LIMIT_EXCEEDED — quota full
  // CONTENT_TYPE_BLOCKED — guest token, upgrade needed
  // RATE_LIMITED — back off and retry
}
Tip: Always namespace your localStorage key (e.g. "myapp:putput_token") to avoid collisions when multiple apps run on localhost.

Python

Server-side upload with requests library — includes pagination and deletion.

python
# Complete PutPut upload flow — Python
import requests
import os

BASE = "https://putput-ala.pages.dev"

# Step 1: Get a guest token (reuse across sessions)
# Store in env var for server-side usage
token = os.environ.get("PUTPUT_TOKEN")
if not token:
    guest = requests.post(f"{BASE}/api/v1/auth/guest").json()
    token = guest["token"]
    claim_url = guest["claim_url"]  # Save — user needs it to upgrade
    print(f"Token: {token}")
    print(f"Claim URL: {claim_url}")

headers = {"Authorization": f"Bearer {token}"}


def upload(filepath: str, content_type: str) -> str:
    """Upload a file and return the public URL."""
    file_size = os.path.getsize(filepath)
    filename = os.path.basename(filepath)

    # Step 2: Presign
    presign = requests.post(
        f"{BASE}/api/v1/upload/presign",
        headers=headers,
        json={
            "filename": filename,
            "content_type": content_type,
            "size_bytes": file_size,
        },
    )
    presign.raise_for_status()
    data = presign.json()
    upload_id = data["upload_id"]
    presigned_url = data["presigned_url"]

    # Step 3: Upload directly to storage (no auth header)
    with open(filepath, "rb") as f:
        resp = requests.put(
            presigned_url,
            headers={"Content-Type": content_type},
            data=f,
        )
        resp.raise_for_status()

    # Step 4: Confirm
    result = requests.post(
        f"{BASE}/api/v1/upload/confirm",
        headers=headers,
        json={"upload_id": upload_id},
    )
    result.raise_for_status()
    return result.json()["file"]["public_url"]


# Usage
url = upload("photo.jpg", "image/jpeg")
print(f"Uploaded: {url}")

# List files
files = requests.get(f"{BASE}/api/v1/files", headers=headers).json()
for f in files["files"]:
    print(f"{f['original_name']}  {f['public_url']}")

# Paginate
cursor = files.get("cursor")
while files.get("has_more"):
    files = requests.get(
        f"{BASE}/api/v1/files",
        headers=headers,
        params={"cursor": cursor},
    ).json()
    for f in files["files"]:
        print(f"{f['original_name']}  {f['public_url']}")
    cursor = files.get("cursor")

# Delete a file
file_id = files["files"][0]["id"]
requests.delete(f"{BASE}/api/v1/files/{file_id}", headers=headers)
Tip: For server-side usage, store the token in an environment variable: export PUTPUT_TOKEN="pp_..."

Go

Complete example using net/http — get a token, upload, list, and delete.

go
// Complete PutPut upload flow — Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
)

const base = "https://putput-ala.pages.dev"

func main() {
	token := os.Getenv("PUTPUT_TOKEN")
	if token == "" {
		// Step 1: Get a guest token
		t, err := createGuestToken()
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error creating guest token: %v\n", err)
			os.Exit(1)
		}
		token = t
	}

	// Upload a file
	publicURL, fileID, err := upload(token, "hello.txt", "text/plain", []byte("Hello world!"))
	if err != nil {
		fmt.Fprintf(os.Stderr, "Upload error: %v\n", err)
		os.Exit(1)
	}
	fmt.Println("Uploaded:", publicURL)

	// List files
	if err := listFiles(token); err != nil {
		fmt.Fprintf(os.Stderr, "List error: %v\n", err)
		os.Exit(1)
	}

	// Delete the file
	if err := deleteFile(token, fileID); err != nil {
		fmt.Fprintf(os.Stderr, "Delete error: %v\n", err)
		os.Exit(1)
	}
	fmt.Println("Deleted:", fileID)
}

func createGuestToken() (string, error) {
	resp, err := http.Post(base+"/api/v1/auth/guest", "", nil)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	var result struct {
		Token    string `json:"token"`
		ClaimURL string `json:"claim_url"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "", err
	}
	fmt.Println("Claim URL:", result.ClaimURL)
	return result.Token, nil
}

func upload(token, filename, contentType string, data []byte) (string, string, error) {
	// Step 2: Presign
	presignBody, _ := json.Marshal(map[string]interface{}{
		"filename":     filename,
		"content_type": contentType,
		"size_bytes":   len(data),
	})

	req, _ := http.NewRequest("POST", base+"/api/v1/upload/presign", bytes.NewReader(presignBody))
	req.Header.Set("Authorization", "Bearer "+token)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		body, _ := io.ReadAll(resp.Body)
		return "", "", fmt.Errorf("presign failed (%d): %s", resp.StatusCode, body)
	}

	var presign struct {
		UploadID     string `json:"upload_id"`
		PresignedURL string `json:"presigned_url"`
	}
	json.NewDecoder(resp.Body).Decode(&presign)

	// Step 3: Upload to storage (no auth header)
	putReq, _ := http.NewRequest("PUT", presign.PresignedURL, bytes.NewReader(data))
	putReq.Header.Set("Content-Type", contentType)

	putResp, err := http.DefaultClient.Do(putReq)
	if err != nil {
		return "", "", err
	}
	defer putResp.Body.Close()

	if putResp.StatusCode != 200 {
		return "", "", fmt.Errorf("storage upload failed: %d", putResp.StatusCode)
	}

	// Step 4: Confirm
	confirmBody, _ := json.Marshal(map[string]string{"upload_id": presign.UploadID})
	confirmReq, _ := http.NewRequest("POST", base+"/api/v1/upload/confirm", bytes.NewReader(confirmBody))
	confirmReq.Header.Set("Authorization", "Bearer "+token)
	confirmReq.Header.Set("Content-Type", "application/json")

	confirmResp, err := http.DefaultClient.Do(confirmReq)
	if err != nil {
		return "", "", err
	}
	defer confirmResp.Body.Close()

	if confirmResp.StatusCode != 200 {
		body, _ := io.ReadAll(confirmResp.Body)
		return "", "", fmt.Errorf("confirm failed (%d): %s", confirmResp.StatusCode, body)
	}

	var result struct {
		File struct {
			ID        string `json:"id"`
			PublicURL string `json:"public_url"`
		} `json:"file"`
	}
	json.NewDecoder(confirmResp.Body).Decode(&result)
	return result.File.PublicURL, result.File.ID, nil
}

func listFiles(token string) error {
	req, _ := http.NewRequest("GET", base+"/api/v1/files", nil)
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	var result struct {
		Files []struct {
			OriginalName string `json:"original_name"`
			PublicURL    string `json:"public_url"`
		} `json:"files"`
	}
	json.NewDecoder(resp.Body).Decode(&result)

	for _, f := range result.Files {
		fmt.Printf("%s  %s\n", f.OriginalName, f.PublicURL)
	}
	return nil
}

func deleteFile(token, fileID string) error {
	req, _ := http.NewRequest("DELETE", base+"/api/v1/files/"+fileID, nil)
	req.Header.Set("Authorization", "Bearer "+token)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 && resp.StatusCode != 204 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("delete failed (%d): %s", resp.StatusCode, body)
	}
	return nil
}

Ruby

Complete example using net/http — no external gems required.

ruby
# Complete PutPut upload flow — Ruby
require "net/http"
require "json"
require "uri"

BASE = "https://putput-ala.pages.dev"

token = ENV["PUTPUT_TOKEN"]

# Step 1: Get a guest token (if none stored)
unless token
  uri = URI("#{BASE}/api/v1/auth/guest")
  res = Net::HTTP.post(uri, "", {})
  raise "Failed to create guest token: #{res.code}" unless res.is_a?(Net::HTTPSuccess)

  guest = JSON.parse(res.body)
  token = guest["token"]
  puts "Token: #{token}"
  puts "Claim URL: #{guest["claim_url"]}"  # Save — user needs it to upgrade
end

def api_request(method, path, token, body = nil)
  uri = URI("#{BASE}#{path}")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == "https"

  req = case method
        when :get    then Net::HTTP::Get.new(uri)
        when :post   then Net::HTTP::Post.new(uri)
        when :put    then Net::HTTP::Put.new(uri)
        when :delete then Net::HTTP::Delete.new(uri)
        end

  req["Authorization"] = "Bearer #{token}" if token
  if body
    req["Content-Type"] = "application/json"
    req.body = body.to_json
  end

  http.request(req)
end

# Step 2: Presign
presign_res = api_request(:post, "/api/v1/upload/presign", token, {
  filename: "hello.txt",
  content_type: "text/plain",
  size_bytes: 12
})
raise "Presign failed: #{presign_res.body}" unless presign_res.is_a?(Net::HTTPSuccess)

presign = JSON.parse(presign_res.body)
upload_id = presign["upload_id"]
presigned_url = presign["presigned_url"]

# Step 3: Upload to storage (no auth header)
put_uri = URI(presigned_url)
put_http = Net::HTTP.new(put_uri.host, put_uri.port)
put_http.use_ssl = put_uri.scheme == "https"
put_req = Net::HTTP::Put.new(put_uri)
put_req["Content-Type"] = "text/plain"
put_req.body = "Hello world!"
put_res = put_http.request(put_req)
raise "Storage upload failed: #{put_res.code}" unless put_res.is_a?(Net::HTTPSuccess)

# Step 4: Confirm
confirm_res = api_request(:post, "/api/v1/upload/confirm", token, {
  upload_id: upload_id
})
raise "Confirm failed: #{confirm_res.body}" unless confirm_res.is_a?(Net::HTTPSuccess)

file = JSON.parse(confirm_res.body)["file"]
puts "Uploaded: #{file["public_url"]}"

# List files
list_res = api_request(:get, "/api/v1/files", token)
files = JSON.parse(list_res.body)
files["files"].each do |f|
  puts "#{f["original_name"]}  #{f["public_url"]}"
end

# Delete the file
delete_res = api_request(:delete, "/api/v1/files/#{file["id"]}", token)
puts "Deleted: #{file["id"]}"

PHP

Complete example using curl — standard PHP, no Composer packages needed.

php
<?php
// Complete PutPut upload flow — PHP

$base = "https://putput-ala.pages.dev";
$token = getenv("PUTPUT_TOKEN");

// Step 1: Get a guest token (if none stored)
if (!$token) {
    $ch = curl_init("$base/api/v1/auth/guest");
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $res = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        die("Failed to create guest token: $res\n");
    }

    $guest = json_decode($res, true);
    $token = $guest["token"];
    echo "Token: $token\n";
    echo "Claim URL: " . $guest["claim_url"] . "\n";
}

function apiRequest(string $method, string $url, string $token, ?array $body = null): array {
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $headers = ["Authorization: Bearer $token"];
    if ($body !== null) {
        $headers[] = "Content-Type: application/json";
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ["code" => $httpCode, "body" => $response];
}

// Step 2: Presign
$fileData = "Hello world!";
$presign = apiRequest("POST", "$base/api/v1/upload/presign", $token, [
    "filename" => "hello.txt",
    "content_type" => "text/plain",
    "size_bytes" => strlen($fileData),
]);

if ($presign["code"] !== 200) {
    die("Presign failed: " . $presign["body"] . "\n");
}

$presignData = json_decode($presign["body"], true);
$uploadId = $presignData["upload_id"];
$presignedUrl = $presignData["presigned_url"];

// Step 3: Upload to storage (no auth header)
$ch = curl_init($presignedUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_POSTFIELDS, $fileData);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: text/plain"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$putCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_exec($ch);
curl_close($ch);

// Step 4: Confirm
$confirm = apiRequest("POST", "$base/api/v1/upload/confirm", $token, [
    "upload_id" => $uploadId,
]);

if ($confirm["code"] !== 200) {
    die("Confirm failed: " . $confirm["body"] . "\n");
}

$file = json_decode($confirm["body"], true)["file"];
echo "Uploaded: " . $file["public_url"] . "\n";

// List files
$list = apiRequest("GET", "$base/api/v1/files", $token);
$files = json_decode($list["body"], true);
foreach ($files["files"] as $f) {
    echo $f["original_name"] . "  " . $f["public_url"] . "\n";
}

// Delete the file
$delete = apiRequest("DELETE", "$base/api/v1/files/" . $file["id"], $token);
echo "Deleted: " . $file["id"] . "\n";

Error handling

All API errors follow a consistent shape. Check error.code for programmatic handling:

javascript
const res = await fetch(`${BASE}/api/v1/upload/presign`, { ... });
if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "UNAUTHORIZED":
      // Token expired or invalid — get a new one
      break;
    case "FILE_TOO_LARGE":
    case "STORAGE_LIMIT_EXCEEDED":
      // Show error.message to user
      console.log(error.hint); // e.g. "Upgrade to Free for 10 GB"
      break;
    case "CONTENT_TYPE_BLOCKED":
      // Guest token — claim account for this file type
      break;
    case "RATE_LIMITED":
      // Back off and retry
      break;
    default:
      console.error(error.message);
  }
}

See the full error code reference for all possible codes.

More resources

v0.4.78