Skip to content

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.77