Help
Help
AI Integration
The AI integration runs an AI feature and returns content you can preview and then confirm, under /api/ai/*. Every feature follows the same two-step flow: POST /api/ai/suggest produces a validated preview artifact without writing anything, and POST /api/ai/generate persists a confirmed artifact and returns the created resource id(s). GET /api/ai/status reports which providers you have configured and your remaining daily quota.
All AI endpoints accept either a session cookie or a Bearer sync token (Authorization: Bearer <token>), so the web app, native/mobile clients, and CLI tools all use the same routes. AI features are subscriber only: a free account receives 403 subscription_required from /suggest and /generate.
You bring your own provider key. InterlinedList calls Anthropic, OpenAI, or Google Gemini with the key you store on the Integrations page (see Settings (Help Center)). Stored keys are decrypted server-side to call the provider and are never returned by any endpoint.
The suggest → generate flow
The two-step design lets a client show a preview and get explicit user confirmation before anything is written:
- Suggest.
POST /api/ai/suggestvalidates your input, resolves a provider/model, checks quota, calls the model, and validates the model's output into a typed artifact. It returns the artifact plus tokenusageandquota. Nothing is saved. - Confirm and generate. The client shows the artifact to the user. On confirmation, send that same artifact back to
POST /api/ai/generate. The server re-validates it (defense in depth) and persists it, returning the created resource id(s).
Both steps count against your daily quota and are recorded in a per-user audit ledger that stores token counts and status only — never prompt or output text.
Endpoint table
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/ai/suggest | Session or Bearer | Run a feature and return a validated preview artifact. No write. Subscriber only. |
| POST | /api/ai/generate | Session or Bearer | Persist a confirmed artifact from /suggest. Subscriber only. |
| GET | /api/ai/status | Session or Bearer | Configured providers, your default provider/model, and remaining daily quota. Never returns keys. |
Endpoint reference
POST /api/ai/suggest
Auth required: yes (Session or Bearer) Subscriber only: yes Description: Run an AI feature and return a validated preview artifact. Writes nothing.
Request body
{
"feature": "powered_template | powered_document | message_series | article_series | writing_assist",
"input": "the user's free-text instruction or draft",
"context": { "…feature-specific fields…": "see Context below" },
"provider": "anthropic | openai | gemini (optional override)",
"model": "provider model id (optional override)",
"maxOutputTokens": 2048
}
| Field | Type | Required | Description |
|---|---|---|---|
feature | string | yes | One of the five features. Any other value returns 422 invalid_input. |
input | string | yes | Free-text instruction (or draft, for writing_assist). Word-capped per feature; see Input and output limits. |
context | object | no | Feature-specific hints and source references. See Context object. |
provider | string | no | Force a provider. Must be one you have a stored key for, else 409 no_provider_configured. |
model | string | no | Override the model id for the resolved provider. |
maxOutputTokens | number | no | Requested output-token budget. Clamped down to the feature's ceiling; a larger value is ignored, never honored. |
Response 200 OK
{
"ok": true,
"feature": "writing_assist",
"artifact": { "kind": "message", "content": "Rewritten draft…" },
"usage": { "inputTokens": 412, "outputTokens": 96, "model": "claude-sonnet-5" },
"quota": { "usedToday": 7, "dailyLimit": 50 }
}
The artifact shape depends on the feature; see Artifacts by feature.
Error responses
| Status | Code | Condition |
|---|---|---|
| 401 | unauthorized | Not authenticated. |
| 403 | subscription_required | Authenticated but not a subscriber. |
| 409 | no_provider_configured | No stored provider key, or the requested provider has no key. |
| 422 | invalid_input | Missing/unknown feature, empty input, input over the word cap, or a series under the 10-word gate. |
| 422 | invalid_ai_output | The model returned output that failed validation (not valid JSON, missing required fields, or below the minimum item count). |
| 429 | quota_exceeded | The daily quota is reached. |
| 429 | rate_limited | Per-user short-window rate limit tripped (separate from the daily quota). Honor the Retry-After header. |
| 500 / 502 | provider_error | Upstream provider failure/timeout (502, 60s per-call), or an unexpected server error (500). Both carry code: "provider_error". |
POST /api/ai/generate
Auth required: yes (Session or Bearer)
Subscriber only: yes
Description: Persist a confirmed artifact returned by /api/ai/suggest.
Request body
{
"feature": "powered_template | powered_document | message_series | article_series | writing_assist",
"artifact": { "kind": "…", "…": "the artifact from /suggest, edited if desired" },
"provider": "anthropic | openai | gemini (optional, recorded in the audit ledger)",
"model": "provider model id (optional, recorded in the audit ledger)"
}
| Field | Type | Required | Description |
|---|---|---|---|
feature | string | yes | The feature the artifact was produced for. |
artifact | object | yes | A confirmed artifact envelope. Re-validated server-side before it is written. |
provider | string | no | Recorded for auditing only; does not affect the write. |
model | string | no | Recorded for auditing only; does not affect the write. |
Response 201 Created
{
"ok": true,
"feature": "powered_document",
"created": { "documentId": "clx…" },
"quota": { "usedToday": 8, "dailyLimit": 50 }
}
The created object is one of three shapes, depending on what the artifact persists:
created shape | Produced by |
|---|---|
{ "listId": "…" } | list and message_series artifacts (both create a list). |
{ "documentId": "…" } | A single document artifact. |
{ "folderId": "…", "documentIds": ["…", "…"] } | A doc_series artifact (creates a folder of documents). |
Writes are always scoped to the authenticated user; nothing in the artifact controls ownership. A tags, thread, or message artifact (from writing_assist) is not persistable and returns 422 invalid_input — those are meant to be inserted into the composer client-side, not written as standalone content.
Error responses
| Status | Code | Condition |
|---|---|---|
| 401 | unauthorized | Not authenticated. |
| 403 | subscription_required | Authenticated but not a subscriber. |
| 409 | no_provider_configured | You have no stored provider key. /generate checks this too, so a keyless subscriber posting a hand-edited artifact gets a 409. |
| 422 | invalid_input | Missing feature/artifact, or an artifact type that cannot be persisted. |
| 422 | invalid_ai_output | The confirmed artifact fails re-validation. |
| 429 | quota_exceeded | The daily quota is reached. |
| 429 | rate_limited | Per-user short-window rate limit tripped. Honor the Retry-After header. |
| 500 / 502 | provider_error | An unexpected server error (500) or an upstream provider failure/timeout (502). Both carry code: "provider_error". |
GET /api/ai/status
Auth required: yes (Session or Bearer) Subscriber only: no (any authenticated user can read their status) Description: Report configured providers, the user's default provider/model, the per-provider fallback models, and remaining daily quota. Never returns keys.
Response 200 OK
{
"subscriber": true,
"providers": ["anthropic", "openai"],
"defaultProvider": "anthropic",
"defaultModel": null,
"defaultModels": {
"anthropic": "claude-sonnet-5",
"openai": "gpt-4.1-mini",
"gemini": "gemini-2.0-flash"
},
"quota": { "usedToday": 8, "dailyLimit": 50, "remaining": 42 }
}
| Field | Type | Description |
|---|---|---|
subscriber | boolean | Whether the account may use /suggest and /generate. |
providers | string[] | Providers you have a stored key for, in fixed priority order (anthropic, openai, gemini). Empty means AI features will return 409 no_provider_configured. |
defaultProvider | string | null | Your chosen default provider, or null if unset. |
defaultModel | string | null | Your chosen default model, or null to use the provider's fallback. |
defaultModels | object | The fallback model per provider when you have not set defaultModel. |
quota | object | usedToday, dailyLimit, and remaining for the rolling 24-hour window. |
Error responses
| Status | Code | Condition |
|---|---|---|
| 401 | unauthorized | Not authenticated. |
Features
feature selects which AI capability runs and, in turn, the artifact shape you get back:
| Feature | What it does | Suggest artifact kind | Generate creates |
|---|---|---|---|
powered_template | Generate a personalized list (schema + starter rows) from a description. | list | A list ({ listId }). |
powered_document | Draft a single markdown document (four modes; see below). | document | A document ({ documentId }). |
message_series | Plan a threaded series of short messages. | message_series | A list of scheduled rows ({ listId }). |
article_series | Plan and write a coherent series of documents. | doc_series | A folder of documents ({ folderId, documentIds }). |
writing_assist | Rewrite, tighten, expand, fix grammar, thread, or tag a composer draft. | message, thread, or tags | Not persistable (insert client-side). |
Powered Document modes
powered_document supports four modes, set in context.mode. Derived modes resolve their source server-side under the owning user (IDOR-guarded), and the Research URL fetch goes through the SSRF-guarded fetcher. You pass a reference, not the source text:
context.mode | Also requires | Source resolution |
|---|---|---|
article (default) | — | No source; drafts from your input topic alone. |
from_list | context.listId | Loads your list's schema and up to 50 rows, server-side. |
from_article | context.documentId | Loads your document's title and body, server-side. |
research_url | context.url | Fetches the page (http/https only, SSRF-guarded), extracts title/description and page text, and cites the URL. |
If the required reference is missing or not yours, the mode returns 422 invalid_input. Server-fetched context is truncated to a fixed size before it reaches the model, so very large lists or pages are summarized from a capped excerpt.
Context object
context is optional and id-based. Fields are feature-specific; unknown fields are ignored. Numeric hints are clamped server-side.
| Field | Type | Used by | Description |
|---|---|---|---|
templateKey | string | powered_template | Which base template to personalize. |
mode | string | powered_document | article, from_list, from_article, or research_url. |
listId | string | powered_document (from_list) | Source list; must be yours. |
documentId | string | powered_document (from_article) | Source document; must be yours. |
url | string | powered_document (research_url) | Source URL (http/https). |
action | string | writing_assist | rewrite (default), tighten, expand, grammar, thread, or tags. |
targetPlatform | string | writing_assist | Platform hint for threading/rewrites (for example a character-limit target). |
count | number | message_series, article_series | Requested item count; clamped to the series bounds below. |
spacingMinutes | number | message_series | Reserved / currently ignored. Accepted for forward compatibility but not applied — generated series are spaced a fixed 4 minutes apart (see below). |
Artifacts by feature
An artifact is the single typed envelope for "content an AI produced". /suggest returns one; /generate accepts one back. Every artifact has a kind.
list (from powered_template):
{
"kind": "list",
"title": "Conference Talks",
"description": "Talks I want to submit this year.",
"dsl": { "name": "Conference Talks", "fields": [ { "…": "DSL schema object" } ] },
"rows": [ { "field_key": "value" } ]
}
The dsl is a full List Schema DSL object (see List Schema DSL), validated on both suggest and generate. The model is constrained to at most 20 fields, each with a key matching ^[a-z][a-z0-9_-]*$; schemas beyond that fail validation. Starter rows that fail schema validation are dropped rather than failing the whole artifact.
document (from powered_document):
{
"kind": "document",
"title": "Getting Started with Widgets",
"markdown": "# Getting Started…",
"outline": ["Overview", "Setup", "Next steps"],
"isPublic": false
}
message_series (from message_series):
{
"kind": "message_series",
"listTitle": "Launch thread",
"items": [
{ "order": 1, "content": "1/ Today we shipped…", "crossPostTargets": ["mastodon"] },
{ "order": 2, "content": "2/ Here's why it matters…" }
]
}
On generate, this becomes a list whose rows are scheduled posts. The server assigns future timestamps spaced a fixed 4 minutes apart (context.spacingMinutes and any model-supplied timestamps are ignored) and keeps only cross-post targets it recognizes.
doc_series (from article_series):
{
"kind": "doc_series",
"folderTitle": "Kubernetes from Scratch",
"documents": [
{ "order": 1, "title": "Introduction", "outline": ["Why", "What"], "markdown": "# Introduction…" },
{ "order": 2, "title": "Core Concepts", "markdown": "# Core Concepts…" }
]
}
On generate, this creates a folder (name de-duplicated) and one document per entry, in order.
writing_assist artifacts — one of three, depending on context.action:
{ "kind": "message", "content": "Rewritten or tightened draft" }
{ "kind": "thread", "parts": ["1/ …", "2/ …"] }
{ "kind": "tags", "tags": ["kubernetes", "devops"] }
These are meant to be inserted into the composer by the client. They are not accepted by /generate.
Provider resolution
For each /suggest call the provider and model are resolved in this precedence:
- Per-request override — the
providerin the request body (must have a stored key). - Your default —
defaultProviderset on the Integrations page. - First configured — in fixed priority order:
anthropic, thenopenai, thengemini.
The model is resolved as: the request model, else your defaultModel (only when the resolved provider is your default provider), else the provider's fallback model from defaultModels (see GET /api/ai/status). If the resolved provider has no stored key, the call returns 409 no_provider_configured. Keys are decrypted only to call the provider and are never included in any response.
Input and output limits
Every ceiling below is enforced server-side. maxOutputTokens may request smaller than the feature's ceiling but is clamped down, never up.
| Feature | Max input (words) | Max output tokens |
|---|---|---|
writing_assist | 1500 | 1024 |
powered_template | 300 | 4096 |
message_series | 500 | 4096 |
powered_document | 500 | 8000 |
article_series | 500 | 8000 |
Other bounds:
- Composer 10-word gate.
message_seriesandarticle_seriesrequire at least 10 words ofinput; less returns422 invalid_input. This matches the composer, where the series generator buttons stay inactive until the draft has enough content. - Series counts.
message_seriesproduces 3–12 items (default 5);article_seriesproduces 2–6 documents (default 4).context.countis clamped into range. - Content caps. A generated document is capped at 40,000 characters; a single message at 3,000 characters. Server-fetched context (a list dump or a page's text) is truncated to 12,000 characters.
- Daily quota. 50 AI generations per rolling 24 hours per user, enforced even with your own provider keys. At the cap, calls return
429 quota_exceeded. Both phases count: a/suggestand its follow-up/generateare two generations. Failed attempts also count — a call that ends ininvalid_ai_output,refused, orprovider_errorstill consumes one generation, so build in retry restraint. - Rate limit. In addition to the daily quota,
/suggestand/generateare rate limited to 15 requests per 60 seconds per user; tripping it returns429 rate_limited(distinct fromquota_exceeded) with aRetry-Afterheader giving the seconds to wait. The limiter is a best-effort, per-server-instance fixed window, so under horizontal scaling the effective ceiling can be somewhat higher — treat 15/60s as the guaranteed floor, not a hard cap.
Error responses
All AI errors share the shape { "error": "human message", "code": "machine_code" }, so clients can switch on code without parsing prose.
| HTTP | code | Meaning |
|---|---|---|
| 401 | unauthorized | Not authenticated (no valid session or Bearer token). |
| 403 | subscription_required | Authenticated but not a subscriber. |
| 409 | no_provider_configured | No stored provider key, or the requested provider has no key. |
| 422 | invalid_input | Bad request: unknown/missing feature, empty or over-length input, series under the 10-word gate, a missing source reference for a derived Powered Document mode, or a non-persistable artifact on generate. |
| 422 | invalid_ai_output | The model's output failed validation (not JSON, missing fields, or below the minimum item count). |
| 429 | quota_exceeded | The 50-per-day quota is reached (failed attempts count too). |
| 429 | rate_limited | The 15-per-60s rate limit is tripped. Response includes a Retry-After header. |
| 502 | provider_error | The upstream provider errored or timed out (60-second per-call timeout). |
| 500 | provider_error | An unexpected server error. Same code as the 502 case, so switch on both status and code if you need to tell them apart. |
A 422 refused code also exists for the case where a provider declines to answer; treat it like invalid_ai_output (retry with different input). Note the two provider_error rows above share one code across HTTP 500 and 502.
Examples
Preview a rewrite of a composer draft:
POST /api/ai/suggest
Authorization: Bearer 3f1c9e...<64 hex chars>...a8
Content-Type: application/json
{
"feature": "writing_assist",
"input": "we shipped the new thing today its pretty cool and fast",
"context": { "action": "rewrite", "targetPlatform": "mastodon" }
}
Preview a document from one of your lists, then confirm it:
POST /api/ai/suggest
Authorization: Bearer <token>
Content-Type: application/json
{
"feature": "powered_document",
"input": "Summarize this reading list into a blog-style overview.",
"context": { "mode": "from_list", "listId": "clx123abc" }
}
POST /api/ai/generate
Authorization: Bearer <token>
Content-Type: application/json
{
"feature": "powered_document",
"artifact": { "kind": "document", "title": "My Reading Overview", "markdown": "# My Reading Overview…" }
}
Check what you can use before offering AI actions in a client:
GET /api/ai/status
Authorization: Bearer <token>
Related
- Settings (Help Center): set your provider keys and default provider/model on the Integrations page, and how AI features are gated.
- Lists (API) and List Schema DSL: the shape a
powered_templateartifact produces. - Documents (API): what
powered_documentandarticle_serieswrite. - Messages (API): scheduled posts, which a
message_serieslist mirrors. - Authentication & OAuth: session cookies and Bearer sync tokens accepted by these endpoints.
- API overview: base URL, authentication at a glance, and response conventions.
- API explorer: try endpoints live with the interactive Swagger console.