Recoverable creation protocol: protocol 1 clients send X-Linkifact-Publish-Recovery: 1 with a pre-saved, CSPRNG-backed lfpr_ idempotency key on account publication. An unsupported publication-recovery version returns 400 unsupported_publish_recovery_protocol. The first-party browser also sends X-Linkifact-Expected-Account-SHA256 on irreversible authenticated operations. It is the lowercase SHA-256 of the opaque account ID that prepared the operation; the API verifies it before reading content or application state. API-key clients authenticate with their bearer key and omit this browser-only intent header.
Requests and identity
Publishing requires an account. Agent and backend clients use a scoped Linkifact API key. Browser sessions use secure host-only cookies on the API origin, and the app makes credentialed requests to that origin. Viewer and bundle links remain unlisted bearer URLs: readers do not sign in, but anyone who receives a URL can open it.
multipart/form-data
Scoped API key
Artifact, collection, and session routes
/v1/artifactsPublish authenticated JSON or multipart HTML
/v1/artifactsList the current account's artifacts
/v1/artifacts/{id}Read metadata as the owner
/v1/artifacts/{id}Delete as the owner
/v1/artifacts/{id}/versionsPublish an authenticated version with If-Match
/v1/collectionsList the current account's collections
/v1/collectionsCreate a collection
/v1/collections/{id}Read an owned collection and its ordered items
/v1/collections/{id}Rename an owned collection
/v1/collections/{id}Delete a collection, not its artifacts
/v1/collections/{id}/itemsAdd or reposition an owned artifact
/v1/collections/{id}/items/{artifactId}Remove an item, not its artifact
/v1/sessionsPublish one reviewed, redacted session bundle
/v1/sessionsList the current account's shared sessions
/v1/sessions/{id}Read metadata as the owner
/v1/sessions/{id}Delete as the owner
/v1/session-abuse-reportsReport a shared session without downloading it
/v1/admin/session-abuse-reportsPage moderation reports by exact status
/v1/admin/session-abuse-reports/{id}Advance or close a moderation report
/v1/admin/sessions/{id}/quarantineRemove an active session from public view
/v1/admin/sessions/{id}/restoreRestore an unexpired quarantined session
/v1/admin/sessions/{id}/deletePermanently schedule a moderated session deletion
/v1/api-keysList API key metadata for the current browser session
/v1/api-keysCreate a scoped API key; the token is returned once
/v1/api-keys/{id}Revoke an API key from the current browser session
/v1/abuse-reportsReport an artifact without loading it on the trusted origin
POST /v1/artifacts
Send authenticated JSON with html and optional title, or multipart with a file part and optional title. Generate a 32-byte random value locally, encode it as 52 lowercase, unpadded RFC 4648 base32 characters, and prefix it with lfpr_. Durably save it before upload, send it as Idempotency-Key with X-Linkifact-Publish-Recovery: 1, and retry an uncertain response only with the exact same HTML, normalized title, and key. Use a new key after any change.
Artifacts use a script-free HTML/CSS profile. Inline CSS, same-document fragment links, native controls, embedded data, and compact data: or blob: media are accepted. Scripts, event handlers, forms, password inputs, frames, objects, popups, downloads, workers, and known active URL carriers are rejected. A deny-by-default CSP blocks any remaining network load, and the opaque sandbox prevents navigation of the parent page.
POST /v1/artifacts HTTP/1.1
Host: api.linkifact.com
Authorization: Bearer lfk_<account API key>
Content-Type: application/json
Idempotency-Key: lfpr_<52 lowercase base32 characters>
X-Linkifact-Publish-Recovery: 1
{
"title": "Quarterly model",
"html": "<!doctype html>..."
}Response fields
idInternal artifact identifier used by owner-management endpoints.
urlPublic, unlisted linkifact.com/artifacts/<id> wrapper URL. Anyone with this bearer link can view.
expires_atExact instant the public URL stops serving the artifact.
POST /v1/sessions
Send one Linkifact Session Bundle v1 from an authenticated account after local redaction review. privacy_confirmed must be exactly true; unknown fields and invalid bundle shapes are rejected. The reviewed title, when present, belongs inside the bundle; an outer title override is rejected. Generate and durably save one CSPRNG-backed lfpr_ key before publication, send it as Idempotency-Key together with X-Linkifact-Publish-Recovery: 1, and reuse it only for uncertain retries of that exact reviewed bundle. Choose a new key after any change and re-review. Raw JSON ingress has a plan-specific streamed byte limit in addition to the canonical bundle limit; the historical 16 MiB portable-request ceiling remains an absolute bound, so heavily escaped Pro JSON can require a more compact wire representation. Before storing bytes, the server verifies publisher ledger claims against actual fixed markers, normalizes their counts, ignores caller-asserted server provenance, and applies a separately tagged mandatory server redaction pass. The public metadata count reports only fresh server detections. The public manifest commits last.
POST /v1/sessions HTTP/1.1
Host: api.linkifact.com
Authorization: Bearer lfk_<account API key>
Content-Type: application/json; charset=utf-8
Idempotency-Key: lfpr_<52 lowercase base32 characters>
X-Linkifact-Publish-Recovery: 1
{
"privacy_confirmed": true,
"bundle": {
"$schema": "https://linkifact.com/schemas/session-bundle/v1",
"kind": "linkifact.session-bundle",
"version": 1,
"exported_at": "2026-09-04T12:00:00.000Z",
"source": { "adapter": "generic-json", "product": "Generic JSON", "format": "json" },
"conversation": { "turns": [{ "role": "user", "kind": "message", "content": "Selected context" }] },
"redactions": []
}
}Success returns id, the unlisted branded url, a cookie-free bundle_url, and exact expires_at. Share url as the human viewer and pass bundle_url to linkifact session import; the viewer page is not an import source. The bundle download uses application/vnd.linkifact.session+json as an attachment. Session source-adapter metadata is publisher-claimed, not authenticated provider provenance. Every session, bundle, management, error, and tombstone response is noindex. Unlisted is not private: anyone with a session or bundle URL can read it.
The API does not locate native transcripts or import into harness state. Use the session-exchange package or CLI to create a bundle from an explicitly selected source and render an imported bundle as untrusted continuation context.
Optimistic, immutable versions
Authenticated artifacts retain a stable public URL. Send the current version in If-Match when publishing a new version. The first-party browser saves a fresh lfpr_ idempotency key, the base-version ID, and an exact request digest before upload. If a response is uncertain, reselect the exact same HTML and title: the app reuses the saved key and original base even if the stable URL already advanced. A genuinely stale new attempt returns 409 Conflict; fetch fresh metadata before starting it.
Use the narrowest credential
Browser account routes use a secure session cookie. Agent clients send Authorization: Bearer lfk_…. Reading artifact metadata requires artifacts:read; publishing versions and deleting require artifacts:write; collection mutations require collections:write; shared-session metadata and publication use sessions:read and sessions:write. Moderation routes require the privileged admin:artifacts scope. API-key create, list, and revoke routes accept browser sessions only, so an API key cannot mint another key.
Create requests use { "name": "Codex on laptop", "scopes": ["artifacts:read", "artifacts:write", "sessions:read", "sessions:write"] }. Save the returned token immediately; only its hash is retained.
Assemble an unlisted collection
Create with { "name": "Release demos" }. Add or reposition an active owned artifact with { "artifact_id": "…", "position": 0 }; omit position to append. Collection responses use the branded https://linkifact.com/collections/<id> URL; its fixed wrapper loads the safely escaped renderer at https://pages.linkifact.com/c/<id>. It is unlisted and noindex, but anyone with its URL can view it.
Report without rendering
For an artifact, send artifact_id to /v1/abuse-reports. For a shared session, send session_id to /v1/session-abuse-reports. Both accept a reason of malware, phishing, harassment, copyright, privacy, or other, plus optional bounded details and contact. Browser reports also send the configured Turnstile token. A successful submission returns 201 with an opaque report ID and open status.
Triage, quarantine, restore, or delete
Moderators with admin:artifacts can list shared-session reports by an exact status of open, reviewing, resolved, or dismissed. limit is 1–100 and before is the opaque cursor returned as next_before. Update a report with { "status": "reviewing" }.
Quarantine, restore, and delete mutations require { "reason": "bounded moderator note" }. Restore accepts only an unexpired quarantined session (or an idempotent replay of a completed restore); it never extends expires_at. A manifest-commit failure fails closed back to quarantine. All moderation responses remain noindex and no-store, and report records should never be copied into public session URLs or crawlable pages.
Stable codes, useful request IDs
Non-2xx responses use one envelope. Branch on code, show message, and include request_id when reporting a problem.
{
"error": {
"code": "html_validation_failed",
"message": "The HTML did not pass publication checks.",
"request_id": "req_01J...",
"details": {
"issues": [{ "code": "external_url", "message": "..." }]
}
}
}Documented error families cover authentication, validation, quota, expiry, conflict, plan limits, and quarantine.
Retry deliberately
Do not immediately retry 429. Honor Retry-After when it is present; otherwise inspect the structured quota window and wait for that UTC window to reset. Retry transient 5xx failures with exponential backoff and the same idempotency key. Do not retry policy violations or size errors unchanged.