/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:3001for local developmenthttps://api.example.comas a placeholder self-hosted origin
Authentication
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. 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.
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.
Errors
Product routes return:/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_UNAVAILABLEwhen password verification is busy
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.
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.
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.