style(retro): style API documentation as Win98 windows

Re-skin the API documentation layout for the retro theme to ensure readability and maintain the Windows 98 aesthetic. The default dark revamp tokens were unreadable on the black retro desktop background.

Changes include:
- Styling the API sidebar as a raised silver window with a classic title bar.
- Styling endpoint cards as silver windows with navy title bars.
- Excluding API navigation links, shortcut cards, and link pills from default retro link styles to prevent styling conflicts.
- Updating API documentation content, including adding a section for resumable uploads.
This commit is contained in:
2026-06-11 09:19:06 +03:00
parent 6a7590493c
commit a0027fbd18
8 changed files with 1410 additions and 114 deletions

View File

@@ -0,0 +1,81 @@
#requires -version 5
<#
.SYNOPSIS
warpbox: command line uploader for Warpbox
.DESCRIPTION
Set the server once, then upload anything:
setx WARPBOX_HOST "https://your.warpbox.host"
warpbox .\report.pdf
Install (PowerShell):
iwr "$env:WARPBOX_HOST/static/api/warpbox.ps1" -OutFile $HOME\warpbox.ps1
# add a function to your $PROFILE: function warpbox { & "$HOME\warpbox.ps1" @args }
Auth: set the token once so it never lands in your command history.
setx WARPBOX_TOKEN "wbx_your_token"
Create a token under Account, Access tokens.
.EXAMPLE
.\warpbox.ps1 .\report.pdf
.EXAMPLE
.\warpbox.ps1 -Password 123 -Expiry 2d .\photo.png .\clip.mp4
#>
[CmdletBinding()]
param(
[Alias('p')][string]$Password,
[Alias('e')][string]$Expiry,
[Alias('n')][int]$MaxDownloads,
[Alias('o')][switch]$Obfuscate,
[string]$Server = $env:WARPBOX_HOST,
[string]$Auth = $env:WARPBOX_TOKEN,
[string]$AuthFile,
[switch]$Json,
[switch]$Help,
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Files
)
if ($Help -or -not $Files) {
Write-Host 'warpbox: upload files to Warpbox'
Write-Host 'USAGE: warpbox.ps1 [-Password pw] [-Expiry 2d] [-MaxDownloads n] [-Obfuscate] [-Json] <file> [file ...]'
Write-Host 'SERVER: set WARPBOX_HOST in your environment (setx WARPBOX_HOST "https://your.host")'
Write-Host 'AUTH: set WARPBOX_TOKEN in your environment (setx WARPBOX_TOKEN "wbx_...")'
if (-not $Files -and -not $Help) { exit 2 } else { exit 0 }
}
if (-not $Server) {
Write-Error 'warpbox: no server set. Use -Server <url> or set WARPBOX_HOST'
exit 2
}
if ($AuthFile) { $Auth = (Get-Content -Raw $AuthFile).Trim() }
function ConvertTo-Minutes($v) {
if ($v -match '^(\d+)([mhdw]?)$') {
$n = [int]$Matches[1]
switch ($Matches[2]) {
'h' { return $n * 60 }
'd' { return $n * 1440 }
'w' { return $n * 10080 }
default { return $n }
}
}
return $v
}
# Expand wildcards (PowerShell does not expand them in arguments).
$expanded = @()
foreach ($f in $Files) {
$hits = Get-ChildItem -Path $f -File -ErrorAction SilentlyContinue
if ($hits) { $expanded += $hits.FullName } else { $expanded += $f }
}
$curlArgs = @('-fS')
foreach ($f in $expanded) { $curlArgs += @('-F', "file=@$f") }
if ($Password) { $curlArgs += @('-F', "password=$Password") }
if ($Expiry) { $curlArgs += @('-F', "expires_minutes=$(ConvertTo-Minutes $Expiry)") }
if ($MaxDownloads) { $curlArgs += @('-F', "max_downloads=$MaxDownloads") }
if ($Obfuscate) { $curlArgs += @('-F', 'obfuscate_metadata=on') }
if ($Auth) { $curlArgs += @('-H', "Authorization: Bearer $Auth") }
if ($Json) { $curlArgs += @('-H', 'Accept: application/json') }
$curlArgs += "$($Server.TrimEnd('/'))/api/v1/upload"
& curl.exe @curlArgs

View File

@@ -0,0 +1,120 @@
#!/usr/bin/env bash
#
# warpbox: command line uploader for Warpbox
#
# Set the server once, then upload anything:
# export WARPBOX_HOST=https://your.warpbox.host
# warpbox ./report.pdf
#
# Install:
# curl -fsSL "$WARPBOX_HOST/static/api/warpbox.sh" -o ~/.local/bin/warpbox
# chmod +x ~/.local/bin/warpbox
# # make sure ~/.local/bin is on your PATH
#
set -eo pipefail
WARPBOX_HOST="${WARPBOX_HOST:-}"
AUTH="${WARPBOX_TOKEN:-}"
PASSWORD=""
EXPIRY=""
MAX_DOWNLOADS=""
OBFUSCATE=""
AS_JSON=0
FILES=()
usage() {
cat <<'EOF'
warpbox: upload files to Warpbox from the terminal
USAGE:
warpbox [options] <file> [file ...]
OPTIONS:
-p, --password <pw> Require a password to view/download the box
-e, --expiry <dur> Lifetime before expiry: 30m, 6h, 2d, 1w (or bare minutes)
-n, --max-downloads <n> Expire after N downloads
-o, --obfuscate Hide file names/counts until unlocked (needs --password)
--host <url> Warpbox server to upload to (or set WARPBOX_HOST)
--auth <token> API token (prefer the WARPBOX_TOKEN env var, see AUTH)
--auth-file <path> Read the API token from a file (safer than --auth)
--json Print the full JSON response instead of just the URL
-h, --help Show this help
AUTH:
Uploads are anonymous unless a token is supplied. The most secure option is the
WARPBOX_TOKEN environment variable, so the token never lands in your shell
history or the process list:
export WARPBOX_TOKEN=wbx_your_token
warpbox ./photo.png
Create a token under Account, Access tokens. Avoid --auth on shared machines.
EXAMPLES:
warpbox ./report.pdf
warpbox --password 123 --expiry 2d ./first_file.zip ./whatever.png ./all_*_photos.jpg
warpbox --max-downloads 5 --json ./build.zip
EOF
}
expiry_to_minutes() {
local v="$1" num unit
num="${v%%[mhdw]*}"
unit="${v##*[0-9]}"
case "$unit" in
h) echo $(( num * 60 )) ;;
d) echo $(( num * 1440 )) ;;
w) echo $(( num * 10080 )) ;;
m|"") echo "$num" ;;
*) echo "$num" ;;
esac
}
while [ $# -gt 0 ]; do
case "$1" in
-p|--password) PASSWORD="$2"; shift 2 ;;
-e|--expiry) EXPIRY="$2"; shift 2 ;;
-n|--max-downloads) MAX_DOWNLOADS="$2"; shift 2 ;;
-o|--obfuscate) OBFUSCATE="on"; shift ;;
--host) WARPBOX_HOST="$2"; shift 2 ;;
--auth) AUTH="$2"; shift 2 ;;
--auth-file) AUTH="$(cat "$2")"; shift 2 ;;
--json) AS_JSON=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; while [ $# -gt 0 ]; do FILES+=("$1"); shift; done ;;
-*) echo "warpbox: unknown option $1" >&2; exit 2 ;;
*) FILES+=("$1"); shift ;;
esac
done
if [ -z "$WARPBOX_HOST" ]; then
echo "warpbox: no server set. Use --host <url> or export WARPBOX_HOST=<url>" >&2
exit 2
fi
if [ ${#FILES[@]} -eq 0 ]; then
echo "warpbox: no files given" >&2
echo >&2
usage >&2
exit 2
fi
CURL_ARGS=()
for f in "${FILES[@]}"; do
if [ ! -f "$f" ]; then
echo "warpbox: not a file: $f" >&2
exit 2
fi
CURL_ARGS+=(-F "file=@${f}")
done
[ -n "$PASSWORD" ] && CURL_ARGS+=(-F "password=${PASSWORD}")
[ -n "$EXPIRY" ] && CURL_ARGS+=(-F "expires_minutes=$(expiry_to_minutes "$EXPIRY")")
[ -n "$MAX_DOWNLOADS" ] && CURL_ARGS+=(-F "max_downloads=${MAX_DOWNLOADS}")
[ -n "$OBFUSCATE" ] && CURL_ARGS+=(-F "obfuscate_metadata=on")
HEADERS=()
[ -n "$AUTH" ] && HEADERS+=(-H "Authorization: Bearer ${AUTH}")
[ "$AS_JSON" -eq 1 ] && HEADERS+=(-H "Accept: application/json")
exec curl -fS "${HEADERS[@]}" "${CURL_ARGS[@]}" "${WARPBOX_HOST%/}/api/v1/upload"

View File

@@ -152,16 +152,16 @@
/* Links: classic blue, underlined, purple when visited. Sidebar links and tabs
are styled as their own Win98 controls below, so they're excluded here. */
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link) {
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill) {
color: #0000ee;
text-decoration: underline;
}
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):visited {
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill):visited {
color: #551a8b;
}
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):hover {
:root[data-theme="retro"] a:not(.button):not(.brand):not(.sidebar-link):not(.tab):not(.sort-link):not(.api-nav-link):not(.shortcut-card):not(.link-pill):hover {
color: #ee0000;
}
@@ -741,3 +741,219 @@
:root[data-theme="retro"] .file-main small {
color: inherit;
}
/* ------------------------------------------------------------------------- */
/* API documentation: sidebar + panels as Win98 windows */
/* The new .api-docs layout uses dark revamp tokens by default, which are */
/* unreadable on the black retro desktop. Re-skin it as Win98 chrome: a */
/* raised silver sidebar window, plain light section intros on the desktop, */
/* and each card a silver window with a navy title bar from its heading. */
/* ------------------------------------------------------------------------- */
/* Sidebar = raised silver window with a real title bar from its <h1>. */
:root[data-theme="retro"] .api-sidebar {
background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%);
background-color: #c0c0c0;
border: 1px solid #000000;
box-shadow: var(--shadow);
padding: 0.5rem;
gap: 0.5rem;
}
:root[data-theme="retro"] .api-sidebar > .kicker {
display: none;
}
:root[data-theme="retro"] .api-sidebar-title {
margin: -0.5rem -0.5rem 0.5rem;
font-size: 0.9rem;
}
:root[data-theme="retro"] .api-nav {
border-left: 0;
padding-left: 0;
gap: 0.2rem;
}
/* Nav entries are flat silver list items; the active one is a navy bar. */
:root[data-theme="retro"] .api-nav-link {
color: #000000;
font-weight: 700;
text-decoration: none;
border: 1px solid transparent;
}
:root[data-theme="retro"] .api-nav-link:hover {
background: #d4d0c8;
color: #000000;
}
:root[data-theme="retro"] .api-nav-link.is-active {
background: linear-gradient(to right, #000078, 80%, #0f80cd);
color: #ffffff;
border-color: #000000;
}
:root[data-theme="retro"] .api-sidebar-meta {
border-top: 1px solid #808080;
box-shadow: 0 -1px 0 #ffffff;
padding-top: 0.5rem;
margin-top: 0.5rem;
}
/* Section intros sit on the black desktop: gold kicker, white title, light
subtitle, and readable inline code (not the default black-on-black). */
:root[data-theme="retro"] .panel-head .kicker {
color: #ffd966;
display: block;
}
:root[data-theme="retro"] .panel-head h2,
:root[data-theme="retro"] .section-label {
color: #ffffff;
}
:root[data-theme="retro"] .panel-head .lead {
color: #cfd8ff;
}
:root[data-theme="retro"] .panel-head .lead code {
color: #ffffff;
background: #000078;
padding: 0 0.2rem;
}
/* Each card heading becomes a Win98 title bar with a fake close button.
Headings bleed to the window edges; only the first hugs the top edge so a
multi-step card (e.g. ShareX) reads as stacked group bars, not overlaps. */
:root[data-theme="retro"] .api-content .card > .card-content > h3 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
margin: 1.5rem -1.5rem 1rem;
padding: 0.35rem 0.5rem;
background: linear-gradient(to right, #000078, 80%, #0f80cd);
color: #ffffff;
font-size: 1rem;
font-weight: 700;
}
:root[data-theme="retro"] .api-content .card > .card-content > h3:first-child {
margin-top: -1.5rem;
}
/* The upload endpoint card leads with a method + path row; make that the bar. */
:root[data-theme="retro"] .api-content .endpoint-head {
margin: -1.5rem -1.5rem 1rem;
padding: 0.3rem 0.5rem;
background: linear-gradient(to right, #000078, 80%, #0f80cd);
}
:root[data-theme="retro"] .endpoint-head .endpoint-path {
color: #ffffff;
}
:root[data-theme="retro"] .api-content .card > .card-content > h3::after,
:root[data-theme="retro"] .api-content .endpoint-head::after {
content: "\2715";
display: grid;
place-items: center;
width: 1.15rem;
height: 1rem;
margin-left: auto;
background: #c0c0c0;
color: #000000;
font-size: 0.7rem;
font-weight: 700;
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
}
/* Body text inside windows reads black, not muted purple. */
:root[data-theme="retro"] .api-content .card p,
:root[data-theme="retro"] .api-content .card h4,
:root[data-theme="retro"] .api-content .field-grid span,
:root[data-theme="retro"] .endpoint-list div em,
:root[data-theme="retro"] .faq-item summary,
:root[data-theme="retro"] .faq-item p {
color: #1a1a1a;
}
/* Sub-labels (Request fields, Example, ...) become small black headers. */
:root[data-theme="retro"] .api-content .card h4 {
text-transform: none;
letter-spacing: 0;
}
/* Endpoint rows are sunken white fields. */
:root[data-theme="retro"] .endpoint-list div {
background: #ffffff;
border: 1px solid #000000;
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
}
/* Home shortcut tiles and quick links: silver windows / sunken white fields. */
:root[data-theme="retro"] .shortcut-card {
background: linear-gradient(to bottom, #ffffff, 6%, #c0c0c0 10%);
background-color: #c0c0c0;
border: 1px solid #000000;
box-shadow: inset -1px -1px 0 #404040, inset 1px 1px 0 #ffffff, inset -2px -2px 0 #808080, inset 2px 2px 0 #dfdfdf;
}
:root[data-theme="retro"] .shortcut-card:hover {
transform: none;
background-color: #d4d0c8;
}
:root[data-theme="retro"] .shortcut-eyebrow {
color: #000078;
}
:root[data-theme="retro"] .shortcut-title,
:root[data-theme="retro"] .shortcut-sub {
color: #1a1a1a;
}
:root[data-theme="retro"] .link-pill {
background: #ffffff;
border: 1px solid #000000;
box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #ffffff;
color: #000000;
}
:root[data-theme="retro"] .link-pill span {
background: #000078;
color: #ffffff;
border: 1px solid #000000;
}
/* CLI download cards = silver windows. */
:root[data-theme="retro"] .download-card {
background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%);
background-color: #c0c0c0;
border: 1px solid #000000;
box-shadow: var(--shadow);
}
:root[data-theme="retro"] .download-card .download-os,
:root[data-theme="retro"] .download-card p {
color: #1a1a1a;
}
/* FAQ entries are silver windows; the +/- marker stays. */
:root[data-theme="retro"] .faq-item {
background: linear-gradient(to bottom, #ffffff, 4%, #c0c0c0 8%);
background-color: #c0c0c0;
border: 1px solid #000000;
box-shadow: var(--shadow);
}
:root[data-theme="retro"] .faq-item summary::after {
color: #000000;
}
/* Copy buttons: stay visible (retro already paints them as silver buttons). */
:root[data-theme="retro"] .code-block .copy-btn {
background: #c0c0c0;
opacity: 1;
}

View File

@@ -10,6 +10,408 @@
padding: 2rem 0 3rem;
}
/* ============================================================
API documentation — sidebar layout
============================================================ */
.api-docs {
width: min(74rem, calc(100% - 2rem));
margin: 0 auto;
padding: 2rem 0 3rem;
display: grid;
grid-template-columns: 13.5rem minmax(0, 1fr);
gap: 2rem;
align-items: start;
}
.api-sidebar {
position: sticky;
top: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.api-sidebar-title {
margin: 0 0 0.75rem;
font-size: 1.15rem;
}
.api-nav {
display: flex;
flex-direction: column;
gap: 0.15rem;
border-left: 1px solid var(--border);
padding-left: 0.3rem;
}
.api-nav-link {
display: block;
padding: 0.45rem 0.7rem;
border-radius: calc(var(--radius) - 0.3rem);
color: var(--muted-foreground);
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
line-height: 1.2;
transition: background 0.12s ease, color 0.12s ease;
}
.api-nav-link:hover {
background: var(--muted);
color: var(--foreground);
}
.api-nav-link.is-active {
background: color-mix(in srgb, var(--primary) 16%, transparent);
color: var(--foreground);
}
.api-sidebar-meta {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
font-size: 0.8rem;
}
.api-sidebar-meta a {
color: var(--muted-foreground);
}
/* --- Panels: only one visible at a time --- */
.api-content {
min-width: 0;
}
.doc-panel {
display: none;
outline: none;
}
.doc-panel.is-active {
display: block;
animation: doc-fade 0.18s ease;
}
@keyframes doc-fade {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
.panel-head {
max-width: 46rem;
margin-bottom: 1.5rem;
}
.panel-head h2 {
margin: 0;
font-size: 1.5rem;
}
.panel-head .lead {
margin: 0.6rem 0 0;
color: var(--muted-foreground);
font-size: 0.95rem;
line-height: 1.6;
}
.api-content .card + .card,
.api-content .quickstart {
margin-top: 1rem;
}
.api-content h3 {
margin: 0;
font-size: 1.05rem;
}
.api-content h4 {
margin: 1.4rem 0 0;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--muted-foreground);
}
.api-content .card p {
margin: 0.65rem 0 0;
color: var(--muted-foreground);
font-size: 0.9rem;
line-height: 1.6;
}
.api-content code {
color: var(--foreground);
}
.api-content .field-grid p {
margin: 0;
}
.section-label {
margin: 1.75rem 0 0.75rem !important;
font-size: 0.8rem !important;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--muted-foreground);
}
/* --- Home shortcuts --- */
.shortcut-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
gap: 0.75rem;
margin-bottom: 1.25rem;
}
.shortcut-card {
display: flex;
flex-direction: column;
gap: 0.2rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: color-mix(in srgb, var(--card) 94%, transparent);
text-decoration: none;
transition: border-color 0.12s ease, transform 0.12s ease;
}
.shortcut-card:hover {
border-color: var(--ring);
transform: translateY(-2px);
}
.shortcut-eyebrow {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--primary);
}
.shortcut-title {
font-size: 1rem;
font-weight: 650;
color: var(--foreground);
}
.shortcut-sub {
font-size: 0.82rem;
color: var(--muted-foreground);
}
.link-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
gap: 0.5rem;
}
.link-pill {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.85rem;
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 0.2rem);
background: var(--card);
color: var(--foreground);
font-size: 0.88rem;
text-decoration: none;
transition: border-color 0.12s ease;
}
.link-pill:hover {
border-color: var(--ring);
}
.link-pill span {
flex: none;
min-width: 2.6rem;
text-align: center;
padding: 0.15rem 0.35rem;
border-radius: 0.3rem;
background: var(--muted);
color: var(--muted-foreground);
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.03em;
}
/* --- Code blocks with copy button --- */
.code-block {
position: relative;
margin: 0;
}
.code-block .copy-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.3rem 0.6rem;
border: 1px solid var(--border);
border-radius: 0.4rem;
background: color-mix(in srgb, var(--card) 80%, transparent);
color: var(--muted-foreground);
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.code-block:hover .copy-btn,
.code-block .copy-btn:focus-visible {
opacity: 1;
}
.code-block .copy-btn:hover {
color: var(--foreground);
border-color: var(--ring);
}
/* --- Endpoint blocks --- */
.endpoint-head {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
}
.endpoint-path {
font-size: 0.95rem;
font-weight: 600;
}
.method {
flex: none;
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 0.35rem;
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.04em;
color: #fff;
}
.method-get { background: #2563eb; }
.method-post { background: #16a34a; }
.method-put { background: #d97706; }
.endpoint-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin: 1rem 0 0;
}
.endpoint-list div {
display: flex;
align-items: center;
gap: 0.6rem;
flex-wrap: wrap;
padding: 0.55rem 0.7rem;
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 0.3rem);
background: var(--background);
}
.endpoint-list div code {
font-size: 0.82rem;
word-break: break-all;
}
.endpoint-list div em {
margin-left: auto;
color: var(--muted-foreground);
font-size: 0.8rem;
font-style: normal;
}
/* --- CLI download cards --- */
.download-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
}
.download-card {
padding: 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: color-mix(in srgb, var(--card) 94%, transparent);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.download-card .download-os {
font-size: 1.05rem;
font-weight: 650;
color: var(--foreground);
}
.download-card p {
margin: 0;
color: var(--muted-foreground);
font-size: 0.88rem;
}
.download-card .button {
margin-top: auto;
}
/* --- FAQ --- */
.faq-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.faq-item {
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 0.2rem);
background: color-mix(in srgb, var(--card) 94%, transparent);
padding: 0 1rem;
}
.faq-item summary {
padding: 0.9rem 0;
cursor: pointer;
font-weight: 600;
color: var(--foreground);
list-style: none;
position: relative;
padding-right: 1.5rem;
}
.faq-item summary::-webkit-details-marker {
display: none;
}
.faq-item summary::after {
content: "+";
position: absolute;
right: 0.1rem;
top: 50%;
transform: translateY(-50%);
color: var(--muted-foreground);
font-size: 1.1rem;
}
.faq-item[open] summary::after {
content: "\2212";
}
.faq-item p {
margin: 0 0 0.95rem;
color: var(--muted-foreground);
font-size: 0.9rem;
line-height: 1.6;
}
.docs-header {
max-width: 44rem;
}
@@ -63,42 +465,19 @@
grid-column: 1 / -1;
}
.endpoint-list,
.field-grid {
display: grid;
gap: 0.65rem;
margin: 1rem 0 0;
}
.endpoint-list div,
.field-grid {
min-width: 0;
}
.endpoint-list div {
display: grid;
grid-template-columns: 7rem minmax(0, 1fr);
gap: 0.75rem;
align-items: baseline;
}
.endpoint-list dt,
.endpoint-list dd {
margin: 0;
min-width: 0;
}
.endpoint-list dt,
.field-grid span {
color: var(--muted-foreground);
font-size: 0.78rem;
font-weight: 700;
}
.endpoint-list dd code {
display: block;
}
.docs-steps {
margin: 0.85rem 0 0;
padding-left: 1.1rem;

View File

@@ -57,6 +57,44 @@
grid-template-columns: 1fr;
}
.api-docs {
grid-template-columns: 1fr;
gap: 1.25rem;
}
.api-sidebar {
position: static;
top: auto;
}
.api-sidebar-title {
margin-bottom: 0.5rem;
}
.api-nav {
flex-direction: row;
flex-wrap: wrap;
border-left: 0;
padding-left: 0;
gap: 0.35rem;
}
.api-nav-link {
border: 1px solid var(--border);
}
.api-sidebar-meta {
flex-direction: row;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 0.5rem;
}
.endpoint-list div em {
margin-left: 0;
width: 100%;
}
.app-sidebar {
position: static;
width: 100%;

View File

@@ -0,0 +1,94 @@
(function () {
const root = document.querySelector("[data-api-docs]");
if (!root) {
return;
}
const panels = Array.from(root.querySelectorAll("[data-doc-panel]"));
const navLinks = Array.from(root.querySelectorAll("[data-doc-link]"));
const DEFAULT = "home";
function activate(name, focus) {
let matched = false;
panels.forEach((panel) => {
const on = panel.dataset.docPanel === name;
panel.classList.toggle("is-active", on);
if (on) {
matched = true;
}
});
if (!matched) {
return false;
}
root.querySelectorAll(".api-nav-link").forEach((link) => {
link.classList.toggle(
"is-active",
link.getAttribute("href") === "#" + name
);
});
if (focus) {
const panel = root.querySelector('[data-doc-panel="' + name + '"]');
if (panel) {
panel.focus({ preventScroll: true });
}
}
return true;
}
// Resolve the current hash to a panel. The hash can point at a panel id
// (e.g. #endpoints) or at any element inside a panel (e.g. #ep-upload),
// letting FAQ answers deep-link straight into the reference.
function resolveHash(focus) {
const id = (location.hash || "").slice(1);
if (!id) {
activate(DEFAULT, focus);
return;
}
const target = document.getElementById(id);
if (!target) {
activate(DEFAULT, focus);
return;
}
const panel = target.closest("[data-doc-panel]");
const name = panel ? panel.dataset.docPanel : DEFAULT;
activate(name, focus && target === panel);
if (panel && target !== panel) {
// Scroll the deep-linked element into view once its panel is visible.
window.requestAnimationFrame(() => {
target.scrollIntoView({ block: "start", behavior: "smooth" });
});
} else {
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
window.addEventListener("hashchange", () => resolveHash(true));
navLinks.forEach((link) => {
link.addEventListener("click", () => {
// hashchange handles activation; this keeps top-level nav clicks snappy.
if (link.getAttribute("href") === location.hash) {
resolveHash(true);
}
});
});
// Add a copy button to every code block.
root.querySelectorAll(".code-block").forEach((block) => {
const pre = block.querySelector("pre");
if (!pre) {
return;
}
const button = document.createElement("button");
button.type = "button";
button.className = "copy-btn";
button.textContent = "Copy";
button.setAttribute("aria-label", "Copy code");
button.addEventListener("click", () => {
window.Warpbox.copyText(pre.innerText.trim(), button, "Copied");
});
block.appendChild(button);
});
resolveHash(false);
})();

View File

@@ -81,6 +81,7 @@
<script defer src="/static/js/35-pagination.js?version={{.AppVersion}}"></script>
<script defer src="/static/js/40-upload.js?version={{.AppVersion}}"></script>
<script defer src="/static/js/45-preview.js?version={{.AppVersion}}"></script>
<script defer src="/static/js/48-api-docs.js?version={{.AppVersion}}"></script>
</head>
<body class="dark">
<a class="skip-link" href="#main">Skip to content</a>

View File

@@ -1,69 +1,132 @@
{{define "api.html"}}{{template "base" .}}{{end}}
{{define "content"}}
<section class="docs-view" aria-labelledby="api-title">
<div class="docs-header">
<section class="api-docs" aria-labelledby="api-title" data-api-docs>
<aside class="api-sidebar">
<p class="kicker">Developer docs</p>
<h1 id="api-title">Warpbox API</h1>
<p>Anonymous uploads for curl, scripts, and ShareX. The upload endpoint accepts multipart files and returns either plain text or JSON based on the <code>Accept</code> header.</p>
</div>
<h1 id="api-title" class="api-sidebar-title">Warpbox API</h1>
<nav class="api-nav" aria-label="Documentation sections">
<a class="api-nav-link" href="#home" data-doc-link>Home</a>
<a class="api-nav-link" href="#endpoints" data-doc-link>Endpoints</a>
<a class="api-nav-link" href="#cli" data-doc-link>CLI / Binary</a>
<a class="api-nav-link" href="#integrations" data-doc-link>Integrations</a>
<a class="api-nav-link" href="#examples" data-doc-link>Examples</a>
<a class="api-nav-link" href="#faq" data-doc-link>FAQ</a>
</nav>
<div class="api-sidebar-meta">
<a href="{{.Data.RequestSchemaURL}}">Request schema</a>
<a href="{{.Data.ResponseSchemaURL}}">Response schema</a>
</div>
</aside>
<div class="docs-grid">
<article class="card docs-card">
<div class="card-content">
<div class="api-content">
<!-- ===================== HOME ===================== -->
<section id="home" class="doc-panel" data-doc-panel="home" tabindex="-1">
<header class="panel-head">
<p class="kicker">Get started</p>
<h2>Upload files anywhere, from anything</h2>
<p class="lead">Warpbox is a one endpoint upload API. Send a multipart file with <code>curl</code>, a script, ShareX, or the <code>warpbox</code> CLI and get back a shareable box link. Request JSON to also receive private manage and delete URLs.</p>
</header>
<div class="shortcut-grid">
<a class="shortcut-card" href="#examples" data-doc-link>
<span class="shortcut-eyebrow">60-second start</span>
<span class="shortcut-title">Copy-paste examples</span>
<span class="shortcut-sub">curl, wget, HTTPie, Python &amp; more</span>
</a>
<a class="shortcut-card" href="#cli" data-doc-link>
<span class="shortcut-eyebrow">Terminal</span>
<span class="shortcut-title">Install the CLI</span>
<span class="shortcut-sub">One command for macOS, Linux &amp; Windows</span>
</a>
<a class="shortcut-card" href="#endpoints" data-doc-link>
<span class="shortcut-eyebrow">Reference</span>
<span class="shortcut-title">All endpoints</span>
<span class="shortcut-sub">Payloads, responses &amp; status codes</span>
</a>
<a class="shortcut-card" href="#integrations" data-doc-link>
<span class="shortcut-eyebrow">Screenshots</span>
<span class="shortcut-title">ShareX integration</span>
<span class="shortcut-sub">Import once, upload as your account</span>
</a>
</div>
<div class="quickstart card">
<div class="card-content">
<h3>Your first upload</h3>
<p>No account required. This prints one plain box URL you can share immediately.</p>
<figure class="code-block">
<pre><code>curl -F file=@./report.pdf {{.Data.UploadURL}}</code></pre>
</figure>
<p class="muted-copy">Want file URLs, a manage link, and a delete link back? Add <code>-H 'Accept: application/json'</code>. See <a href="#responses" data-doc-link>the JSON response</a>.</p>
</div>
</div>
<h3 class="section-label">Quick links</h3>
<div class="link-grid">
<a class="link-pill" href="#ep-upload" data-doc-link><span>POST</span> Upload endpoint</a>
<a class="link-pill" href="/static/api/warpbox.sh" download><span>GET</span> warpbox.sh (macOS/Linux)</a>
<a class="link-pill" href="/static/api/warpbox.ps1" download><span>GET</span> warpbox.ps1 (Windows)</a>
<a class="link-pill" href="{{.Data.ShareXDownloadURL}}" download><span>GET</span> ShareX .sxcu config</a>
<a class="link-pill" href="{{.Data.RequestSchemaURL}}"><span>JSON</span> Request schema</a>
<a class="link-pill" href="{{.Data.ResponseSchemaURL}}"><span>JSON</span> Response schema</a>
<a class="link-pill" href="/account/settings"><span>KEY</span> Create an API token</a>
<a class="link-pill" href="#faq" data-doc-link><span>?</span> FAQ &amp; troubleshooting</a>
</div>
</section>
<!-- ===================== ENDPOINTS ===================== -->
<section id="endpoints" class="doc-panel" data-doc-panel="endpoints" tabindex="-1">
<header class="panel-head">
<p class="kicker">Reference</p>
<h2>Endpoints</h2>
<dl class="endpoint-list">
<div><dt>Upload</dt><dd><code>POST /api/v1/upload</code></dd></div>
<div><dt>Resumable create</dt><dd><code>POST /api/v1/uploads/resumable</code></dd></div>
<div><dt>Resumable status</dt><dd><code>GET /api/v1/uploads/resumable/{sessionID}</code></dd></div>
<div><dt>Resumable chunk</dt><dd><code>PUT /api/v1/uploads/resumable/{sessionID}/files/{fileID}/chunks/{index}</code></dd></div>
<div><dt>Resumable complete</dt><dd><code>POST /api/v1/uploads/resumable/{sessionID}/complete</code></dd></div>
<div><dt>Health</dt><dd><code>GET /health</code></dd></div>
<div><dt>Request schema</dt><dd><a href="/api/v1/schemas/upload-request.json"><code>/api/v1/schemas/upload-request.json</code></a></dd></div>
<div><dt>Response schema</dt><dd><a href="/api/v1/schemas/upload-response.json"><code>/api/v1/schemas/upload-response.json</code></a></dd></div>
</dl>
</div>
</article>
<p class="lead">Base URL <code>{{.Data.BaseURL}}</code>. Authentication is optional: send <code>Authorization: Bearer &lt;token&gt;</code> to upload as your account and use your account limits, or omit it to upload anonymously.</p>
</header>
<article class="card docs-card docs-card-wide">
<div class="card-content">
<h2>Resumable uploads</h2>
<p>Browser uploads use the resumable API by default. Custom clients can use the same flow: create a session with file metadata, upload exact-sized chunks, then complete the session. Chunks are temporary and are cleaned if the session expires.</p>
<pre><code># 1. Create a session.
curl -s {{.Data.BaseURL}}/api/v1/uploads/resumable \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"files":[{"name":"report.pdf","size":1048576,"contentType":"application/pdf"}],"expiresMinutes":1440}'
<article id="ep-upload" class="endpoint card">
<div class="card-content">
<div class="endpoint-head">
<span class="method method-post">POST</span>
<code class="endpoint-path">/api/v1/upload</code>
</div>
<p>The core endpoint. Accepts a <code>multipart/form-data</code> body with one or more files. Returns a plain box URL by default, or the full JSON object when you send <code>Accept: application/json</code>.</p>
# 2. Upload each chunk using the returned sessionId, file id, and chunkSize.
dd if=./report.pdf bs=8388608 count=1 skip=0 2>/dev/null | \
curl -X PUT --data-binary @- \
{{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/files/FILE_ID/chunks/0
<h4>Request fields</h4>
<div class="field-grid">
<span><code>file</code></span><p>One or more files. Repeat the field for multiple files. Used by curl, browsers, and the CLI.</p>
<span><code>sharex</code></span><p>Alternative file field used by ShareX custom uploader configs. Same behaviour as <code>file</code>.</p>
<span><code>max_days</code></span><p>Optional. Days before the box expires. Defaults to 7.</p>
<span><code>expires_minutes</code></span><p>Optional. Lifetime in minutes. Takes precedence over <code>max_days</code> when &gt; 0. Use it for expiries under a day (e.g. <code>60</code> = one hour).</p>
<span><code>max_downloads</code></span><p>Optional. Auto-expire the box after this many downloads.</p>
<span><code>password</code></span><p>Optional. Password required before viewing or downloading.</p>
<span><code>obfuscate_metadata</code></span><p>Optional <code>on</code>. Hides file names/counts until unlock (only meaningful with a password).</p>
</div>
# 3. Complete after all chunks are present. The response is the normal upload JSON.
curl -X POST -H 'Accept: application/json' \
{{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/complete</code></pre>
<p class="muted-copy">For authenticated uploads, send the same <code>Authorization: Bearer &lt;token&gt;</code> header on every resumable request. Incomplete chunks are stored under <code>data/tmp/uploads</code> before finalizing into the selected storage backend.</p>
</div>
</article>
<h4>Request headers</h4>
<div class="field-grid">
<span><code>Accept</code></span><p><code>application/json</code> to receive the JSON body; otherwise a single plain-text URL.</p>
<span><code>Authorization</code></span><p>Optional <code>Bearer &lt;token&gt;</code>. Attributes the upload to your account.</p>
<span><code>X-Warpbox-Batch</code></span><p>Optional grouping key. Uploads sharing a value within {{.Data.ShareXGroupWindow}} land in the same box. See <a href="#integrations" data-doc-link>Integrations</a>.</p>
</div>
<article class="card docs-card">
<div class="card-content">
<h2>Curl upload</h2>
<p>Without a JSON <code>Accept</code> header, Warpbox prints one plain box URL for shell-friendly usage.</p>
<pre><code>curl -F file=@./report.pdf {{.Data.UploadURL}}</code></pre>
<p>For automation, request JSON to get file URLs and the private manage/delete URLs.</p>
<pre><code>curl -F file=@./report.pdf \
<h4>Example</h4>
<figure class="code-block">
<pre><code>curl -F file=@./report.pdf \
-F max_downloads=5 \
-F expires_minutes=1440 \
-H 'Accept: application/json' \
{{.Data.UploadURL}}</code></pre>
</div>
</article>
</figure>
</div>
</article>
<article class="card docs-card">
<div class="card-content">
<h2>JSON response</h2>
<p>The raw delete token is returned only once inside <code>manageUrl</code> and <code>deleteUrl</code>. Keep those links private. On error the body is <code>{ "error": "message" }</code> with a non-2xx status (e.g. rate limited or over a limit).</p>
<pre><code>{
<article id="responses" class="endpoint card">
<div class="card-content">
<h3>JSON response</h3>
<p>Returned when <code>Accept: application/json</code> is sent. The raw delete token appears <strong>only once</strong>, inside <code>manageUrl</code> and <code>deleteUrl</code>, so store them privately. Full schema: <a href="{{.Data.ResponseSchemaURL}}">upload-response.json</a>.</p>
<figure class="code-block">
<pre><code>{
"boxId": "abc123",
"boxUrl": "{{.Data.BaseURL}}/d/abc123",
"zipUrl": "{{.Data.BaseURL}}/d/abc123/zip",
@@ -81,29 +144,177 @@ curl -X POST -H 'Accept: application/json' \
}
]
}</code></pre>
</figure>
<p class="muted-copy">On error the body is <code>{ "error": "message" }</code> with a non-2xx status. Common causes: <code>413</code> over the size limit, <code>429</code> rate limited or over your daily quota, <code>401</code> bad token.</p>
</div>
</article>
<article id="ep-resumable" class="endpoint card">
<div class="card-content">
<h3>Resumable uploads</h3>
<p>For large files. Browser uploads use this by default. Create a session with file metadata, <code>PUT</code> exact-sized chunks, then complete. Chunks are temporary and cleaned if the session expires. Send the same <code>Authorization</code> header on every request for authenticated sessions.</p>
<div class="endpoint-list">
<div><span class="method method-post">POST</span><code>/api/v1/uploads/resumable</code><em>Create a session</em></div>
<div><span class="method method-get">GET</span><code>/api/v1/uploads/resumable/{sessionID}</code><em>Session status</em></div>
<div><span class="method method-put">PUT</span><code>/api/v1/uploads/resumable/{sessionID}/files/{fileID}/chunks/{index}</code><em>Upload one chunk</em></div>
<div><span class="method method-post">POST</span><code>/api/v1/uploads/resumable/{sessionID}/complete</code><em>Finalize (returns the upload JSON)</em></div>
</div>
<figure class="code-block">
<pre><code># 1. Create a session.
curl -s {{.Data.BaseURL}}/api/v1/uploads/resumable \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"files":[{"name":"report.pdf","size":1048576,"contentType":"application/pdf"}],"expiresMinutes":1440}'
# 2. Upload each chunk using the returned sessionId, file id, and chunkSize.
dd if=./report.pdf bs=8388608 count=1 skip=0 2>/dev/null | \
curl -X PUT --data-binary @- \
{{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/files/FILE_ID/chunks/0
# 3. Complete after all chunks are present. The response is the normal upload JSON.
curl -X POST -H 'Accept: application/json' \
{{.Data.BaseURL}}/api/v1/uploads/resumable/SESSION_ID/complete</code></pre>
</figure>
<p class="muted-copy">Incomplete chunks are stored under <code>data/tmp/uploads</code> before finalizing into the selected storage backend.</p>
</div>
</article>
<article id="ep-meta" class="endpoint card">
<div class="card-content">
<h3>Health &amp; schemas</h3>
<div class="endpoint-list">
<div><span class="method method-get">GET</span><code>/health</code><em>Liveness check</em></div>
<div><span class="method method-get">GET</span><code>/api/v1/schemas/upload-request.json</code><em>Request JSON Schema</em></div>
<div><span class="method method-get">GET</span><code>/api/v1/schemas/upload-response.json</code><em>Response JSON Schema</em></div>
</div>
</div>
</article>
</section>
<!-- ===================== CLI / BINARY ===================== -->
<section id="cli" class="doc-panel" data-doc-panel="cli" tabindex="-1">
<header class="panel-head">
<p class="kicker">Terminal</p>
<h2>The <code>warpbox</code> CLI</h2>
<p class="lead">A tiny uploader script that wraps the API. It only needs <code>curl</code> (already on macOS, Linux, and Windows 10+). Point it at this instance once by setting <code>WARPBOX_HOST</code> to <code>{{.Data.BaseURL}}</code>, then upload from anywhere.</p>
</header>
<div class="download-row">
<div class="download-card">
<div class="download-os">macOS &amp; Linux</div>
<p>POSIX shell script (<code>warpbox.sh</code>).</p>
<a class="button button-primary" href="/static/api/warpbox.sh" download>Download for macOS / Linux</a>
</div>
<div class="download-card">
<div class="download-os">Windows</div>
<p>PowerShell script (<code>warpbox.ps1</code>).</p>
<a class="button button-primary" href="/static/api/warpbox.ps1" download>Download for Windows</a>
</div>
</div>
</article>
<article class="card docs-card">
<div class="card-content">
<article id="cli-install" class="card">
<div class="card-content">
<h3>Install &amp; add to PATH</h3>
<h4>macOS / Linux</h4>
<p>Download into a directory on your <code>PATH</code>, then make it executable. <code>~/.local/bin</code> is the recommended location.</p>
<figure class="code-block">
<pre><code>mkdir -p ~/.local/bin
curl -fsSL {{.Data.BaseURL}}/static/api/warpbox.sh -o ~/.local/bin/warpbox
chmod +x ~/.local/bin/warpbox
# Point it at this instance (add to ~/.profile or ~/.zshrc to keep it set)
echo 'export WARPBOX_HOST={{.Data.BaseURL}}' >> ~/.profile
# If 'warpbox: command not found', add the dir to PATH:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.profile
# zsh users: use ~/.zshrc, then reload with: source ~/.profile</code></pre>
</figure>
<p class="muted-copy">Verify with <code>warpbox --help</code>. Prefer a system wide install? Drop it in <code>/usr/local/bin</code> with <code>sudo</code>.</p>
<h4>Windows (PowerShell)</h4>
<p>Save the script, then add a function to your PowerShell profile so <code>warpbox</code> works anywhere.</p>
<figure class="code-block">
<pre><code># Save it to your home folder
iwr {{.Data.BaseURL}}/static/api/warpbox.ps1 -OutFile $HOME\warpbox.ps1
# Point it at this instance, and add a 'warpbox' command (run once)
setx WARPBOX_HOST "{{.Data.BaseURL}}"
Add-Content $PROFILE 'function warpbox { &amp; "$HOME\warpbox.ps1" @args }'
. $PROFILE # reload the profile</code></pre>
</figure>
<p class="muted-copy">If scripts are blocked, allow local scripts for your user: <code>Set-ExecutionPolicy -Scope CurrentUser RemoteSigned</code>.</p>
</div>
</article>
<article id="cli-usage" class="card">
<div class="card-content">
<h3>Usage</h3>
<p>A password, an expiry of two days, and a glob the shell expands for you:</p>
<figure class="code-block">
<pre><code>warpbox --password 123 --expiry 2d ./first_file.zip ./whatever.png ./all_*_photos.jpg</code></pre>
</figure>
<div class="field-grid">
<span><code>-p, --password</code></span><p>Require a password to open the box.</p>
<span><code>-e, --expiry</code></span><p>Lifetime: <code>30m</code>, <code>6h</code>, <code>2d</code>, <code>1w</code> (or bare minutes).</p>
<span><code>-n, --max-downloads</code></span><p>Expire after N downloads.</p>
<span><code>-o, --obfuscate</code></span><p>Hide names/counts until unlock (needs <code>--password</code>).</p>
<span><code>--json</code></span><p>Print the full JSON response instead of just the URL.</p>
<span><code>--host</code></span><p>Server to upload to. Defaults to your <code>WARPBOX_HOST</code>.</p>
</div>
<p class="muted-copy">Windows uses PowerShell flags: <code>warpbox -Password 123 -Expiry 2d .\file.zip</code>.</p>
</div>
</article>
<article id="cli-auth" class="card">
<div class="card-content">
<h3>Secure authentication</h3>
<p>To upload as your account (and use your account's size, daily, and retention limits), the CLI needs an API token. <strong>Set it in your environment</strong> so it never appears in your shell history or in the process list that any user on the machine can read:</p>
<figure class="code-block">
<pre><code># macOS / Linux (add to ~/.profile or ~/.zshrc to persist)
export WARPBOX_TOKEN=wbx_your_token
warpbox ./photo.png
# Windows (persist for your user)
setx WARPBOX_TOKEN "wbx_your_token"</code></pre>
</figure>
<p>For CI or shared machines, keep the token in a file with locked down permissions and point the CLI at it. This avoids putting the secret on the command line at all:</p>
<figure class="code-block">
<pre><code>printf '%s' "wbx_your_token" > ~/.warpbox-token
chmod 600 ~/.warpbox-token
warpbox --auth-file ~/.warpbox-token ./photo.png</code></pre>
</figure>
<p class="muted-copy"><code>--auth &lt;token&gt;</code> exists for quick tests but is discouraged: it leaks into shell history and <code>ps</code>. Create or revoke tokens under <a href="/account/settings">Account, Access tokens</a>.</p>
</div>
</article>
</section>
<!-- ===================== INTEGRATIONS ===================== -->
<section id="integrations" class="doc-panel" data-doc-panel="integrations" tabindex="-1">
<header class="panel-head">
<p class="kicker">Integrations</p>
<h2>ShareX setup</h2>
<p>Import the uploader, then add your API key to upload as your account — with your account's size, daily, and retention limits — instead of as an anonymous guest.</p>
<p class="lead">Import the uploader once, then optionally add your API key to upload as your account instead of as an anonymous guest.</p>
</header>
<h3>1 · Import the uploader</h3>
<ol class="docs-steps">
<li>Download <a href="/api/v1/sharex/warpbox-anonymous.sxcu"><code>warpbox-anonymous.sxcu</code></a>.</li>
<li>In ShareX: <code>Destinations → Custom uploader settings → Import → From file</code>, then pick the <code>.sxcu</code>.</li>
</ol>
<article id="sharex" class="card">
<div class="card-content">
<h3>1. Import the uploader</h3>
<ol class="docs-steps">
<li>Download <a href="{{.Data.ShareXDownloadURL}}" download><code>warpbox-anonymous.sxcu</code></a>.</li>
<li>In ShareX: <code>Destinations → Custom uploader settings → Import → From file</code>, then pick the <code>.sxcu</code>.</li>
</ol>
<h3>2 · Add your API key (upload as your account)</h3>
<ol class="docs-steps">
<li>Create a personal access token under <a href="/account/settings">Account Access tokens</a> and copy it.</li>
<li>In <code>Custom uploader settings</code>, select the Warpbox uploader and open the <code>Headers</code> section.</li>
<li>Add a header Name <code>Authorization</code>, Value <code>Bearer &lt;your token&gt;</code>.</li>
</ol>
<p class="muted-copy">Without that header, uploads stay anonymous. With it, they're attributed to your account and use your account's limits.</p>
<h3>2. Add your API key (optional, upload as your account)</h3>
<ol class="docs-steps">
<li>Create a personal access token under <a href="/account/settings">Account, Access tokens</a> and copy it.</li>
<li>In <code>Custom uploader settings</code>, select the Warpbox uploader and open the <code>Headers</code> section.</li>
<li>Add a header. Name <code>Authorization</code>, Value <code>Bearer &lt;your token&gt;</code>.</li>
</ol>
<p class="muted-copy">Without that header, uploads stay anonymous. With it, they're attributed to your account and use your account's limits.</p>
<pre><code>{
<figure class="code-block">
<pre><code>{
"Version": "1.0.0",
"Name": "Warpbox (my account)",
"DestinationType": "ImageUploader, FileUploader, TextUploader",
@@ -121,27 +332,183 @@ curl -X POST -H 'Accept: application/json' \
"DeletionURL": "{json:deleteUrl}",
"ErrorMessage": "{json:error}"
}</code></pre>
</figure>
<h3>Grouping multiple files into one box</h3>
<p>Grouping is <strong>opt-in via the <code>X-Warpbox-Batch</code> request header</strong> — without it, every file becomes its own box (the default). When the header is present, uploads sharing the same value (per account, or per IP for anonymous) within {{.Data.ShareXGroupWindow}} of each other are added to the <strong>same</strong> box, so a multi-file ShareX selection produces one shareable link instead of one per file. The shipped config sets <code>X-Warpbox-Batch: sharex</code>; remove that header for one box per file.</p>
<p class="muted-copy">The response also exposes <code>{json:thumbnailUrl}</code> for ShareX previews, <code>{json:deleteUrl}</code> for the deletion URL, and <code>{json:error}</code> so ShareX surfaces messages like rate limiting.</p>
</div>
</article>
<article class="card docs-card docs-card-wide">
<div class="card-content">
<h2>Multipart fields</h2>
<div class="field-grid">
<span><code>file</code></span><p>One or more files for curl, browser, and generic multipart clients.</p>
<span><code>sharex</code></span><p>One or more files from ShareX custom uploader configs.</p>
<span><code>max_days</code></span><p>Optional number of days before expiration. Defaults to 7.</p>
<span><code>expires_minutes</code></span><p>Optional lifetime in minutes. Takes precedence over <code>max_days</code> when greater than zero — useful for sub-day expiries (e.g. <code>60</code> for one hour).</p>
<span><code>max_downloads</code></span><p>Optional download count limit.</p>
<span><code>password</code></span><p>Optional password required before viewing/downloading.</p>
<span><code>obfuscate_metadata</code></span><p>Optional <code>on</code>; hides names/counts until unlock when a password is set.</p>
<h3>Grouping multiple files into one box</h3>
<p>Grouping is <strong>opt in via the <code>X-Warpbox-Batch</code> request header</strong>. Without it, every file becomes its own box (the default). When the header is present, uploads sharing the same value (per account, or per IP for anonymous) within {{.Data.ShareXGroupWindow}} of each other are added to the <strong>same</strong> box, so a ShareX selection of several files produces one shareable link instead of one per file. The shipped config sets <code>X-Warpbox-Batch: sharex</code>; remove that header for one box per file.</p>
<p class="muted-copy">The response also exposes <code>{json:thumbnailUrl}</code> for ShareX previews, <code>{json:deleteUrl}</code> for the deletion URL, and <code>{json:error}</code> so ShareX surfaces messages like rate limiting.</p>
</div>
</article>
</section>
<!-- ===================== EXAMPLES ===================== -->
<section id="examples" class="doc-panel" data-doc-panel="examples" tabindex="-1">
<header class="panel-head">
<p class="kicker">Cookbook</p>
<h2>Examples</h2>
<p class="lead">Every snippet hits <code>POST {{.Data.UploadURL}}</code>. Add <code>-H 'Authorization: Bearer &lt;token&gt;'</code> to any of them to upload as your account.</p>
</header>
<article id="ex-curl" class="card">
<div class="card-content">
<h3>curl</h3>
<p>Plain text (one URL) for the shell; JSON for automation.</p>
<figure class="code-block">
<pre><code># Just the box URL
curl -F file=@./report.pdf {{.Data.UploadURL}}
# Full JSON with manage + delete URLs, password and 1-hour expiry
curl -F file=@./report.pdf \
-F password=hunter2 \
-F expires_minutes=60 \
-H 'Accept: application/json' \
{{.Data.UploadURL}}</code></pre>
</figure>
</div>
</article>
<article id="ex-wget" class="card">
<div class="card-content">
<h3>wget</h3>
<p>The endpoint needs a real <code>multipart/form-data</code> body, which <code>wget</code> can't assemble on its own, so build the body by hand and post it. It also shows the wire format:</p>
<figure class="code-block">
<pre><code>B=----warpbox$$
{ printf -- '--%s\r\nContent-Disposition: form-data; name="file"; filename="report.pdf"\r\nContent-Type: application/octet-stream\r\n\r\n' "$B"
cat ./report.pdf
printf -- '\r\n--%s--\r\n' "$B"; } > /tmp/wb.body
wget --quiet --output-document=- \
--header="Content-Type: multipart/form-data; boundary=$B" \
--header="Accept: application/json" \
--post-file=/tmp/wb.body \
{{.Data.UploadURL}}</code></pre>
</figure>
<p class="muted-copy">Add more form fields (<code>password</code>, <code>expires_minutes</code>, …) by repeating the <code>--%s … Content-Disposition: form-data; name="…"</code> block before the closing boundary. If this feels fiddly, <code>curl</code> or the CLI build the body for you.</p>
</div>
</article>
<article id="ex-httpie" class="card">
<div class="card-content">
<h3>HTTPie</h3>
<p>Multipart with form fields:</p>
<figure class="code-block">
<pre><code>http --multipart POST {{.Data.UploadURL}} \
Accept:application/json \
file@./report.pdf \
max_downloads=3 \
expires_minutes=1440</code></pre>
</figure>
</div>
</article>
<article id="ex-python" class="card">
<div class="card-content">
<h3>Python (requests)</h3>
<figure class="code-block">
<pre><code>import requests
with open("report.pdf", "rb") as f:
r = requests.post(
"{{.Data.UploadURL}}",
headers={"Accept": "application/json"}, # add "Authorization": "Bearer <token>"
files={"file": f},
data={"expires_minutes": 1440, "max_downloads": 5},
)
r.raise_for_status()
print(r.json()["boxUrl"])</code></pre>
</figure>
</div>
</article>
<article id="ex-node" class="card">
<div class="card-content">
<h3>Node.js (fetch)</h3>
<figure class="code-block">
<pre><code>import { readFile } from "node:fs/promises";
const form = new FormData();
form.set("file", new Blob([await readFile("report.pdf")]), "report.pdf");
form.set("expires_minutes", "1440");
const res = await fetch("{{.Data.UploadURL}}", {
method: "POST",
headers: { Accept: "application/json" }, // add Authorization: "Bearer <token>"
body: form,
});
const box = await res.json();
console.log(box.boxUrl);</code></pre>
</figure>
</div>
</article>
<article id="ex-ps" class="card">
<div class="card-content">
<h3>PowerShell</h3>
<p>PowerShell 7+ has native multipart with <code>-Form</code>:</p>
<figure class="code-block">
<pre><code>$resp = Invoke-RestMethod -Uri "{{.Data.UploadURL}}" -Method Post -Headers @{ Accept = "application/json" } -Form @{
file = Get-Item ".\report.pdf"
expires_minutes = 1440
}
$resp.boxUrl</code></pre>
</figure>
<p class="muted-copy">On Windows PowerShell 5.1, use the bundled <code>curl.exe</code> (the same approach the <a href="#cli" data-doc-link>CLI</a> takes) or the <code>warpbox.ps1</code> script.</p>
</div>
</article>
</section>
<!-- ===================== FAQ ===================== -->
<section id="faq" class="doc-panel" data-doc-panel="faq" tabindex="-1">
<header class="panel-head">
<p class="kicker">Help</p>
<h2>FAQ &amp; troubleshooting</h2>
<p class="lead">Quick answers, each linking back to the relevant part of the docs.</p>
</header>
<div class="faq-list">
<details class="faq-item">
<summary>Do I need an account or API key?</summary>
<p>No. Anonymous uploads work without one, see the <a href="#home" data-doc-link>quickstart</a>. Add a token only to upload as your account and use your account's limits; set one up under <a href="/account/settings">Account, Access tokens</a> and pass it as described in <a href="#cli-auth" data-doc-link>CLI authentication</a>.</p>
</details>
<details class="faq-item">
<summary>How do I send a password, expiry, or download limit?</summary>
<p>They're multipart form fields on the upload endpoint: <code>password</code>, <code>expires_minutes</code> (or <code>max_days</code>), and <code>max_downloads</code>. See the full list under <a href="#ep-upload" data-doc-link>Endpoints, request fields</a>, or use the CLI flags in <a href="#cli-usage" data-doc-link>CLI usage</a>.</p>
</details>
<details class="faq-item">
<summary>How do I get file URLs and a delete link back?</summary>
<p>Send <code>Accept: application/json</code>. The response includes <code>boxUrl</code>, per-file <code>url</code>s, and the private <code>manageUrl</code>/<code>deleteUrl</code> (shown only once). See <a href="#responses" data-doc-link>the JSON response</a>.</p>
</details>
<details class="faq-item">
<summary>How do I upload one big file reliably?</summary>
<p>Use the <a href="#ep-resumable" data-doc-link>resumable endpoints</a>: create a session, PUT chunks, then complete. Interrupted uploads can resume from the last chunk.</p>
</details>
<details class="faq-item">
<summary>Can I upload several files into one shareable link?</summary>
<p>Yes. Send the <code>X-Warpbox-Batch</code> header with a shared value within {{.Data.ShareXGroupWindow}}. Details in <a href="#integrations" data-doc-link>Integrations, grouping</a>.</p>
</details>
<details class="faq-item">
<summary>Where's the keep-it-secret way to store my token?</summary>
<p>Use the <code>WARPBOX_TOKEN</code> environment variable or <code>--auth-file</code>, not <code>--auth</code> on the command line. Full guidance in <a href="#cli-auth" data-doc-link>CLI authentication</a>.</p>
</details>
<details class="faq-item">
<summary>My upload returns an error, what do the codes mean?</summary>
<p>Errors come back as <code>{ "error": "message" }</code> with a non-2xx status: <code>413</code> too large, <code>429</code> rate limited / over quota, <code>401</code> invalid token. See <a href="#responses" data-doc-link>error responses</a>.</p>
</details>
<details class="faq-item">
<summary>How do I use Warpbox from ShareX?</summary>
<p>Import the <code>.sxcu</code> and (optionally) add your token header. Step by step with the config in <a href="#integrations" data-doc-link>Integrations, ShareX setup</a>.</p>
</details>
<details class="faq-item">
<summary><code>warpbox: command not found</code> after install?</summary>
<p>The install directory isn't on your <code>PATH</code>. Fix it per your platform in <a href="#cli-install" data-doc-link>Install &amp; add to PATH</a>.</p>
</details>
<details class="faq-item">
<summary>Is there a machine-readable schema?</summary>
<p>Yes: <a href="{{.Data.RequestSchemaURL}}">upload-request.json</a> and <a href="{{.Data.ResponseSchemaURL}}">upload-response.json</a> (JSON Schema 2020-12).</p>
</details>
</div>
</article>
</section>
</div>
</section>
{{end}}