Xeonr Developer Docs
API Specification

S3-Compatible API

Use AWS tooling — SDKs, the aws CLI, rclone — directly against your upl buckets.

Every upl bucket is S3-compatible. The endpoint model mirrors AWS: the base domain is the endpoint root, and each bucket's alias hostname is the bucket root where all object operations land.

ProductionStaging
Endpointhttps://upl.imhttps://upl.fyi
Bucket hostname<alias>.upl.im<alias>.upl.fyi
Bucket nameyour bucket's slug (e.g. myfiles)

Authentication uses S3 access keys with standard AWS Signature V4 — header auth and presigned query URLs both work.

Bucket identity

Every bucket has a slug: a permanent DNS label chosen at creation (or generated for you) that is both its S3 bucket name and a hostname that always resolves — <slug>.upl.im. The slug never changes, so scripts, sync jobs, and stored URLs stay valid for the life of the bucket.

Aliases are separate and optional: additional names, including custom domains, that you can add, rename, and remove freely. A bucket reached through a custom domain still lists and addresses as its slug in S3.

Aliases are purely a mapping: which hostnames reach the bucket and which one is canonical. They do not affect who may read it — that is governed by the bucket's private flag, per-file and per-folder visibility, and access key grants. Adding or removing an alias never changes a bucket's visibility.

Private buckets

New buckets are private by default. CreateBucket treats an omitted private field as true, so a bucket is closed unless you ask for it to be public — content is readable the moment something lands in it, so the safe default is closed. Pass private: false to create a public bucket.

A bucket marked private turns off anonymous access entirely — nothing is served to visitors on any hostname, and per-file or per-folder public settings no longer apply. Content stays fully reachable through:

  • S3 presigned URLs (aws s3 presign, SDK presigners)
  • Share links created in upl, with their own password/expiry/use limits
  • S3 credentials (SigV4 header auth) and the authenticated API

This is the setting for storage buckets that back an application: the data is private by default, and you hand out access deliberately.

Setup

aws configure set aws_access_key_id UPL... --profile upl
aws configure set aws_secret_access_key upl... --profile upl
aws configure set region us-east-1 --profile upl
aws configure set endpoint_url https://upl.im --profile upl
aws configure set s3.addressing_style virtual --profile upl

The profile stores the endpoint (endpoint_url, aws CLI v2.13+), so no --endpoint-url flag is needed on commands. Virtual-hosted addressing is primary (the CLI defaults to path-style for custom endpoints, hence the explicit setting); path-style on the base endpoint also works for tools that prefer it (rclone, s3fs).

aws s3 ls s3://myfiles/ --profile upl
aws s3 cp report.pdf s3://myfiles/docs/report.pdf --profile upl
aws s3 sync ./site s3://myfiles/site/ --profile upl

rclone:

[upl]
type = s3
provider = Other
access_key_id = UPL...
secret_access_key = upl...
endpoint = https://upl.im

Quickstarts

Create an access key on the dashboard's Access Keys page first — the secret is shown once. Replace myfiles with your bucket's alias.

aws CLI

aws configure set aws_access_key_id UPL... --profile upl
aws configure set aws_secret_access_key upl... --profile upl
aws configure set region us-east-1 --profile upl
aws configure set endpoint_url https://upl.im --profile upl
aws configure set s3.addressing_style virtual --profile upl

aws s3 cp ./photo.jpg s3://myfiles/photo.jpg --profile upl
aws s3 presign s3://myfiles/photo.jpg --profile upl --expires-in 3600

Go

package main

import (
	"context"
	"fmt"
	"strings"
	"time"

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

func main() {
	ctx := context.Background()
	cfg, err := config.LoadDefaultConfig(ctx,
		config.WithRegion("us-east-1"),
		config.WithCredentialsProvider(
			credentials.NewStaticCredentialsProvider("UPL...", "upl...", ""),
		),
	)
	if err != nil {
		panic(err)
	}
	client := s3.NewFromConfig(cfg, func(o *s3.Options) {
		o.BaseEndpoint = aws.String("https://upl.im")
	})

	// Upload
	_, err = client.PutObject(ctx, &s3.PutObjectInput{
		Bucket: aws.String("myfiles"),
		Key:    aws.String("hello.txt"),
		Body:   strings.NewReader("hello from Go"),
	})
	if err != nil {
		panic(err)
	}

	// Presign a shareable (CDN) link — works for private files too
	presigner := s3.NewPresignClient(client)
	req, err := presigner.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String("myfiles"),
		Key:    aws.String("hello.txt"),
	}, s3.WithPresignExpires(time.Hour))
	if err != nil {
		panic(err)
	}
	fmt.Println(req.URL) // https://myfiles.upl.im/hello.txt?X-Amz-...
}

Node.js

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

const s3 = new S3Client({
	region: "us-east-1",
	endpoint: "https://upl.im",
	credentials: {
		accessKeyId: "UPL...",
		secretAccessKey: "upl...",
	},
});

// Upload
await s3.send(new PutObjectCommand({
	Bucket: "myfiles",
	Key: "hello.txt",
	Body: "hello from Node",
}));

// Presign a shareable (CDN) link — works for private files too
const url = await getSignedUrl(
	s3,
	new GetObjectCommand({ Bucket: "myfiles", Key: "hello.txt" }),
	{ expiresIn: 3600 },
); // https://myfiles.upl.im/hello.txt?X-Amz-...

Presigned URLs are CDN URLs

Because the bucket hostname is the CDN hostname, a presigned URL is a shareable CDN link — including for private files:

aws s3 presign s3://myfiles/private/report.pdf \
  --endpoint-url https://upl.im --profile upl --expires-in 3600
# → https://myfiles.upl.im/private/report.pdf?X-Amz-Algorithm=...

The link serves through the edge cache with full validation: expiry is honored (max 7 days), tampering with the path, host, or signature fails, and revoking or rotating the access key invalidates its outstanding URLs within a minute.

Cross-origin access (CORS)

Each bucket has a CORS configuration controlling which sites' JavaScript may read its files with fetch or XHR. Manage it in the dashboard (Portal settings → Advanced sharing controls) or with the S3 API:

aws s3api put-bucket-cors --bucket myfiles --profile upl \
  --cors-configuration '{"CORSRules":[{
    "AllowedOrigins":["https://app.example.com"],
    "AllowedMethods":["GET","HEAD"],
    "AllowedHeaders":["*"],
    "ExposeHeaders":["ETag"],
    "MaxAgeSeconds":3600}]}'

aws s3api get-bucket-cors --bucket myfiles --profile upl
aws s3api delete-bucket-cors --bucket myfiles --profile upl

GetBucketCors, PutBucketCors and DeleteBucketCors all work; writes require an access key with an admin grant on the bucket (see access keys).

Defaults. A bucket with no CORS configuration behaves according to its portal mode: CDN-mode buckets are permissive (any origin may read them), and other buckets allow only upl's own dashboard and portal. Buckets that existed before CORS configuration shipped were given an explicit permissive configuration, so their behaviour did not change. Deleting a configuration returns a bucket to its mode-derived default; saving a configuration with no rules is different — that blocks every cross-origin read.

upl's own origins are always allowed, so tightening a bucket can never lock the dashboard out of it.

Embed protection (bucket policies)

CORS governs whether scripts may read your files. It has no effect on another site embedding them with an <img>, <video> or <iframe> tag — those loads never preflight and never look at Access-Control-Allow-Origin. Blocking that needs a referrer check, which upl expresses as a bucket policy:

aws s3api put-bucket-policy --bucket myfiles --profile upl --policy '{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "BlockHotlinking",
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::myfiles/*",
    "Condition": {"StringNotLike": {"aws:Referer": ["https://mysite.example/*", ""]}}
  }]
}'

The empty string in that list is what keeps direct visits and link previews working — requests that send no referrer at all. Drop it and shared links stop previewing in chat apps and documentation tools.

The dashboard has a form that generates exactly this document, so you don't have to write it by hand.

Supported policy subset

upl accepts the part of the policy language it can actually enforce, and rejects anything else rather than storing it — a policy that was accepted but not enforced would tell you your files were protected when they were not.

ElementAccepted
Version2012-10-17, or omitted
EffectDeny only
Principal"*" or {"AWS": "*"}
Actions3:GetObject (s3:* is treated as such)
ResourceAn ARN naming this bucket: arn:aws:s3:::<slug>, /*, or /<prefix>*
ConditionStringEquals, StringNotEquals, StringLike, StringNotLike on aws:Referer

Policies apply to every request for the object, including presigned URLs and signed API calls — matching AWS. A rule that only bound anonymous traffic could be sidestepped by presigning the URL, which is the very case it exists to stop.

Referrer checks are a deterrent, not a guarantee: a determined client can send whatever referrer it likes. For content that must stay private, make the bucket private and hand out presigned links.

Supported operations

GetObject, HeadObject (with Range, If-None-Match, If-Modified-Since), PutObject, CopyObject, DeleteObject, DeleteObjects, ListObjectsV2 (delimiter / only), ListBuckets, HeadBucket, GetBucketLocation, multipart uploads (create, upload part, complete, abort), presigned GET URLs, CORS preflight, Get/Put/DeleteBucketCors, Get/Put/DeleteBucketPolicy.

Grants apply throughout: reads need the key to cover the bucket at read, writes at read-write, and everything is additionally capped by the owner's own live permissions.

Deviations from AWS

  • ETags are not MD5s. They're stable content-version identifiers (quoted, change on every content update). Tools that verify MD5 ETags should use size/mtime comparison (aws s3 sync does by default).
  • Content-MD5 request headers are not validated. Header-auth uploads with an exact x-amz-content-sha256 are verified against the body.
  • Bucket policies accept the Deny/aws:Referer subset above, not the full policy language.
  • Object versioning, ACL/tagging writes, UploadPartCopy, SelectObjectContent, and ListObjects v1 are not supported (the bucket sub-resource reads answer with empty/absent configurations, so probing tools behave).
  • Single-request uploads (and copies) are capped around 200MB — use multipart for anything large (the aws CLI does automatically above 8MB).
  • Buckets are created and managed in the upl dashboard, not via the S3 API.
  • Folders are real upl objects, not virtual prefixes: deleting every file under a prefix leaves the (empty) folder visible in delimiter listings until it's deleted in upl.
  • Anonymous reads of public files work without signing (--no-sign-request, plain curl) on every bucket.

On this page