Use this reference to implement an Energon client for discovery, authentication, site and loose-file operations, listing, and error recovery. Query the target instance’s /v1/help for deployment-specific identity and policy, then use the advertised /v1/openapi.json document because it is the canonical HTTP schema. Store returned url and api_url values instead of deriving them from route shapes.
HTTP API
Base URLs and authentication
PUBLIC_ORIGIN/v1/... API; token except help, health, and OpenAPI discovery
CONTENT_ORIGIN/{handle}/s/{slug}/... human-facing site content
CONTENT_ORIGIN/{handle}/f/{id}/{filename} human-facing loose-file contentThe hub origin (PUBLIC_ORIGIN) and content origin (CONTENT_ORIGIN) are different hostnames in production. API responses use:
urlfor the canonical public content URL onCONTENT_ORIGIN.api_urlfor an authenticated raw-byte API URL onPUBLIC_ORIGIN, when the operation has one.
Do not build the content URL by replacing or appending to the API host. Store the returned value.
Every /v1 route except GET /v1/help, GET or HEAD /v1/health, and GET or HEAD /v1/openapi.json requires:
Authorization: Bearer HUMAN_MINTED_INSTANCE_TOKENThe token must come from this instance’s /tokens page. Tokens cannot be recovered or renewed after expiry.
Request and storage flow
Sites have a slug plus many paths. Loose files have one generated id and one current filename. The API routes sites by slug and loose files by id; the public content URL additionally includes the publishing handle.
Authenticated site routes are not globally namespaced by handle. For a caller, Energon resolves the caller’s own site with that slug first; otherwise the slug must identify exactly one site on the instance. If two accounts use the same slug, a cross-account api_url can be ambiguous or resolve to the caller’s own site. For a site handoff across accounts, use the returned public path URL (plus X-Energon-Password when protected), or confirm that the slug is unique before relying on the authenticated site URL. Loose-file api_url values do not have this ambiguity because the generated id is instance-wide identity.
Discover the instance
Replace the placeholder host before running this example:
export ENERGON_ORIGIN="https://hub.your-company.example"
curl -fsS "$ENERGON_ORIGIN/v1/health"
curl -fsS "$ENERGON_ORIGIN/v1/help" | jq '{openapi, content_origin, env, token_prefix, retention, tokens, limits}'
curl -fsS "$ENERGON_ORIGIN/v1/openapi.json" | jq '{openapi, info, servers}'
curl -fsS "$ENERGON_ORIGIN/llms.txt"/v1/help describes this deployed instance: origins, token environment and prefix, retention presets, token policy, and limits. Its openapi field names the live OpenAPI document. /v1/openapi.json describes the HTTP contract: paths, request and response schemas, status codes, and error codes. Read both rather than treating either document as a substitute for the other.
| Method and path | Auth | Response |
|---|---|---|
GET /v1/health | No | { "ok": true } |
HEAD /v1/health | No | Header-only health check. |
GET /v1/help | No | Instance product/SOP, origins, routes, retention, token policy, identity, and limits. |
GET /v1/openapi.json | No | OpenAPI 3.1 contract with servers[0].url set to this instance’s PUBLIC_ORIGIN. |
HEAD /v1/openapi.json | No | Header-only availability check for the OpenAPI contract. |
GET /llms.txt | No | Plain-text agent-oriented description. |
GET /v1/whoami | Bearer | { email, label, expires_at }; expires_at is null for a never-expiring token. |
Common request headers
| Header | Routes | Meaning |
|---|---|---|
Authorization: Bearer … | Authenticated /v1 routes | Use the token prefix and environment name advertised by /v1/help. |
Content-Type: application/json | Site create/duplicate/PATCH; loose duplicate/PATCH | JSON object body. |
Content-Type: application/zip | Site import | ZIP bytes for an existing site. |
X-Filename | Loose-file POST/PUT | Required for raw create; optional on PUT to rename while replacing. |
X-Energon-TTL | Loose-file create/duplicate | Content lifetime; multipart field ttl is also accepted. |
X-Energon-Write-Policy | Loose-file create/duplicate | owner or instance; multipart field write_policy is also accepted. |
X-Energon-Set-Password | Loose-file create/duplicate/PUT | Set or clear the public share password; multipart field password is also accepted. |
X-Energon-Duplicate-From | Loose-file POST | Source file id for server-side copy. |
X-Energon-Password | Public content URL only | Read a share-password-protected public content URL. It is not used on authenticated API GETs. |
For JSON site requests and JSON loose-file duplicates, use the body fields ttl, write_policy, and password. Header-based loose-file duplication uses the X-Energon-* headers only when the request is not parsed as JSON. Valid write_policy inputs are exactly owner and instance.
Site routes
Route summary
| Method and path | Request | Success |
|---|---|---|
POST /v1/sites | JSON create, overwrite claim, or duplicate | 201 create/copy; 200 explicit claim |
GET /v1/sites | List query | { sites, total, next_cursor } |
GET /v1/sites/{slug} | None | Site metadata plus files |
PATCH /v1/sites/{slug} | JSON metadata patch | Updated site metadata |
DELETE /v1/sites/{slug} | None | { ok: true, deleted: slug } |
PUT /v1/sites/{slug}/files/{path} | Raw bytes | 201 new path; 200 replacement |
GET /v1/sites/{slug}/files/{path} | None | Raw bytes |
DELETE /v1/sites/{slug}/files/{path} | None | { ok: true, deleted: true, path } |
POST /v1/sites/{slug}/import | ZIP bytes | { slug, url, written } |
GET /v1/sites/{slug}/export | None | ZIP attachment |
The {path} segment may contain slashes. It must be a safe relative path: no leading slash, drive prefix, or .. segment.
Create, claim, or duplicate a site
Create body:
{
"slug": "onboarding-review",
"overwrite": false,
"password": "optional-share-password",
"ttl": "7d",
"write_policy": "owner"
}| Field | Required | Meaning |
|---|---|---|
slug | Yes | Lowercase letters, numbers, and internal hyphens; 1–63 characters. |
overwrite | No | Defaults false. If true, claims an existing site without deleting its paths. |
password | No | Public-link share password. Empty clears it when claiming. |
ttl | No | Instance-supported duration, positive seconds, or never when allowed. |
write_policy | No | owner or instance for a new site or copy; otherwise the instance default. An overwrite claim keeps the existing policy. |
duplicate_from | Copy only | Source site slug. Requires a new slug; do not combine with overwrite. |
New-site 201 response fields:
| Field | Meaning |
|---|---|
slug, handle | Stored site identity. |
url | Canonical content-origin site root. |
created | true. |
password_protected | Whether the public content URL is gated. |
password | The new phrase when this request set one; otherwise null. Use password_protected to determine the current gate state. Later reads omit this field. |
ttl, expires_at | Resolved lifetime and timestamp; expires_at can be null. |
write_policy | Resolved owner or instance. |
An overwrite claim returns the same shape with created: false and claimed: true. ttl appears only when the claim supplied one; otherwise expires_at remains unchanged. The claim preserves the existing write_policy; use a creator-authorized PATCH to change it. A collision without overwrite returns 409 site_exists with url, handle, last_written_by, updated_at, file_count, password_protected, expires_at, and hint in the error envelope.
A duplicate request uses the create fields plus duplicate_from. Its 201 response adds duplicated: true, duplicated_from, and file_count. The copy has caller ownership and fresh destination TTL/write policy; it does not inherit the source password unless the request supplies a new one.
Write, read, and delete site paths
export ENERGON_ORIGIN="https://hub.your-company.example"
test -n "$ENERGON_TOKEN" || { echo "Set a human-minted ENERGON_TOKEN" >&2; exit 1; }
curl -fsS "$ENERGON_ORIGIN/v1/sites/onboarding-review/files/assets/app.css" \
-X PUT \
-H "Authorization: Bearer $ENERGON_TOKEN" \
-H "Content-Type: text/css" \
--data-binary @dist/assets/app.cssSite-file PUT returns { url, api_url, path, size, content_type }. It does not create a missing site and does not extend expiry. GET returns raw bytes with the stored content type. DELETE removes only that path and returns { ok, deleted, path }.
Read site detail
GET /v1/sites/{slug} returns:
- Site fields:
slug,handle,url,created_at,updated_at,created_by,last_written_by,password_protected,expires_at,write_policy. files: ordered path objects withhandle,slug,path,size,content_type,updated_at,last_written_by,url, andapi_url.
Patch site metadata
{
"password": "",
"ttl": "30d",
"write_policy": "instance"
}Every field is optional, but write_policy must be present with owner or instance to change it. Empty password clears the public gate. Supplying ttl resets expiry from now and can revive an expired catalog row before purge wins. Response fields are slug, handle, url, password_protected, password, expires_at, write_policy, and optional ttl. password echoes a newly set phrase; it is null when the request cleared or did not change the password, so use password_protected for the current state.
Import and export
Import requires an existing writable site. It replaces archive paths but preserves site paths absent from the archive. One wrapping directory is stripped; common OS junk is skipped. The response’s written array lists the imported paths.
Export requires a non-empty site and returns application/zip with an attachment filename based on the slug. Import, export, and site copy accept at most 200 paths. Import and export are also bounded by the instance file/ZIP byte limit; a site copy is bounded by remaining platform capacity instead.
Public site behavior
The returned root url ends in /. A public root serves index.html, otherwise index.md, otherwise a generated file listing. A specific .md path is rendered for Accept: text/html; ?raw=1 returns source and ?download=1 returns an attachment. Other stored types are served inline unless download is requested.
Loose-file routes
Route summary
| Method and path | Request | Success |
|---|---|---|
POST /v1/files | Raw upload, multipart upload, or duplicate | 201 metadata |
GET /v1/files | List query | { files, total, next_cursor } |
GET /v1/files/{id} | Optional ?download=1 | Raw bytes |
PUT /v1/files/{id} | Raw or multipart bytes; optional rename | 200 replacement metadata |
PATCH /v1/files/{id} | JSON metadata patch | Updated file metadata |
DELETE /v1/files/{id} | None | { ok: true, deleted: id } |
The router also accepts an extra filename segment on authenticated GET|PUT|PATCH|DELETE /v1/files/{id}/{filename}, but identity and lookup use the id.
Create a loose file
Raw upload:
curl -fsS "$ENERGON_ORIGIN/v1/files" \
-H "Authorization: Bearer $ENERGON_TOKEN" \
-H "X-Filename: architecture.pdf" \
-H "X-Energon-TTL: 30d" \
-H "X-Energon-Write-Policy: owner" \
-H "Content-Type: application/pdf" \
--data-binary @architecture.pdfAccepted request forms:
| Form | Inputs |
|---|---|
| Raw bytes | X-Filename required; optional TTL, write-policy, and set-password headers. |
| Multipart | file required; optional password, ttl, and write_policy fields. |
| JSON duplicate | { duplicate_from, filename?, password?, ttl?, write_policy? }. |
| Header duplicate | X-Energon-Duplicate-From; optional X-Filename and policy headers; do not send Content-Type: application/json when using header policy. |
| Multipart duplicate | duplicate_from; optional filename, password, ttl, and write_policy; do not also include file. |
If Content-Type is application/json and X-Filename is present without a duplicate header, the JSON text is uploaded as literal file bytes.
Create 201 fields are url, api_url, id, handle, filename, size, content_type, password_protected, password, ttl, expires_at, and write_policy. password contains the newly set phrase or null when no phrase was set.
A duplicate response adds created_by, duplicated: true, and duplicated_from; it otherwise has the create fields. The copy receives a new id and does not inherit source password, TTL, ownership, or write policy.
Replace or rename a loose file
PUT replaces bytes under the existing id. X-Filename is optional: omit it to keep the filename or supply it to rename while replacing. Multipart PUT accepts a file field and uses that file’s name. X-Energon-Set-Password can set or clear the share password during the replacement.
PUT response fields are url, api_url, id, handle, filename, size, content_type, replaced: true, password_protected, and password. password echoes a newly set phrase; it is null when the request cleared or did not change the password. PUT does not return or change TTL/write policy. PATCH does not rename.
Read a loose file
GET /v1/files/{id} returns raw bytes. ?download=1 changes Content-Disposition to attachment. Response headers include Content-Type, Content-Disposition, X-Content-Type-Options: nosniff, X-Energon-Write-Policy, and Content-Length when available.
The public content URL in url includes the current filename. If a client uses the same id with a stale filename, Energon redirects to the current canonical url. Public Markdown follows the same HTML-versus-raw and download rules as site Markdown.
Patch loose-file metadata
PATCH accepts the same JSON fields as site PATCH: optional password, ttl, and write_policy. It returns id, handle, filename, url, api_url, password_protected, password, expires_at, write_policy, and optional ttl. password echoes a newly set phrase; it is null when the request cleared or did not change the password.
Fresh concurrent write claims produce 409 file_busy. Stale claims can be reclaimed. PUT and DELETE conditionally commit against the claim so competing operations do not silently split catalog metadata from stored bytes.
List filters and pagination
GET /v1/sites and GET /v1/files share these query parameters:
| Parameter | Values and default | Meaning |
|---|---|---|
scope | involved default, created, edited | Created by you or last written by you; created only; or other people’s objects you last edited. |
q | empty | Case-normalized substring of site slug or loose filename; %, _, and backslash are removed; at most 64 characters. |
created_by | empty | Lowercased exact creator-email filter, combined with scope. |
sort | updated default, name | Descending last-update order or ascending slug/filename order. |
limit | 25; clamped to 1–50 | Page size. Invalid or missing values use 25. |
cursor | empty | Opaque keyset cursor returned as next_cursor. Invalid cursors restart from the first page. |
sites_cursor | empty | Site-specific cursor accepted in place of cursor. |
files_cursor | empty | File-specific cursor accepted in place of cursor. |
Do not parse or edit a cursor. Repeat the same filters and pass it back as cursor.
Site list items contain slug, handle, url, created_at, updated_at, created_by, last_written_by, file_count, size, password_protected, expires_at, and write_policy.
Loose-file list items contain id, handle, filename, url, api_url, size, content_type, created_at, created_by, updated_at, last_written_by, password_protected, expires_at, and write_policy.
Lists are scoped to the caller’s involvement view. A valid token can still fetch a known object through its API route even when that object is absent from the list.
Response and error conventions
Successful JSON responses use Content-Type: application/json; charset=utf-8. Binary GET/export responses stream bytes with the stored or generated content type.
Expected /v1 failures use this envelope:
{
"error": "machine_readable_code",
"message": "Human-readable recovery guidance.",
"hub": "https://hub.your-company.example/account"
}Some failures add fields such as collision metadata, accepted presets, configured limits, an expiry timestamp, or a token page. Consumers should branch on HTTP status and error, show message, and preserve unknown extra fields.
| Status | Common codes | Client action |
|---|---|---|
400 | bad_json, bad_slug, bad_path, bad_filename, bad_password, bad_ttl, ttl_required, ttl_too_long, bad_write_policy, missing_filename, missing_file, bad_duplicate, invalid_zip, bad_zip_path, empty_zip, empty_site, too_many_files | Correct the request; use advertised presets and limits. |
401 | unauthorized, token_expired | Stop for a missing or expired token; a human must mint a replacement. |
403 | forbidden_write, forbidden_write_policy, forbidden_domain | Use an authorized creator account or follow instance identity policy. |
404 | not_found, site_not_found, file_not_found | Check identity/path. Create a site before PUT; loose-file PUT never creates. |
409 | site_exists, site_busy, file_busy, file_write_lost, file_delete_lost | Resolve a slug collision or retry a transient competing mutation. |
410 | expired | The content lifetime passed; do not retry the same read as if it were transient. |
413 | too_large, storage_cap | Shrink/split the upload or free instance capacity. Read /v1/help for current limits. |
415 | bad_content_type | Send a JSON object to JSON-only site/PATCH routes. |
500 | storage_rollback_failed, site_import_rollback_failed, site_copy_rollback_failed, site_delete_rollback_failed, file_delete_rollback_failed | Report the explicit recovery failure; retry only after storage recovers. |
503 | content_origin_not_configured | The operator must configure a distinct valid content origin. |
An export or server-side copy can also return 500 export_failed or 500 copy_failed when catalog metadata points to missing source bytes. Re-upload the missing source path or file before retrying.
The public content surface has its own JSON gate response when an agent sends X-Energon-Password or requests JSON: 401 { error: "password_required", message }. That response does not include the /v1 envelope’s hub field. Repeated failed guesses can instead return 429 { error: "rate_limited", message }; stop guessing and wait for the gate window.
Common mistakes
Never invent or reuse a token from stale chat history, never default to overwrite: true, and never infer url from PUBLIC_ORIGIN. PUT does not create a missing object or extend TTL. A loose-file rename is PUT with X-Filename; PATCH changes only password, TTL, or write policy. DELETE has no recycle bin.
Related guides
- Use Energon explains sites, loose files, and two-origin handoffs.
- Manage shared work turns these routes into everyday workflows.
- Sharing, access, and expiry explains what tokens, share passwords, TTL, and write policy do.
- Configure instance policy documents the settings surfaced by
/v1/help.