> ## Documentation Index
> Fetch the complete documentation index at: https://docs.factrove.app/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Describe what the running code does. Do not invent hosted pricing, launch dates, or security guarantees. Share and viewer links are a configured base URL plus an artifact path. Show localhost and a placeholder origin such as https://library.example.com.

# API overview

> How Factrove authenticates callers, shapes errors, and documents endpoints from the contract schemas.

Product routes live under `/api/v1`. Auth and API key management live under `/api/auth` (Better Auth). The pages in this tab are generated from `@factrove/contracts` plus the HTTP operation catalog. Re-run `pnpm docs:openapi` after those change.

Servers in the spec:

* `http://localhost:3001` for local development
* `https://api.example.com` as a placeholder self-hosted origin

## Authentication

| Caller | How |
| - | - |
| Browser session | Cookie from `POST /api/auth/sign-in/email` or sign-up |
| CLI and other clients | `Authorization: Bearer <api-key>` |

`x-api-key` does not authenticate. A present `Authorization` header that is not a valid key does not fall back to the session cookie. API key create, list, and delete require a browser session and reject `Authorization`.

Cookie-authenticated writes (`POST`, `PUT`, `DELETE`, and other non-GET/HEAD/OPTIONS methods) also need `Origin` or `Referer` to be `WEB_ORIGIN` or `PUBLIC_BASE_URL`. A missing or different origin is `403 FORBIDDEN` with message `This request origin is not allowed.` Bearer requests skip that check. The public unlock route ignores a leftover session cookie.

## Create an API key from a session

People can create a key in the web app at `/account/api-keys`. The secret is shown once. Agents can use the same session route. Do not send `Authorization` on these calls. Send `Origin` set to `WEB_ORIGIN` or `PUBLIC_BASE_URL`. On an `https` API origin the session cookie is named `__Secure-better-auth.session_token`. On `http` it is `better-auth.session_token`. A cookie jar records whichever name the server set.

Sign-up signs the new account in (`autoSignIn` is on) and stores the cookie. Passwords are 8 to 128 characters. Replace the email and password before you run this. A plus-address such as `name+tag@example.com` is valid. The stored address is lower case and keeps the plus tag. A successful sign-up sends one invite email to that address. With `EMAIL_TRANSPORT` unset or `log`, the API logs the message and does not send it. The live-check commands, with the invitee address as a parameter, are in [Invite email](https://docs.factrove.app/guides/self-hosting.md#invite-email). If the email is already registered, sign-up returns 422 and does not send another message. Use the sign-in request instead, with the same cookie jar and `Origin`.

```bash theme={null}
api=http://localhost:3001
origin=http://localhost:3000
jar=$(mktemp)

curl -sS -c "$jar" -X POST "$api/api/auth/sign-up/email" \
  -H "Content-Type: application/json" \
  -H "Origin: $origin" \
  -d '{"name":"Local Owner","email":"owner@example.com","password":"replace-this-password"}'

curl -sS -b "$jar" -X POST "$api/api/auth/api-key/create" \
  -H "Content-Type: application/json" \
  -H "Origin: $origin" \
  -d '{"name":"cli"}'
```

Sign in an existing account:

```bash theme={null}
curl -sS -c "$jar" -X POST "$api/api/auth/sign-in/email" \
  -H "Content-Type: application/json" \
  -H "Origin: $origin" \
  -d '{"email":"owner@example.com","password":"replace-this-password"}'
```

A successful create returns JSON that includes `key` once. A missing or disallowed `Origin` is `403 FORBIDDEN` before Better Auth handles the body. A request that also sends `Authorization` is 401 with message `API key management requires a browser session.` Point `factrove configure` at the printed key. The CLI does not print it back. See [configure](https://docs.factrove.app/cli-reference/configure.md).

## Errors

Product routes return:

```json theme={null}
{ "error": { "code": "ARTIFACT_NOT_FOUND", "message": "Artifact was not found." } }
```

Better Auth `/api/auth` routes use Better Auth's own JSON.

`Retry-After` is set only when the thrown API error includes `retryAfterSeconds`:

* every password-throttle `429 RATE_LIMITED`
* `503 UNLOCK_UNAVAILABLE` when password verification is busy

Public-view `429 RATE_LIMITED` and Markdown `503 CONVERSION_UNAVAILABLE` do not set `Retry-After`.

`PUT /api/v1/artifacts/{id}/content` replaces HTML for the owner. Validation matches create. A `409 CONTENT_CONFLICT` means another replace finished first and leaves the current name in place. A rare `500` after the object is written can still mean the new bytes are live; GET the artifact before retrying.

`PATCH /api/v1/artifacts/{id}` shares a private artifact with the organization the caller confirmed. The body is `{ "accessType": "org_shared", "organizationId": "<current organization id>" }`. Only the creator can call it. A non-owner gets the same `404 ARTIFACT_NOT_FOUND` as an unknown id. No credentials is `401 UNAUTHENTICATED`. No current organization is `400 ORGANIZATION_REQUIRED` and the artifact stays private. `organizationId` must be that current membership. The only explicit row lock is the artifact. The organization foreign-key check takes an implicit KEY SHARE lock on that organization. The update does not lock the user. It re-checks membership and, for a session, that the session is still present and unexpired, in the conditional UPDATE. That statement is the linearization point. A membership or session change that commits before the statement reads is `409 ORGANIZATION_CHANGED`, `400`, or `401 UNAUTHENTICATED` and does not write. A change that commits after that snapshot can land before commit and does not cancel the share. A deadlock, serialization failure, or lock timeout retries once and then returns that same 409 without writing. About 16 seconds is the budget for those two attempts, not a deadline for the endpoint. An artifact that is already `org_shared` returns 200 and keeps its organization, share token, and share password, for any confirmed `organizationId` and whether or not the caller has a current organization. A JSON array, and a JSON object with the wrong shape, is `400 INVALID_REQUEST` with message `Send { "accessType": "org_shared", "organizationId": "<current organization id>" }. This shares a private artifact with that organization.` Malformed JSON, null, and JSON primitives are `400 INVALID_REQUEST` with message `The request is invalid.` A body that is not `application/json` is `400 INVALID_REQUEST` with message `Send JSON with Content-Type application/json.` A body larger than 102,400 bytes is `413 FILE_TOO_LARGE` with message `The request is too large.` Cookie writes need an allowed `Origin` or `Referer`. `PATCH` is included in the credentialed CORS methods.

```bash theme={null}
api=http://localhost:3001
artifact_id=11111111-1111-4111-8111-111111111111

curl -sS -X PATCH "$api/api/v1/artifacts/$artifact_id" \
  -H "Authorization: Bearer $FACTROVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"accessType":"org_shared","organizationId":"local-org"}'
```

`name` is the library label. `POST /api/v1/artifacts` accepts an optional multipart field `name` (one line, 1 to 120 characters). When it is omitted, the API stores the file name without `.html` or `.htm` when that stem is one line of 1 to 120 characters. An empty stem, a stem longer than 120 characters, or a stem that is not one line stores null, and the upload still succeeds. `PUT /api/v1/artifacts/{id}/content` accepts the same field. The name is set in that same content revision update when the request sends one. Omitting it leaves the column unchanged. A `409` does not change the name. A content commit whose acknowledgement is lost can still land the new HTML and the new name together. An acknowledged or successfully reconciled update returns 200 even when a later replace has landed since. The 200 shows the artifact's current state, which may already include a later replace. An invalid name is `400 INVALID_REQUEST`. List, detail, upload, replace, and public detail responses include `name`, which is null when the artifact has none. `filename` stays `{id}.html`.

`PATCH /api/v1/artifacts/{id}/name` with `{ "name": "<library name>" }` changes that label and leaves the HTML object, content revision, access, organization, and share columns unchanged. It cannot clear a name. Only the creator can call it. Organization role is not consulted, so an organization admin who did not create the artifact cannot rename it. A missing id and any caller who is not the creator, including a member who can already read an `org_shared` artifact, get the same `404 ARTIFACT_NOT_FOUND` as an unknown id. No credentials is `401 UNAUTHENTICATED`. `403 FORBIDDEN` stays the cookie-origin failure and is not used for this authorization check.

`DELETE /api/v1/artifacts/{id}` removes the artifact row and the current HTML object. Success is `204` with no body. The same owner rule applies: an organization admin who is not the creator gets `404 ARTIFACT_NOT_FOUND`. The object for the revision that was read is removed first. The row is deleted only while that creator, revision, and object key are still current. A replace that commits in between keeps its row, and the delete reads that row and tries again. A missing object still allows that row delete. After eight attempts that each lose to a newer replace, the response is `409 CONTENT_CONFLICT`. Retry DELETE. `503 STORAGE_UNAVAILABLE` means storage failed before the row delete. The row is kept. The object may already be gone, so retry DELETE. A public link stops working because the share token lived on the row. Older objects left behind by a failed content replace are not swept.

```bash theme={null}
api=http://localhost:3001
artifact_id=11111111-1111-4111-8111-111111111111

curl -sS -X PATCH "$api/api/v1/artifacts/$artifact_id/name" \
  -H "Authorization: Bearer $FACTROVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Launch notes"}'

curl -sS -X DELETE "$api/api/v1/artifacts/$artifact_id" \
  -H "Authorization: Bearer $FACTROVE_API_KEY"
```

## Public shares

Anonymous detail, unlock, and HTML are under `/api/v1/public/artifacts/{id}/{token}`. Send an unlock grant in `X-Factrove-Share-Grant`. See [Sharing links](https://docs.factrove.app/guides/sharing-links.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.