Overview

No authentication. No account. POST your log content, receive a permanent shareable URL. The viewer renders your log with automatic or specified syntax highlighting, line numbers, and a raw content endpoint.

Base URL: https://report.openutils.net

Three content types are accepted for uploads: application/json (body object), text/plain (raw body = content, metadata via query params), and multipart/form-data (form fields or direct file upload — no escaping required).

Create a report

POST /api/report

Creates a new log report. Returns an ID and a shareable viewer URL.

Request fields

FieldTypeRequiredDescription
contentstring required* The log content to store. Maximum 20 MB. In multipart/form-data mode, use -F "content=<file.txt" to read from a file without any escaping.
filefile required* multipart/form-data only. Upload a file directly with -F "file=@path/to/file.log". The filename is used as the default title and the extension is auto-extracted. Mutually exclusive with content.
languagestring optional Language identifier for syntax highlighting — e.g. python, javascript, bash. Falls back to auto-detection if omitted.
extensionstring optional File extension without dot — e.g. log, txt. Used for the download filename. Overrides the auto-extracted extension when using file upload.
titlestring optional Human-readable label displayed in the viewer header. Overrides the filename when using file upload.
* Either content or file is required. Provide one, not both.

Responses

201Report created.
{
  "id":  "7e3MhXGsYT9nxsbd6u8Xb6",
  "url": "https://report.openutils.net/log/7e3MhXGsYT9nxsbd6u8Xb6"
}
400Missing or empty content field.
413Content exceeds 20 MB.
429Rate limit exceeded. See Rate Limiting.
{ "error": "content is required" }

Code examples

# Upload a file directly — no escaping, extension and title auto-detected
curl -sF "[email protected]" \
     -F "title=Payment worker crash" \
     https://report.openutils.net/api/report

# Form field — inline content without JSON escaping
curl -sF 'content=Error: connection refused at 127.0.0.1:5432' \
     -F "language=javascript" \
     -F "title=DB failure" \
     https://report.openutils.net/api/report

# JSON body
curl -sX POST https://report.openutils.net/api/report \
  -H "Content-Type: application/json" \
  -d '{"content":"Error: connection refused","language":"javascript","title":"DB failure"}'

# Plain text — pipe stdin directly
curl -sX POST "https://report.openutils.net/api/report?language=bash&title=Build+failure" \
  -H "Content-Type: text/plain" \
  --data-binary @build.log
import requests

# File upload — no escaping, extension auto-detected from filename
with open("worker.log", "rb") as f:
    r = requests.post(
        "https://report.openutils.net/api/report",
        files={"file": f},
        data={"title": "Payment worker crash"}
    )
print(r.json()["url"])

# JSON body
r = requests.post(
    "https://report.openutils.net/api/report",
    json={
        "content":  open("app.log").read(),
        "language": "python",
        "title":    "Unhandled exception"
    }
)
print(r.json()["url"])
// File upload (browser File object or Node.js Blob)
const form = new FormData();
form.append("file", fileObject);          // File / Blob
form.append("title", "Payment worker crash");
const res = await fetch("https://report.openutils.net/api/report", {
  method: "POST", body: form
});
const { url } = await res.json();

// JSON body
const res2 = await fetch("https://report.openutils.net/api/report", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ content: error.stack, language: "javascript", title: "Unhandled rejection" })
});
const { url: url2 } = await res2.json();
// File upload — no escaping needed
$ch = curl_init("https://report.openutils.net/api/report");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => [
        "file"  => new CURLFile("/var/log/app/worker.log"),
        "title" => "Payment worker crash",
    ],
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data["url"];

// JSON body
$ch = curl_init("https://report.openutils.net/api/report");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
    CURLOPT_POSTFIELDS     => json_encode([
        "content"  => $logContent,
        "language" => "php",
        "title"    => "Exception report",
    ]),
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data["url"];

Get a report

GET /api/report/:id

Returns the full report as JSON. Calling this endpoint also resets the 6-month inactivity timer for reports under 2 MB.

Response fields

FieldTypeDescription
idstring22-character unique identifier.
contentstringThe full log content.
languagestring | nullLanguage hint provided at creation.
extensionstring | nullFile extension provided at creation.
titlestring | nullHuman-readable title.
sizeintegerContent size in bytes.
created_atstringCreation timestamp (UTC).
last_accessed_atstringTimestamp of the last view or fetch.

Example response

{
  "id":               "7e3MhXGsYT9nxsbd6u8Xb6",
  "content":          "Error: Connection refused at 127.0.0.1:5432\n  at db.js:42",
  "language":         "javascript",
  "extension":        "log",
  "title":            "DB connection failure",
  "size":             58,
  "created_at":       "2026-05-03 16:50:30",
  "last_accessed_at": "2026-05-03 18:22:11"
}

Get raw content

GET /api/report/:id/raw

Returns the log content as text/plain with Content-Disposition: inline. The suggested filename in the header is derived from the title and extension set at creation.

Use this endpoint to pipe output into other commands or to view unrendered content directly in a browser tab.

curl -s https://report.openutils.net/api/report/7e3MhXGsYT9nxsbd6u8Xb6/raw \
  | grep "ERROR"

Rate Limiting

Upload requests (POST) are limited to 6 per minute per IP address. The counter resets every 60 seconds. Read requests (GET) are not rate-limited.

When the limit is exceeded the API responds with 429 Too Many Requests and a Retry-After: 60 header.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60

{ "error": "Rate limit exceeded: 6 uploads per minute allowed" }

Storage Rules

Reports are stored subject to the following expiry rules. Cleanup runs daily at 03:00 UTC. Once deleted, a URL returns 404 permanently.

ConditionBehaviour
≤ 2 MB Deleted after 6 consecutive months without any view. Every visit to the viewer or API resets the timer.
2 MB – 20 MB Hard expiry 30 days after upload, regardless of access frequency.