docs

pipeout is one endpoint that takes a file and gives back a URL. There is no SDK to install and nothing to configure — if it can make an HTTP request, it can publish.

Quick start

Mint a token at pipeout.sh/app/tokens, put it in your environment, and send a file:

export PIPEOUT_TOKEN=po_9f3c1a7…        # the real one is 43 characters

curl -T report.html -u :$PIPEOUT_TOKEN https://pipeout.sh/a/
https://pipeout.page/r/1f4c9a2b7e05d8613ac2f094
report.html v1 · 7 KB · this version: https://pipeout.page/r/8be31d07f2a45c9018ee6b23

The first line is the artifact URL. Push the same filename again and that URL updates in place.

HostPurpose
pipeout.shApp and API — you push here
pipeout.pageContent — artifacts are served here, never on the app domain

Authentication

Every request carries a token. Two forms, both accepted everywhere:

Authorization: Bearer po_...

curl -u :$PIPEOUT_TOKEN            # HTTP Basic, empty username

Tokens carry scopes. push is required to publish; read is required for the JSON API below. Reading an artifact by its URL needs no token at all — the URL is the credential.

Only a SHA-256 of the secret is stored, so a lost token cannot be recovered, only revoked and replaced from the dashboard.

When creating a token, choose an expiration: 24 hours, 7 days, 31 days (the default), 90 days, or no expiration. Expired tokens cannot publish or use the JSON API. Reports already published stay available. Existing tokens without an expiration keep working until revoked.

Push a file

PUThttps://pipeout.sh/a/{filename}scope: push

The body is the file. With curl -T report.html the filename is appended for you, which is why the example above ends in a bare slash.

Filenames are identity

An artifact is identified by its filename on your account. Pushing report.html twice updates one artifact and one URL — it does not create a second. Each push also mints a permanent version URL frozen at those exact bytes.

Filenames are 1–200 characters of A-Za-z0-9._- with no slashes. Keep the real extension: it decides how the file is rendered.

Identical content keeps the same version

Pushes are compared by SHA-256 of the decompressed body. Re-pushing unchanged bytes returns the existing version and refreshes its retention clock without storing another version. Reviving an expired artifact must fit within your current storage quota.

Compression

Send Content-Encoding: gzip and pipeout decompresses before storing. File limits apply to the decompressed size: 10 MB on Free and 100 MB on Pro. Gzip reduces transferred bytes, but does not raise these limits:

gzip -c big.log | curl -T - -H 'Content-Encoding: gzip' \
  -u :$PIPEOUT_TOKEN https://pipeout.sh/a/big.log

Publish it private

Add a header and the URL asks for a sign-in instead of opening for anyone who has it. Only the account that owns the artifact can read it — there is no passcode to share and nothing to leak.

curl -T report.html -u :$PIPEOUT_TOKEN \
  -H 'X-Pipeout-Access: private' https://pipeout.sh/a/
ValueEffect
privateOnly the owning account can open the URL. Pro only.
unlistedBack to the default: anyone with the link can read it.
header omittedLeaves the policy exactly as it is

Omitting the header never changes anything, so a run that forgets it cannot reopen a private report. Access belongs to the artifact, not to a version: every past version URL is covered too, and re-pushing keeps the policy. You can also flip it either way per artifact in your dashboard.

Response

Plain text by default. Send Accept: application/json for:

{
  "slug": "report.html",
  "version": 3,
  "deduplicated": false,
  "size_bytes": 7183,
  "sha256": "9f2c...",
  "url": "https://pipeout.page/r/1f4c9a2b7e05d8613ac2f094",
  "version_url": "https://pipeout.page/r/8be31d07f2a45c9018ee6b23",
  "access": "unlisted"
}

An unlisted artifact URL can be read by anyone holding it. Do not push secrets, credentials, or other people's personal data — see the privacy policy.

Read an artifact

GEThttps://pipeout.page/r/{hash}no token

The hash is 24 hex characters, 96 bits, derived per account. There is no listing and no index; pages are served noindex with no-referrer.

What each content type does

TypeBehaviour
HTML, images, PDFServed byte-exact — the browser renders them natively
MarkdownRendered to a styled page
Text, logs, JSON, CSVNumbered viewer; above 1 MB it shows the head and says so
Anything elseA download page

Content type is taken from the request when you set it, otherwise inferred from the extension. A NUL byte in the first kilobyte marks a file binary no matter what the extension claims.

Interactive report compatibility

HTML bytes are served unchanged, including your scripts and styles. Reports run in an isolated browser context: service workers, shared browser storage (such as localStorage and IndexedDB), and embedded plugins are not supported. A report cannot read another artifact using same-origin access.

Inline scripts, forms, downloads, and links opening new tabs remain available. External APIs used by your scripts must allow cross-origin requests from an opaque origin (sent as Origin: null). Bundle data into your report when the API does not support this access pattern.

Parameters and methods

WhatEffect
?dl=1Forces a download instead of rendering
HEADContent type and length of the stored bytes, no body

Version URLs keep the same bytes, but browsers revalidate their responses so updated security headers can take effect. The artifact URL is never cached, so it always serves the newest version. Crawlers and unfurlers get metadata only — the blob is never opened for them, and those requests are not counted as views.

List and inspect

GEThttps://pipeout.sh/api/artifactsscope: read
{
  "artifacts": [
    { "slug": "report.html", "title": "Nightly audit",
      "url": "https://pipeout.page/r/1f4c9a2b7e05d8613ac2f094",
      "version": 3, "size_bytes": 7183, "views": 12, "expired": false }
  ]
}

expired marks an artifact whose URL has aged out of the free window. It stays in the listing — it is still yours, just not reachable.

GEThttps://pipeout.sh/api/artifacts/{filename}scope: read

The same object plus the versions your plan can reach:

{
  "slug": "report.html", "version": 3, "views": 12,
  "versions": [
    { "version": 3, "url": "https://pipeout.page/r/8be31d07f2a45c9018ee6b23",
      "size_bytes": 7183, "sha256": "9f2c..." }
  ]
}
POSThttps://pipeout.sh/api/reportno token

Report abuse. Body is {"hash": "...", "reason": "..."} where the hash is the 24-character id from an artifact URL. Limited to 5 per minute per IP.

Errors

Plain text as error (code): message, or JSON as {"error": "...", "message": "..."} when you send Accept: application/json.

StatusCodeMeaning
400bad_slugFilename outside the allowed character set or length
400bad_bodyBody unreadable — usually a gzip header that is not gzip
401invalid_tokenToken missing, unknown, expired, or revoked
403wrong_scopeToken lacks the scope this call needs
403account_suspendedAccount suspended
404not_foundNo artifact by that name
413file_too_largeOver the per-file cap for your plan
429rate_limitedToo many requests; honour Retry-After
507storage_quota_exceededAccount storage full — delete something or upgrade

Reading a hash that never existed or has been deleted gives 404. Content removed for abuse gives 410, and so does a link that has expired under free-tier retention — the expired page says so, and the artifact comes back on upgrade.

Limits

FreePro
Storage250 MB5 GB
Per file10 MB100 MB
Versions reachableNewest 5Every version
Links stay live30 days from last pushIndefinitely
Pushes30 / min per token120 / min per token

Caps are measured on the decompressed file. Cloudflare independently limits request bodies to 100 MB on any plan.

A second limit of 240 pushes per minute applies to the calling IP address rather than to the token, so that a rejected credential is metered too. It sits above both per-token limits and a normal client never meets it; both answer 429 with Retry-After.

Retention deletes nothing. On a free account an artifact stops resolving 30 days after its last push, and version URLs older than the newest five stop resolving immediately — but the rows and the blobs are kept, and upgrading to Pro brings every one of them back straight away. The clock runs from the last push, so a file your agent rewrites on a schedule never expires. An expired URL answers 410.

Storage is charged on what your plan can actually reach, so history that has aged out does not count against the quota and cannot fill your account.