Documentation

One public reference for the Legion SDK, OAuth, API, CLI, MCP, and platform integrations.

Legion developer documentation

This page mirrors the Developer documentation with one exception: MCP access token generation requires login.

Use the copy controls or each section's plain-text view for the exact shared documentation source.

Create an App

Via Dashboard

Via CLI

legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
# Returns a public client_id and a one-time client_secret.
# Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id.
# They still send a registered redirect_uri during authorization and use S256 PKCE.
# Multiple redirect URIs (comma-separated):
legion create-app --name "My App" --redirect-uris "http://localhost:3000/callback,https://myapp.com/callback"
# Optional partner attribution for non-partner developers:
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable

Via MCP (AI Agents)

Use the legion_create_oauth_app tool with name and redirect_uris parameters.

View plain text
### Via Dashboard
- Log in to the Legion dashboard at https://legion-ai.org/developer
- Or start on the public registration page at https://legion-ai.org/register and sign in only after submitting the form
- Create a new app: enter your app name and domain (e.g., https://yourapp.com).
- You'll receive a client_id. Register your domain as the redirect URI.
- The server automatically appends /callback if no path is given, so https://yourapp.com becomes https://yourapp.com/callback. You can also register a custom path (e.g., /auth/callback) and it will be used as-is.
- Use the full redirect URI (with path) in your code.
- Response format is OpenAI-compatible.

### Via CLI
legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
# Returns a public client_id and a one-time client_secret.
# Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id.
# They still send a registered redirect_uri during authorization and use S256 PKCE.

# Multiple redirect URIs (comma-separated):
legion create-app --name "My App" --redirect-uris "http://localhost:3000/callback,https://myapp.com/callback"

# Optional partner attribution for non-partner developers:
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable

### Via MCP (AI Agents)
Use the legion_create_oauth_app tool with name and redirect_uris parameters.

Public Creator Profiles

Creator profile reads are public and resolve either a unique username or a Clerk user ID:

  • GET /v1/profiles/:identifier — public identity and profile counts
  • GET /v1/profiles/:identifier/ais — cursor-paginated hosted chat AIs created directly by the user
  • GET /v1/profiles/:identifier/apps — cursor-paginated registered non-chat apps/websites created directly by the user
  • GET /v1/profiles/:identifier/posts — cursor-paginated creator image posts with viewer-specific locked state

The registered-app response includes public display metadata and a normalized HTTP(S) website origin. It never exposes OAuth callback paths, client IDs, or secrets. Use the returned next_cursor as the cursor query parameter to request another page.

View plain text
Creator profile reads are public and resolve either a unique username or a Clerk user ID:

- GET /v1/profiles/:identifier — public identity and profile counts
- GET /v1/profiles/:identifier/ais — cursor-paginated hosted chat AIs created directly by the user
- GET /v1/profiles/:identifier/apps — cursor-paginated registered non-chat apps/websites created directly by the user
- GET /v1/profiles/:identifier/posts — cursor-paginated creator image posts with viewer-specific locked state

The registered-app response includes public display metadata and a normalized HTTP(S) website origin. It never exposes OAuth callback paths, client IDs, or secrets. Use the returned next_cursor as the cursor query parameter to request another page.

Key Concepts: OAuth App vs Railway Log Source

OAuth App = An OAuth application registration. Creating an app gives you a public client_id for standard PKCE integrations and a one-time client_secret for advanced confidential backend OAuth. OAuth apps are for building AI-powered features.

Railway Log Source = A Railway deployment connection. Connecting a log source gives you access to deployment logs for debugging. Log sources are for monitoring and log access only — they are not required for API integration.

Most developers only need an OAuth App. Railway log sources are optional and only useful if you deploy on Railway and want log access via the CLI or MCP tools.

View plain text
**OAuth App** = An OAuth application registration. Creating an app gives you a public client_id for standard PKCE integrations and a one-time client_secret for advanced confidential backend OAuth. OAuth apps are for building AI-powered features.

**Railway Log Source** = A Railway deployment connection. Connecting a log source gives you access to deployment logs for debugging. Log sources are for monitoring and log access only — they are not required for API integration.

Most developers only need an OAuth App. Railway log sources are optional and only useful if you deploy on Railway and want log access via the CLI or MCP tools.

Integration Approach

Recommended: Login at Point of Use

Prompt users to connect their Legion account when they first try to use AI features (e.g., send a chat message). This provides the best UX - users only authenticate when they actually need it.

Alternative: Dedicated Button

A "Connect Legion" button in your UI is also acceptable if you prefer upfront authentication.

View plain text
### Recommended: Login at Point of Use
Prompt users to connect their Legion account when they first try to use AI features (e.g., send a chat message). This provides the best UX - users only authenticate when they actually need it.

### Alternative: Dedicated Button
A "Connect Legion" button in your UI is also acceptable if you prefer upfront authentication.

OAuth Connect Flow (Manual)

Important notes:

  • Only response_type=code is supported (authorization code flow)
  • scope defaults to 'chat:completions' if omitted
  • Authorization start requires redirect_uri, and it must match a registered redirect URI.
  • Public browser/CMS clients must use S256 PKCE. The authorization request includes code_challenge and code_challenge_method=S256; the token exchange includes code_verifier.
  • redirect_uri is intentionally not sent again in the token exchange body.
  • A public-client token exchange with only grant_type, code, and client_id is invalid for PKCE-bound authorization codes and returns invalid_grant.
  • Public clients do not send client_secret; confidential server-side clients must send client_secret when exchanging a code issued without PKCE.
  • Authorization codes expire after ten minutes and can be used only once.
  • authorization_intent supports exactly: authorize, increase_cap, payment_method.
  • Legion may require the user to add a payment method during consent. If so, the hosted /connect page handles Stripe setup first, then redirects back to your callback with the authorization code.
  • If payment setup is not required during consent, the user can authorize first, receives a payment-method prompt email, and can make capped trial usage until a card is added. User-funded usage is collected through monthly billing.
// Step 1 (recommended): the SDK generates PKCE and redirects to consent.
const legion = createLegion({ clientId: 'YOUR_CLIENT_ID' });
legion.connect({ redirectUri: 'https://yourapp.com/callback' });
// Step 2: User authorizes and returns with code + state. The SDK retrieves the
// verifier associated with that state and performs the protected exchange.
const { access_token, refresh_token, expires_in } = await legion.exchangeCode(code, { state });
// access_token expires in 86400 seconds (24 hours)
// Store both tokens - use refresh_token to get new access tokens
// Step 2, manual equivalent for public clients:
// Send code_verifier. Do not send redirect_uri or client_secret.
const tokenRes = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: pkceVerifierFromStep1,
  }),
});
// Step 3: Make API calls with user's token
const chatRes = await fetch('https://api.legion-ai.org/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
  },
  body: JSON.stringify({
    // Optional: omit model to use your app's default model from the dashboard
    model: 'gemini-2.5-flash',
    messages: [{ role: 'user', content: 'Hello!' }],
    // Optional: inject user context for personalized responses
    user_preferences: true,  // user's AI preferences (e.g. "brief answers")
    user_information: true,  // demographics (age, location, occupation)
    user_context: true,      // query patterns (top topics, intent distribution)
    // Optional: raise the owner margin for this call (integer % of model cost,
    // 0-1000). Raise-only: values below your app's configured margin are ignored.
    margin_pct: 150,
  }),
});
const data = await chatRes.json();
const reply = data.choices[0].message.content;

View plain text
Important notes:
- Only response_type=code is supported (authorization code flow)
- scope defaults to 'chat:completions' if omitted
- Authorization start requires redirect_uri, and it must match a registered redirect URI.
- Public browser/CMS clients must use S256 PKCE. The authorization request includes code_challenge and code_challenge_method=S256; the token exchange includes code_verifier.
- redirect_uri is intentionally not sent again in the token exchange body.
- A public-client token exchange with only grant_type, code, and client_id is invalid for PKCE-bound authorization codes and returns invalid_grant.
- Public clients do not send client_secret; confidential server-side clients must send client_secret when exchanging a code issued without PKCE.
- Authorization codes expire after ten minutes and can be used only once.
- authorization_intent supports exactly: authorize, increase_cap, payment_method.
- Legion may require the user to add a payment method during consent. If so, the hosted /connect page handles Stripe setup first, then redirects back to your callback with the authorization code.
- If payment setup is not required during consent, the user can authorize first, receives a payment-method prompt email, and can make capped trial usage until a card is added. User-funded usage is collected through monthly billing.

// Step 1 (recommended): the SDK generates PKCE and redirects to consent.
const legion = createLegion({ clientId: 'YOUR_CLIENT_ID' });
legion.connect({ redirectUri: 'https://yourapp.com/callback' });

// Step 2: User authorizes and returns with code + state. The SDK retrieves the
// verifier associated with that state and performs the protected exchange.
const { access_token, refresh_token, expires_in } = await legion.exchangeCode(code, { state });
// access_token expires in 86400 seconds (24 hours)
// Store both tokens - use refresh_token to get new access tokens

// Step 2, manual equivalent for public clients:
// Send code_verifier. Do not send redirect_uri or client_secret.
const tokenRes = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code,
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: pkceVerifierFromStep1,
  }),
});

// Step 3: Make API calls with user's token
const chatRes = await fetch('https://api.legion-ai.org/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${access_token}`,
  },
  body: JSON.stringify({
    // Optional: omit model to use your app's default model from the dashboard
    model: 'gemini-2.5-flash',
    messages: [{ role: 'user', content: 'Hello!' }],
    // Optional: inject user context for personalized responses
    user_preferences: true,  // user's AI preferences (e.g. "brief answers")
    user_information: true,  // demographics (age, location, occupation)
    user_context: true,      // query patterns (top topics, intent distribution)
    // Optional: raise the owner margin for this call (integer % of model cost,
    // 0-1000). Raise-only: values below your app's configured margin are ignored.
    margin_pct: 150,
  }),
});
const data = await chatRes.json();
const reply = data.choices[0].message.content;

---

Available Models

Default model: your app's default model if configured in the dashboard; otherwise the Google launch default gemini-2.5-flash. Owner margin: configurable markup on model costs (default 50%). Legion also adds a fixed 20% fee on model cost. You keep 100% of the owner margin. Set via the dashboard, CLI (--margin), or PATCH /v1/apps/:clientId with owner_margin_pct.

Inline (per-request) margin: any metered request (chat, media, audio) may include margin_pct — an integer percent of model cost (0-1000) — next to model. It is applied raise-only: values at or above your app's configured owner_margin_pct are honored for that call; values below it (or invalid values) are ignored and the app default applies. This makes the app-level margin your floor: set it to the lowest margin you ever want, then raise it inline per feature. The embed script supports data-margin-pct, and the WordPress/Drupal plugins expose margin_pct per widget. The applied percent is recorded per transaction and visible in your dashboard usage data (margin_pct_applied). Permitted models are controlled centrally. Canonical defaults live in packages/finance-core/src/index.js (DEFAULT_MODEL_CATALOG and DEFAULT_MODEL_PRICING). Runtime model activation overrides come from auth-server model_catalog, and runtime price overrides come from auth-server price_catalog.

Launch active chat models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite

Launch note: Legion is temporarily Google-only across active AI models to reduce provider billing timing risk. OpenAI, Anthropic, and xAI integrations remain in the codebase but are inactive in the launch catalog until reintroduced.

Speech Models (Audio)

Speech-to-text default: gemini-2.5-flash Text-to-speech default: gemini-2.5-flash-preview-tts

Speech-to-text: gemini-2.5-flash Text-to-speech: gemini-2.5-flash-preview-tts

Note: use gemini-2.5-flash for speech-to-text requests. Legion routes that public model id through the audio transcription endpoint context; clients should not use internal catalog aliases.

Media Generation Models

Image: gemini-3.1-flash-image-preview (Google AI Studio, default), nano-banana-2 (Google Vertex) Video generation is temporarily unavailable during launch. Video model rows and pricing remain in Legion's internal catalog for reactivation, but active model discovery does not expose video models.

Media prices are based on the concrete generation settings Legion sends for each active model (for example: Gemini image generation at 1K). Text pricing also follows vendor large-context tiers when providers charge more above a prompt-token threshold. Configure defaults via the dashboard or PATCH /v1/apps/:clientId with default_image_model.

List Models Programmatically

GET https://api.legion-ai.org/v1/models
Returns an OpenAI-compatible model list including media models. No authentication required.
GET https://api.legion-ai.org/v1/models/catalog
Returns the active Legion catalog with active public defaults plus working-model options used internally by prompt optimization and intent routing. Speech model types are included with `type: "speech_to_text"` and `type: "text_to_speech"`.

Example:

curl https://api.legion-ai.org/v1/models
// Response: { "object": "list", "data": [{ "id": "gemini-2.5-flash", "object": "model", "type": "chat", ... }, { "id": "nano-banana-2", "type": "image_generation", ... }, ...] }

View plain text
Default model: your app's default model if configured in the dashboard; otherwise the Google launch default gemini-2.5-flash.
Owner margin: configurable markup on model costs (default 50%). Legion also adds a fixed 20% fee on model cost. You keep 100% of the owner margin. Set via the dashboard, CLI (--margin), or PATCH /v1/apps/:clientId with owner_margin_pct.

Inline (per-request) margin: any metered request (chat, media, audio) may include margin_pct — an integer percent of model cost (0-1000) — next to model. It is applied raise-only: values at or above your app's configured owner_margin_pct are honored for that call; values below it (or invalid values) are ignored and the app default applies. This makes the app-level margin your floor: set it to the lowest margin you ever want, then raise it inline per feature. The embed script supports data-margin-pct, and the WordPress/Drupal plugins expose margin_pct per widget. The applied percent is recorded per transaction and visible in your dashboard usage data (margin_pct_applied).
Permitted models are controlled centrally. Canonical defaults live in packages/finance-core/src/index.js (DEFAULT_MODEL_CATALOG and DEFAULT_MODEL_PRICING). Runtime model activation overrides come from auth-server model_catalog, and runtime price overrides come from auth-server price_catalog.

Launch active chat models: gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite

Launch note: Legion is temporarily Google-only across active AI models to reduce provider billing timing risk. OpenAI, Anthropic, and xAI integrations remain in the codebase but are inactive in the launch catalog until reintroduced.

### Speech Models (Audio)

Speech-to-text default: gemini-2.5-flash
Text-to-speech default: gemini-2.5-flash-preview-tts

Speech-to-text: gemini-2.5-flash
Text-to-speech: gemini-2.5-flash-preview-tts

Note: use `gemini-2.5-flash` for speech-to-text requests. Legion routes that public model id through the audio transcription endpoint context; clients should not use internal catalog aliases.

### Media Generation Models

Image: gemini-3.1-flash-image-preview (Google AI Studio, default), nano-banana-2 (Google Vertex)
Video generation is temporarily unavailable during launch. Video model rows and pricing remain in Legion's internal catalog for reactivation, but active model discovery does not expose video models.

Media prices are based on the concrete generation settings Legion sends for each active model (for example: Gemini image generation at 1K).
Text pricing also follows vendor large-context tiers when providers charge more above a prompt-token threshold.
Configure defaults via the dashboard or PATCH /v1/apps/:clientId with default_image_model.

### List Models Programmatically
GET https://api.legion-ai.org/v1/models
Returns an OpenAI-compatible model list including media models. No authentication required.

GET https://api.legion-ai.org/v1/models/catalog
Returns the active Legion catalog with active public defaults plus working-model options used internally by prompt optimization and intent routing. Speech model types are included with `type: "speech_to_text"` and `type: "text_to_speech"`.

Example:
curl https://api.legion-ai.org/v1/models
// Response: { "object": "list", "data": [{ "id": "gemini-2.5-flash", "object": "model", "type": "chat", ... }, { "id": "nano-banana-2", "type": "image_generation", ... }, ...] }

---

Custom Models (Your Own Weights)

Upload a fine-tuned LoRA adapter and use it anywhere a model id is accepted — for your own apps. Legion never stores or runs your weights: they are registered with the inference provider, and Legion keeps only a reference.

Add one from the dashboard at /add-model (also linked from Developer → your app → Admin → Models). Give it a name, pick the base model your weights were trained on, and upload a .zip / .tar.gz — or point at a Hugging Face repo. Manage what you have added at /developer/models.

Scope and Visibility

Uploaded models are private to you. They are given an id of the form custom-<slug> and:

  • appear only in YOUR model pickers, under the type you chose
  • can only be invoked through apps YOU own — a request from anyone else's app is rejected with

403 model_not_available

  • never appear in GET /v1/models or GET /v1/models/catalog, which stay public-only
  • are never chosen as a platform default

The model id is frozen at creation. Renaming a model changes its display name only, so app defaults and historical usage records keep working.

Supported Types

chat — LoRA adapters on an allowlisted base, served at the base model's per-token price.

Every other type (image_generation, video_generation, speech_to_text, text_to_speech) is rejected with unsupported_custom_model_type for now.

Your Weights Stay Private

Point Legion at a private Hugging Face repo and supply a read token. The token is used once to fetch the weights and is never stored. The provider then keeps its own copy, so after your model goes live the source repo can stay private, or be deleted entirely — inference never touches it again.

A public repo works too, but note that publishing the weights means anyone can self-host them; you would be monetizing your app experience rather than exclusive access to the model.

The Base Model Is Detected For You

You do not specify the base. Legion reads base_model_name_or_path out of your adapter_config.json and matches it against the supported bases, so a mismatch is caught in seconds rather than after a failed deployment. Community mirrors of the same base (for example unsloth/Meta-Llama-3.1-8B-Instruct) resolve to the canonical base automatically.

Pricing and Earnings

There is no separate charge for a custom model, and no new revenue split. You are billed the base model's provider rate, and you earn through your app's existing owner margin exactly as you do with first-party models:

user charge = provider cost + your owner margin + Legion margin (20%) + infra + risk premium

Set your rate in Admin → Owner Margin, or raise it per request with inline margin_pct.

Using a Custom Model

Set it as an app default (dashboard, or PATCH /v1/apps/:clientId with default_model), or pass the id per request:

  const res = await legion.chat({ model: 'custom-chef-llama-8b', messages });

API Reference

GET /v1/custom-models/options Enabled types, allowed base models with their provider cost (no auth) POST /v1/custom-models/inspect Resolve a repo's base model from its adapter_config.json GET /v1/custom-models Your models (refreshes anything still deploying) POST /v1/custom-models/upload-url Presigned upload for a weights archive POST /v1/custom-models Register a model; returns 202, then poll for status GET /v1/custom-models/:id One model, with a live status refresh while deploying PATCH /v1/custom-models/:id Rename, re-describe, or toggle active/disabled DELETE /v1/custom-models/:id Disable the model (historical usage is preserved)

All endpoints require your Clerk session or a leg_ personal access token, and only ever act on your own models.

Statuses: pending → deploying → active, or failed (with status_detail explaining the provider error). Disabling a model removes it from routing within about two minutes.

Create example:

curl -X POST https://auth.legion-ai.org/v1/custom-models \
  -H "Authorization: Bearer leg_..." -H "Content-Type: application/json" \
  -d '{"display_name":"Chef Llama 8B","model_type":"chat","source":{"kind":"hf_repo","hf_repo":"you/chef-lora","hf_token":"hf_..."},"terms_accepted":true}'

base_model is optional and detected from the adapter. Pass it explicitly only to override detection.

A Hugging Face token may be supplied as source.hf_token for a private repo. It is used once to fetch the weights and is never stored.


View plain text
Upload a fine-tuned LoRA adapter and use it anywhere a model id is accepted — for your own apps.
Legion never stores or runs your weights: they are registered with the inference provider, and
Legion keeps only a reference.

Add one from the dashboard at /add-model (also linked from Developer → your app → Admin → Models).
Give it a name, pick the base model your weights were trained on, and upload a .zip / .tar.gz — or
point at a Hugging Face repo. Manage what you have added at /developer/models.

### Scope and Visibility

Uploaded models are private to you. They are given an id of the form `custom-<slug>` and:
- appear only in YOUR model pickers, under the type you chose
- can only be invoked through apps YOU own — a request from anyone else's app is rejected with
  403 model_not_available
- never appear in GET /v1/models or GET /v1/models/catalog, which stay public-only
- are never chosen as a platform default

The model id is frozen at creation. Renaming a model changes its display name only, so app defaults
and historical usage records keep working.

### Supported Types

chat — LoRA adapters on an allowlisted base, served at the base model's per-token price.

Every other type (image_generation, video_generation, speech_to_text, text_to_speech) is rejected
with unsupported_custom_model_type for now.

### Your Weights Stay Private

Point Legion at a private Hugging Face repo and supply a read token. The token is used once to fetch
the weights and is never stored. The provider then keeps its own copy, so after your model goes live
the source repo can stay private, or be deleted entirely — inference never touches it again.

A public repo works too, but note that publishing the weights means anyone can self-host them; you
would be monetizing your app experience rather than exclusive access to the model.

### The Base Model Is Detected For You

You do not specify the base. Legion reads base_model_name_or_path out of your adapter_config.json
and matches it against the supported bases, so a mismatch is caught in seconds rather than after a
failed deployment. Community mirrors of the same base (for example unsloth/Meta-Llama-3.1-8B-Instruct)
resolve to the canonical base automatically.

### Pricing and Earnings

There is no separate charge for a custom model, and no new revenue split. You are billed the base
model's provider rate, and you earn through your app's existing owner margin exactly as you do with
first-party models:

  user charge = provider cost + your owner margin + Legion margin (20%) + infra + risk premium

Set your rate in Admin → Owner Margin, or raise it per request with inline margin_pct.

### Using a Custom Model

Set it as an app default (dashboard, or PATCH /v1/apps/:clientId with default_model), or pass the id
per request:

  const res = await legion.chat({ model: 'custom-chef-llama-8b', messages });

### API Reference

GET    /v1/custom-models/options   Enabled types, allowed base models with their provider cost (no auth)
POST   /v1/custom-models/inspect   Resolve a repo's base model from its adapter_config.json
GET    /v1/custom-models           Your models (refreshes anything still deploying)
POST   /v1/custom-models/upload-url  Presigned upload for a weights archive
POST   /v1/custom-models           Register a model; returns 202, then poll for status
GET    /v1/custom-models/:id       One model, with a live status refresh while deploying
PATCH  /v1/custom-models/:id       Rename, re-describe, or toggle active/disabled
DELETE /v1/custom-models/:id       Disable the model (historical usage is preserved)

All endpoints require your Clerk session or a leg_ personal access token, and only ever act on your
own models.

Statuses: pending → deploying → active, or failed (with status_detail explaining the provider
error). Disabling a model removes it from routing within about two minutes.

Create example:
curl -X POST https://auth.legion-ai.org/v1/custom-models \
  -H "Authorization: Bearer leg_..." -H "Content-Type: application/json" \
  -d '{"display_name":"Chef Llama 8B","model_type":"chat","source":{"kind":"hf_repo","hf_repo":"you/chef-lora","hf_token":"hf_..."},"terms_accepted":true}'

base_model is optional and detected from the adapter. Pass it explicitly only to override detection.

A Hugging Face token may be supplied as source.hf_token for a private repo. It is used once to fetch
the weights and is never stored.

---

Media Generation

POST https://api.legion-ai.org/v1/media/generations
Generate images. Requires a valid Bearer token.

Request body:

  • type (required): "image"
  • prompt (required): Description of the media to generate
  • model (optional): Specific model to use; falls back to app default then platform default
  • thread_messages (optional): Recent chat turns to preserve continuity for follow-up requests like "make another one like the last image"

For content-grounded image requests such as infographics, diagrams, timelines, or "make a visual from this paper", Legion first builds a structured visual brief from the recent chat thread and any retrieved app knowledge-base content, then converts that brief into the final image prompt.

Media generation is async — returns status "processing" with an id. Poll GET /v1/media/generations/{id} until status is "complete".

Video generation is temporarily unavailable during launch. Requests to POST /v1/media/generations with type "video" or to create a video infographic job return 503 with error "video_generation_unavailable". Existing video job status reads remain available for historical jobs.

Video infographic creation code is retained but disconnected at launch:

SDK usage:

const result = await legion.generateMedia("a sunset over the ocean", { type: "image" });
console.log(result.data[0].url);

Intent Classification

POST https://api.legion-ai.org/v1/media/classify
Determines whether a user message is asking for text, image generation, or media search. Video creation requests are classified as text while launch video generation is suspended. Uses gemini-2.5-flash-lite for fast, accurate classification.

Request: { "message": "draw me a cat on a skateboard", "history": [{ "role": "user", "content": "..." }] }

  • history (optional): last few conversation turns so follow-ups like "show me another one" classify correctly. Server-truncated to the last 3 turns.

Response: { "intent": "generate_image", "available_intents": ["text", "generate_image", "generate_infographic", "find_image", "find_video"] }

  • intent: one of "text", "generate_image", "generate_infographic", "find_image", "find_video"
  • available_intents: the intents this app's routing settings allow. Always includes intent.

Classification is constrained by the app's enabled services (see Prompt Routing) — a disabled service's labels are never offered to the classifier, so intent is always a route the app owner allows.

Prompt Routing (Per-App Service Toggles)

An app owner or partner chooses which services a user prompt may be routed to. The four services are:

  • text — text conversation, questions, code, explanations
  • image_generation — creates images and infographics (generate_image, generate_infographic)
  • image_retrieval — finds existing images (find_image)
  • video_retrieval — finds existing videos (find_video)

All four are enabled by default. At least one must stay enabled.

Configure via the dashboard (Chat settings → Prompt Routing) or PATCH /v1/apps/:clientId with enabled_chat_services.

Request: { "enabled_chat_services": ["text", "image_retrieval"] }

  • Array of service keys; unknown keys and an empty array are rejected with 400 validation_error.
  • Returned on every app payload (GET /v1/apps, GET /v1/apps/:clientId, and the public chat info endpoint).

When a prompt classifies to a disabled service, Legion falls back deterministically: to text if enabled, otherwise to the first enabled service in the order image_retrieval, image_generation, video_retrieval. An app with exactly one enabled service skips the classifier call entirely and routes straight to that service.

Media Search (Image Retrieval)

POST https://api.legion-ai.org/v1/media/search
Semantic search over the app's uploaded media (knowledge-base images/videos) and this user's previously generated media, using Gemini Embedding 2 in a shared text/media vector space. Scoped to the app: a chat with app X only ever surfaces media uploaded for app X.

Request body:

  • query (required): the user's request text
  • media_type (required): "image" or "video"
  • dedup (optional): true to exclude media this user has already been shown by this app. When the whole matching pool has been shown, the least-recently-shown match is returned flagged "repeat": true and the response carries "exhausted": true.

Response: { "data": [ { "url", "storage_key", "item_ref", "media_type", "description", "source", "similarity", "score", "repeat"? } ], "exhausted" }

  • url: presigned download URL (expires ~1h). Persist storage_key, not url — the hosted chat re-mints fresh URLs at read time.
  • item_ref: stable identity ({ kind: "kb" | "generated", id }) — pass it back when persisting the assistant message so the shown-media ledger records it.
  • similarity: cosine similarity of the semantic match (null for keyword-only hits).

Recommended retrieval reply flow (what the hosted chat does):

  1. Classify the message; on find_image, call /v1/media/search with dedup: true.
  2. Take data[0] and send a normal /v1/chat/completions request whose last user message includes both the request text and the image as an image_url content part, instructing the model to explain in character why it chose this image. Content-part arrays work across providers — Legion inlines remote image URLs automatically for Gemini and Anthropic models.
  3. On no results, send a completion instructing the model to decline in character and offer to generate a new image instead.

All completions bill normally; classifier and embedding costs accrue as infra allocation on the next settlement.

Prompt Optimization & Context

All prompts are automatically refined using gemini-2.5-flash-lite before being sent to the generation model. Standard media requests use the same user context and app knowledge base (RAG) as the text path. Content-grounded visuals use the recent chat thread plus app RAG to extract only the relevant source material before the final image prompt is written. If the request clearly depends on source material ("this paper", "above", "this conversation") and no grounding content is available, the API returns 422 insufficient_source_context instead of generating a generic visual. The optimized prompt is returned as revised_prompt.


View plain text
POST https://api.legion-ai.org/v1/media/generations
Generate images. Requires a valid Bearer token.

Request body:
- type (required): "image"
- prompt (required): Description of the media to generate
- model (optional): Specific model to use; falls back to app default then platform default
- thread_messages (optional): Recent chat turns to preserve continuity for follow-up requests like "make another one like the last image"

For content-grounded image requests such as infographics, diagrams, timelines, or "make a visual from this paper", Legion first builds a structured visual brief from the recent chat thread and any retrieved app knowledge-base content, then converts that brief into the final image prompt.

Media generation is async — returns status "processing" with an id. Poll GET /v1/media/generations/{id} until status is "complete".

Video generation is temporarily unavailable during launch. Requests to POST /v1/media/generations with type "video" or to create a video infographic job return 503 with error "video_generation_unavailable". Existing video job status reads remain available for historical jobs.

Video infographic creation code is retained but disconnected at launch:
- POST https://api.legion-ai.org/v1/video-jobs
- GET https://api.legion-ai.org/v1/video-jobs/{id}
- Supports Idempotency-Key for safe client retries
- Status is durable: if the fast Redis cache expires, Legion falls back to the database record instead of treating the job as missing

SDK usage:
const result = await legion.generateMedia("a sunset over the ocean", { type: "image" });
console.log(result.data[0].url);

### Intent Classification
POST https://api.legion-ai.org/v1/media/classify
Determines whether a user message is asking for text, image generation, or media search. Video creation requests are classified as text while launch video generation is suspended. Uses gemini-2.5-flash-lite for fast, accurate classification.

Request: { "message": "draw me a cat on a skateboard", "history": [{ "role": "user", "content": "..." }] }
- history (optional): last few conversation turns so follow-ups like "show me another one" classify correctly. Server-truncated to the last 3 turns.
Response: { "intent": "generate_image", "available_intents": ["text", "generate_image", "generate_infographic", "find_image", "find_video"] }
- intent: one of "text", "generate_image", "generate_infographic", "find_image", "find_video"
- available_intents: the intents this app's routing settings allow. Always includes intent.

Classification is constrained by the app's enabled services (see Prompt Routing) — a disabled service's labels are never offered to the classifier, so intent is always a route the app owner allows.

### Prompt Routing (Per-App Service Toggles)
An app owner or partner chooses which services a user prompt may be routed to. The four services are:
- text — text conversation, questions, code, explanations
- image_generation — creates images and infographics (generate_image, generate_infographic)
- image_retrieval — finds existing images (find_image)
- video_retrieval — finds existing videos (find_video)

All four are enabled by default. At least one must stay enabled.

Configure via the dashboard (Chat settings → Prompt Routing) or PATCH /v1/apps/:clientId with enabled_chat_services.

Request: { "enabled_chat_services": ["text", "image_retrieval"] }
- Array of service keys; unknown keys and an empty array are rejected with 400 validation_error.
- Returned on every app payload (GET /v1/apps, GET /v1/apps/:clientId, and the public chat info endpoint).

When a prompt classifies to a disabled service, Legion falls back deterministically: to text if enabled, otherwise to the first enabled service in the order image_retrieval, image_generation, video_retrieval. An app with exactly one enabled service skips the classifier call entirely and routes straight to that service.

### Media Search (Image Retrieval)
POST https://api.legion-ai.org/v1/media/search
Semantic search over the app's uploaded media (knowledge-base images/videos) and this user's previously generated media, using Gemini Embedding 2 in a shared text/media vector space. Scoped to the app: a chat with app X only ever surfaces media uploaded for app X.

Request body:
- query (required): the user's request text
- media_type (required): "image" or "video"
- dedup (optional): true to exclude media this user has already been shown by this app. When the whole matching pool has been shown, the least-recently-shown match is returned flagged "repeat": true and the response carries "exhausted": true.

Response: { "data": [ { "url", "storage_key", "item_ref", "media_type", "description", "source", "similarity", "score", "repeat"? } ], "exhausted" }
- url: presigned download URL (expires ~1h). Persist storage_key, not url — the hosted chat re-mints fresh URLs at read time.
- item_ref: stable identity ({ kind: "kb" | "generated", id }) — pass it back when persisting the assistant message so the shown-media ledger records it.
- similarity: cosine similarity of the semantic match (null for keyword-only hits).

Recommended retrieval reply flow (what the hosted chat does):
1. Classify the message; on find_image, call /v1/media/search with dedup: true.
2. Take data[0] and send a normal /v1/chat/completions request whose last user message includes both the request text and the image as an image_url content part, instructing the model to explain in character why it chose this image. Content-part arrays work across providers — Legion inlines remote image URLs automatically for Gemini and Anthropic models.
3. On no results, send a completion instructing the model to decline in character and offer to generate a new image instead.
All completions bill normally; classifier and embedding costs accrue as infra allocation on the next settlement.

### Prompt Optimization & Context
All prompts are automatically refined using gemini-2.5-flash-lite before being sent to the generation model. Standard media requests use the same user context and app knowledge base (RAG) as the text path. Content-grounded visuals use the recent chat thread plus app RAG to extract only the relevant source material before the final image prompt is written. If the request clearly depends on source material ("this paper", "above", "this conversation") and no grounding content is available, the API returns 422 insufficient_source_context instead of generating a generic visual. The optimized prompt is returned as revised_prompt.

---

Audio

Legion exposes speech-to-text and text-to-speech as separate API calls. Legion does not orchestrate microphone capture, speaker playback, or conversational voice mode for your platform. Your app records audio or plays audio locally, and Legion handles the authenticated AI call plus billing.

Speech-to-Text

POST https://api.legion-ai.org/v1/audio/transcriptions

OpenAI-compatible multipart upload endpoint.

Form fields:

  • file (required): audio file upload
  • model (optional): defaults to app default default_speech_to_text_model, otherwise Legion default gemini-2.5-flash
  • prompt (optional): extra transcription guidance
  • response_format (optional): json or text

Response:

  • json: { "text": "..." }
  • text: plain transcription text

Text-to-Speech

POST https://api.legion-ai.org/v1/audio/speech

OpenAI-compatible speech generation endpoint.

JSON body:

  • input (required): text to synthesize
  • model (optional): defaults to app default default_text_to_speech_model, otherwise Legion default gemini-2.5-flash-preview-tts
  • voice (optional): provider voice hint
  • response_format (optional): requested output format

Response:

  • raw audio bytes with an audio Content-Type

App-Level Speech Defaults

Each app can set speech defaults independently in the developer dashboard:

  • default_speech_to_text_model
  • default_text_to_speech_model

Legion resolves speech models in this order:

  1. Request-level model
  2. App default speech model
  3. Legion platform default

View plain text
Legion exposes speech-to-text and text-to-speech as separate API calls. Legion does not orchestrate microphone capture, speaker playback, or conversational voice mode for your platform. Your app records audio or plays audio locally, and Legion handles the authenticated AI call plus billing.

### Speech-to-Text
POST https://api.legion-ai.org/v1/audio/transcriptions

OpenAI-compatible multipart upload endpoint.

Form fields:
- file (required): audio file upload
- model (optional): defaults to app default `default_speech_to_text_model`, otherwise Legion default `gemini-2.5-flash`
- prompt (optional): extra transcription guidance
- response_format (optional): `json` or `text`

Response:
- `json`: `{ "text": "..." }`
- `text`: plain transcription text

### Text-to-Speech
POST https://api.legion-ai.org/v1/audio/speech

OpenAI-compatible speech generation endpoint.

JSON body:
- input (required): text to synthesize
- model (optional): defaults to app default `default_text_to_speech_model`, otherwise Legion default `gemini-2.5-flash-preview-tts`
- voice (optional): provider voice hint
- response_format (optional): requested output format

Response:
- raw audio bytes with an audio `Content-Type`

### App-Level Speech Defaults
Each app can set speech defaults independently in the developer dashboard:
- `default_speech_to_text_model`
- `default_text_to_speech_model`

Legion resolves speech models in this order:
1. Request-level `model`
2. App default speech model
3. Legion platform default

---

User Context Injection (ON by Default)

Every API call automatically enriches the AI request with user-specific context for personalized responses. Three flags control what is injected — all default to true. To opt out, set a flag to false explicitly in your request body.

Flag: user_preferences (default: true)

  • Source: User's AI Preferences setting
  • Injects: Freeform preference text (e.g., "I prefer brief answers with sources")
  • Opt out: set user_preferences: false

Flag: user_information (default: true)

  • Source: User's profile demographics
  • Injects: Age, sex, language, location, occupation, industry
  • Opt out: set user_information: false

Flag: user_context (default: true)

  • Source: User's query history (last 90 days, across all apps)
  • Injects: Top 5 topics, intent distribution %, primary language, avg query length, total queries
  • Opt out: set user_context: false

How it works:

  • Context is appended to the system message before forwarding to the AI provider
  • If no system message exists, one is created
  • Empty sections are skipped automatically
  • If user has no profile data, nothing is injected
  • Zero additional latency when all flags are set to false
  • Results are cached for 5 minutes

SDK Example

const response = await legion.chat(
  [{ role: 'user', content: 'Help me plan my week' }],
  {
    model: 'gemini-2.5-flash',
    user_preferences: true,
    user_information: true,
    user_context: true,
  }
);
const transcription = await legion.transcribeAudio(file, {
  response_format: 'json',
});
const speech = await legion.generateSpeech('Hello from Legion', {
  voice: 'Puck',
});

Direct API Example

body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Help me plan my week' }],
  user_preferences: true,
  user_information: true,
  user_context: true,
})

View plain text
Every API call automatically enriches the AI request with user-specific context for personalized responses. Three flags control what is injected — all default to true. To opt out, set a flag to false explicitly in your request body.

Flag: user_preferences (default: true)
- Source: User's AI Preferences setting
- Injects: Freeform preference text (e.g., "I prefer brief answers with sources")
- Opt out: set user_preferences: false

Flag: user_information (default: true)
- Source: User's profile demographics
- Injects: Age, sex, language, location, occupation, industry
- Opt out: set user_information: false

Flag: user_context (default: true)
- Source: User's query history (last 90 days, across all apps)
- Injects: Top 5 topics, intent distribution %, primary language, avg query length, total queries
- Opt out: set user_context: false

How it works:
- Context is appended to the system message before forwarding to the AI provider
- If no system message exists, one is created
- Empty sections are skipped automatically
- If user has no profile data, nothing is injected
- Zero additional latency when all flags are set to false
- Results are cached for 5 minutes

### SDK Example
const response = await legion.chat(
  [{ role: 'user', content: 'Help me plan my week' }],
  {
    model: 'gemini-2.5-flash',
    user_preferences: true,
    user_information: true,
    user_context: true,
  }
);

const transcription = await legion.transcribeAudio(file, {
  response_format: 'json',
});

const speech = await legion.generateSpeech('Hello from Legion', {
  voice: 'Puck',
});

### Direct API Example
body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Help me plan my week' }],
  user_preferences: true,
  user_information: true,
  user_context: true,
})

---

Platform-Funded Mode (Developer Pays)

Developers can pay for AI usage on behalf of their users by passing platform_funded: true in the request body. When enabled, the user is not charged — the developer absorbs the cost (model cost + infra allocation) at cost with no markup.

Requirements

  • The developer must have a payment method on file (Stripe).
  • If no payment method is found, the API returns 402 developer_payment_required.

Request Example

body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Hello!' }],
  platform_funded: true,
})

Response

The _legion object in the response will include:

  • platform_funded: true
  • cost_cents: 0 (user pays nothing)
  • developer_charge_cents: total cost absorbed by the developer

Billing

Platform-funded transactions are recorded with billed=false and collected via the developer's monthly invoice. The developer_charge_cents column tracks the amount owed.


View plain text
Developers can pay for AI usage on behalf of their users by passing platform_funded: true in the request body. When enabled, the user is not charged — the developer absorbs the cost (model cost + infra allocation) at cost with no markup.

### Requirements
- The developer must have a payment method on file (Stripe).
- If no payment method is found, the API returns 402 developer_payment_required.

### Request Example
body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'Hello!' }],
  platform_funded: true,
})

### Response
The _legion object in the response will include:
- platform_funded: true
- cost_cents: 0 (user pays nothing)
- developer_charge_cents: total cost absorbed by the developer

### Billing
Platform-funded transactions are recorded with billed=false and collected via the developer's monthly invoice. The developer_charge_cents column tracks the amount owed.

---

Partner Attribution

Accredited partners can receive a share of Legion's platform margin without increasing the user's cost or reducing the app owner's margin.

Attribution is app-level and editable. When an app is created by a claimed partner account, Legion adds that partner slug to the app's partner list. Non-partner owners may optionally provide a valid partner slug when registering the app; unknown partner slugs are rejected. App owners and active app partners can add or remove accredited partners later. Runtime chat and media calls do not set or override partner attribution.

Eligibility and Accreditation

Accredited partners are approved by Legion and listed in Legion's server-side partner registry. Examples include vibe-coding platforms and AI app-building tools. Eligibility is not self-serve: partner-claim succeeds only when the signed-in account's Clerk verified primary email exactly matches an email in the approved registry entry.

Security notes:

  • Partner claim checks Clerk directly for the verified primary email.
  • Legion does not trust the synced Postgres users.email value for partner or owner claim decisions.
  • A partner slug can only be claimed by one Legion user account.
  • A partner keeps app-admin access only while its slug remains attached to the app.

Revenue Split

Legion adds a fixed 20% platform margin on model cost. The affiliate program is temporarily paused, so new calls do not accrue affiliate shares. Partner share is calculated from the Legion platform margin:

  • No partner: Legion keeps 100% of the platform margin
  • Accredited partner: attached partners share 50%, Legion keeps 50%
  • Unaccredited or unknown partner slug: treated as no partner

The app owner's configured owner margin is separate. Partner attribution does not reduce the app owner's margin and does not increase the end user's price.

When multiple partners are attached to one app, they split the partner pool evenly. Two partners split it in half; three partners split it in thirds. Any odd cent remainder is assigned deterministically.

Partner CLI Workflow

Already-accredited partners can manage client apps from the CLI:

  1. legion login
  2. legion partner-claim
  3. legion partner-status
  4. legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
  5. legion invite-owner <client_id> --email client@example.com

Use legion partner-connect-start to start Stripe Connect onboarding for partner payouts. The first call can create a Stripe Connect Express account before returning an onboarding URL. Use legion partner-connect-status to check payout verification.

After partner-claim succeeds, new apps created by that account automatically include that partner slug. The optional --partner flag on create-app is for manual attribution by non-partner owners and is not required for claimed partner accounts.

Partner API Reference

All partner endpoints are on the auth server: https://auth.legion-ai.org.

Authentication:

  • Dashboard calls use Clerk authentication.
  • CLI calls use the Legion personal access token saved by legion login.
  • Partner claim still verifies eligibility through Clerk's verified primary email even when called with a CLI token.

POST /v1/partner/claim

  • Claims the accredited partner slug for the authenticated account.
  • Requires the authenticated user's verified Clerk primary email to appear in the partner registry.
  • Returns partner_slug, name, and category.
  • Common errors: 404 not_eligible, 409 already_claimed.

GET /v1/partner/me

  • Returns partner_slug, name, category, pending_cents, total_earned_cents, last_payout_at, connect_onboarded, connect_verified, attributed apps, and per-app earnings breakdown.
  • Common errors: 404 not_partner.

POST /v1/partner/connect/onboard

  • Starts Stripe Connect Express onboarding for partner payouts.
  • Side effect: on the first call, Legion can create a Stripe Connect account before returning the onboarding URL.
  • Returns url, account_id, and whether an existing Connect account was reused.
  • Common errors: 404 not_partner.

GET /v1/partner/connect/status

  • Checks the partner Stripe Connect account status and updates Legion's cached verification state.
  • Returns connected, verified, charges_enabled, payouts_enabled, details_submitted, and status.
  • Common errors: 404 not_partner.

GET /v1/apps/:clientId/partners

  • Lists active partner slugs for an app.
  • Available to the app owner and active app partners.

POST /v1/apps/:clientId/partners

  • Adds an accredited partner slug to an app.
  • Body: { "partner": "lovable" }
  • Idempotent for an already attached partner.
  • Common errors: 400 validation_error, 404 App not found or access denied.

DELETE /v1/apps/:clientId/partners/:partnerSlug

  • Removes a partner slug from an app.
  • Removing the final partner is allowed; after that, only the app owner can add partners back.

Partner CLI Commands

legion partner-claim
- Claim the accredited partner account for the logged-in Legion user.
legion partner-status
- Show partner balance, attributed apps, and per-app earnings.
legion partner-connect-start
- Start Stripe Connect onboarding for partner payouts. The first call may create a Stripe Connect account.
legion partner-connect-status
- Check partner payout verification status.
legion list-partners <client_id>
- List partners attached to an app.
legion add-partner <client_id> --partner lovable
- Add an accredited partner to an app.
legion remove-partner <client_id> --partner lovable
- Remove a partner from an app.
legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
- Create a client app. Claimed partner accounts are attached automatically.
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable
- Optional manual attribution for non-partner developers. Claimed partners normally do not need --partner.

View plain text
Accredited partners can receive a share of Legion's platform margin without increasing the user's cost or reducing the app owner's margin.

Attribution is app-level and editable. When an app is created by a claimed partner account, Legion adds that partner slug to the app's partner list. Non-partner owners may optionally provide a valid partner slug when registering the app; unknown partner slugs are rejected. App owners and active app partners can add or remove accredited partners later. Runtime chat and media calls do not set or override partner attribution.

### Eligibility and Accreditation

Accredited partners are approved by Legion and listed in Legion's server-side partner registry. Examples include vibe-coding platforms and AI app-building tools. Eligibility is not self-serve: partner-claim succeeds only when the signed-in account's Clerk verified primary email exactly matches an email in the approved registry entry.

Security notes:
- Partner claim checks Clerk directly for the verified primary email.
- Legion does not trust the synced Postgres users.email value for partner or owner claim decisions.
- A partner slug can only be claimed by one Legion user account.
- A partner keeps app-admin access only while its slug remains attached to the app.

### Revenue Split

Legion adds a fixed 20% platform margin on model cost. The affiliate program is temporarily paused, so new calls do not accrue affiliate shares. Partner share is calculated from the Legion platform margin:

- No partner: Legion keeps 100% of the platform margin
- Accredited partner: attached partners share 50%, Legion keeps 50%
- Unaccredited or unknown partner slug: treated as no partner

The app owner's configured owner margin is separate. Partner attribution does not reduce the app owner's margin and does not increase the end user's price.

When multiple partners are attached to one app, they split the partner pool evenly. Two partners split it in half; three partners split it in thirds. Any odd cent remainder is assigned deterministically.

### Partner CLI Workflow

Already-accredited partners can manage client apps from the CLI:

1. legion login
2. legion partner-claim
3. legion partner-status
4. legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
5. legion invite-owner <client_id> --email client@example.com

Use legion partner-connect-start to start Stripe Connect onboarding for partner payouts. The first call can create a Stripe Connect Express account before returning an onboarding URL. Use legion partner-connect-status to check payout verification.

After partner-claim succeeds, new apps created by that account automatically include that partner slug. The optional --partner flag on create-app is for manual attribution by non-partner owners and is not required for claimed partner accounts.

### Partner API Reference

All partner endpoints are on the auth server: https://auth.legion-ai.org.

Authentication:
- Dashboard calls use Clerk authentication.
- CLI calls use the Legion personal access token saved by legion login.
- Partner claim still verifies eligibility through Clerk's verified primary email even when called with a CLI token.

POST /v1/partner/claim
- Claims the accredited partner slug for the authenticated account.
- Requires the authenticated user's verified Clerk primary email to appear in the partner registry.
- Returns partner_slug, name, and category.
- Common errors: 404 not_eligible, 409 already_claimed.

GET /v1/partner/me
- Returns partner_slug, name, category, pending_cents, total_earned_cents, last_payout_at, connect_onboarded, connect_verified, attributed apps, and per-app earnings breakdown.
- Common errors: 404 not_partner.

POST /v1/partner/connect/onboard
- Starts Stripe Connect Express onboarding for partner payouts.
- Side effect: on the first call, Legion can create a Stripe Connect account before returning the onboarding URL.
- Returns url, account_id, and whether an existing Connect account was reused.
- Common errors: 404 not_partner.

GET /v1/partner/connect/status
- Checks the partner Stripe Connect account status and updates Legion's cached verification state.
- Returns connected, verified, charges_enabled, payouts_enabled, details_submitted, and status.
- Common errors: 404 not_partner.

GET /v1/apps/:clientId/partners
- Lists active partner slugs for an app.
- Available to the app owner and active app partners.

POST /v1/apps/:clientId/partners
- Adds an accredited partner slug to an app.
- Body: { "partner": "lovable" }
- Idempotent for an already attached partner.
- Common errors: 400 validation_error, 404 App not found or access denied.

DELETE /v1/apps/:clientId/partners/:partnerSlug
- Removes a partner slug from an app.
- Removing the final partner is allowed; after that, only the app owner can add partners back.

### Partner CLI Commands

legion partner-claim
- Claim the accredited partner account for the logged-in Legion user.

legion partner-status
- Show partner balance, attributed apps, and per-app earnings.

legion partner-connect-start
- Start Stripe Connect onboarding for partner payouts. The first call may create a Stripe Connect account.

legion partner-connect-status
- Check partner payout verification status.

legion list-partners <client_id>
- List partners attached to an app.

legion add-partner <client_id> --partner lovable
- Add an accredited partner to an app.

legion remove-partner <client_id> --partner lovable
- Remove a partner from an app.

legion create-app --name "Client Site" --redirect-uris "https://client.com/callback"
- Create a client app. Claimed partner accounts are attached automatically.

legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --partner lovable
- Optional manual attribution for non-partner developers. Claimed partners normally do not need --partner.

---

Delegated Ownership

Agency or partner-managed apps can assign a separate client owner for payouts. The developer keeps app management access, while the owner only gets read-only earnings and Stripe Connect setup for owner-margin payouts.

Developers invite an owner from the app's Admin area. Legion emails a claim link to the owner. The owner must sign in with the same verified email address, claim the website, then use My websites to view earnings and set up Stripe Connect.

Partners and developers can also send or cancel owner invites from the CLI:

legion invite-owner <client_id> --email client@example.com
legion cancel-owner-invite <client_id>

Client owners should use the emailed web claim page and My websites payout flow; the CLI does not expose client-owner claim or payout commands.

After an owner is assigned, the developer can no longer attach their own Stripe Connect account for that app's owner payouts. Reassignment is handled manually by Legion support.

Delegated Ownership API Reference

POST /v1/apps/:clientId/owner-invite

  • Invites a client owner by email for an app managed by the authenticated developer or partner.
  • Authentication: Clerk or Legion personal access token.
  • Body: { "email": "client@example.com" }
  • Sends an email containing a claim link to https://legion-ai.org/claim-ownership?token=...
  • Returns invite metadata, email_sent, and email_status.
  • Common errors: 400 validation_error, 404 App not found or access denied, 409 invite_pending, 409 owner_already_assigned.

DELETE /v1/apps/:clientId/owner-invite

  • Cancels the active pending unclaimed owner invite for an app.
  • Authentication: Clerk or Legion personal access token.
  • Returns { "cancelled": true } when an invite was cancelled, or false when no active invite existed.

POST /v1/owner-claims/accept

  • Used by the web claim page, not by the CLI.
  • The invitee must sign in with the same verified Clerk primary email that received the invite.
  • Body: { "token": "claim-token-from-email" }
  • Common errors: 403 email_mismatch, 404 invalid_or_expired, 409 owner_already_assigned.

GET /v1/owned-apps

  • Powers the owner's My websites page.
  • Returns read-only app earnings and payout status for apps where the caller is the delegated owner.

POST /v1/owned-apps/:clientId/connect/onboard

  • Starts Stripe Connect onboarding for an app owner payout account.
  • Used by the My websites web flow.

GET /v1/owned-apps/:clientId/connect/status

  • Checks owner payout verification for an owned app.

Delegated Ownership Boundaries

Client owners receive only payout visibility and payout onboarding for owner-margin earnings. They do not receive app keys, client secrets, redirect URI controls, model settings, knowledge base controls, logs, deletion, rotation, or developer/operator access.

Partners and developers keep app management access after assigning an owner. Owner reassignment and support recovery are manual Legion support operations.

Owner Invite CLI Commands

legion invite-owner <client_id> --email client@example.com
- Sends the owner claim email for an app you manage.
legion cancel-owner-invite <client_id>
- Cancels the active pending claim email for an app you manage.

The CLI reports common owner invite conflicts with actionable messages:

  • invite_pending: cancel the existing invite before sending another.
  • owner_already_assigned: the app already has a delegated owner; contact Legion support for reassignment.

View plain text
Agency or partner-managed apps can assign a separate client owner for payouts. The developer keeps app management access, while the owner only gets read-only earnings and Stripe Connect setup for owner-margin payouts.

Developers invite an owner from the app's Admin area. Legion emails a claim link to the owner. The owner must sign in with the same verified email address, claim the website, then use My websites to view earnings and set up Stripe Connect.

Partners and developers can also send or cancel owner invites from the CLI:

legion invite-owner <client_id> --email client@example.com
legion cancel-owner-invite <client_id>

Client owners should use the emailed web claim page and My websites payout flow; the CLI does not expose client-owner claim or payout commands.

After an owner is assigned, the developer can no longer attach their own Stripe Connect account for that app's owner payouts. Reassignment is handled manually by Legion support.

### Delegated Ownership API Reference

POST /v1/apps/:clientId/owner-invite
- Invites a client owner by email for an app managed by the authenticated developer or partner.
- Authentication: Clerk or Legion personal access token.
- Body: { "email": "client@example.com" }
- Sends an email containing a claim link to https://legion-ai.org/claim-ownership?token=...
- Returns invite metadata, email_sent, and email_status.
- Common errors: 400 validation_error, 404 App not found or access denied, 409 invite_pending, 409 owner_already_assigned.

DELETE /v1/apps/:clientId/owner-invite
- Cancels the active pending unclaimed owner invite for an app.
- Authentication: Clerk or Legion personal access token.
- Returns { "cancelled": true } when an invite was cancelled, or false when no active invite existed.

POST /v1/owner-claims/accept
- Used by the web claim page, not by the CLI.
- The invitee must sign in with the same verified Clerk primary email that received the invite.
- Body: { "token": "claim-token-from-email" }
- Common errors: 403 email_mismatch, 404 invalid_or_expired, 409 owner_already_assigned.

GET /v1/owned-apps
- Powers the owner's My websites page.
- Returns read-only app earnings and payout status for apps where the caller is the delegated owner.

POST /v1/owned-apps/:clientId/connect/onboard
- Starts Stripe Connect onboarding for an app owner payout account.
- Used by the My websites web flow.

GET /v1/owned-apps/:clientId/connect/status
- Checks owner payout verification for an owned app.

### Delegated Ownership Boundaries

Client owners receive only payout visibility and payout onboarding for owner-margin earnings. They do not receive app keys, client secrets, redirect URI controls, model settings, knowledge base controls, logs, deletion, rotation, or developer/operator access.

Partners and developers keep app management access after assigning an owner. Owner reassignment and support recovery are manual Legion support operations.

### Owner Invite CLI Commands

legion invite-owner <client_id> --email client@example.com
- Sends the owner claim email for an app you manage.

legion cancel-owner-invite <client_id>
- Cancels the active pending claim email for an app you manage.

The CLI reports common owner invite conflicts with actionable messages:
- invite_pending: cancel the existing invite before sending another.
- owner_already_assigned: the app already has a delegated owner; contact Legion support for reassignment.

---

App Knowledge Base (RAG Context)

Give your app domain-specific knowledge by uploading files. Content is automatically chunked, embedded, and retrieved at query time via hybrid search (semantic + keyword with Reciprocal Rank Fusion).

Supported File Types

  • PDF (.pdf)
  • Word (.docx)
  • PowerPoint (.pptx)
  • Video (.mp4, .mov) — described, embedded, and automatically split into searchable 15–60 second clips when long enough; MOV uploads are converted to MP4 for browser playback
  • Audio (.mp3) — embedded as single unit
  • Images (.png, .jpg, .webp, .gif, .svg, .heic, .heif) — embedded as single unit; HEIC/HEIF is converted to JPEG during processing

Upload Flow

  1. Go to your app's detail page in the developer dashboard
  2. Select or drag and drop up to 50 local files (500 MB total per batch, 100 MB per file) into the Knowledge Base section
  3. Wait for status to change from "processing" to "ready"
  4. Each file gets a context_id (e.g., ctx_abc123...) that you can copy

The Knowledge section provides separate Add photos and Add files controls so iPhone photo-library selection uses the native image picker. The dashboard uploads at most three files concurrently. If part of a batch fails, successful files remain stored and failed files can be retried individually or together. Confirmed images and videos are retained in R2 until explicitly deleted; HEIC/HEIF uploads are converted to a durable JPEG, and iPhone MOV uploads to browser-compatible MP4.

Using context_ids in API Calls

By default, all app context is searched for every API call. To limit search to specific files, pass context_ids:

SDK Example

const response = await legion.chat(
  [{ role: 'user', content: 'What does our return policy say?' }],
  {
    model: 'gemini-2.5-flash',
    context_ids: ['ctx_abc123def456', 'ctx_789xyz'],
  }
);

Direct API Example

body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'What does our return policy say?' }],
  context_ids: ['ctx_abc123def456'],
})

How It Works

  • Files are chunked (~1000 tokens with overlap) and embedded using Gemini Embedding 2
  • At ingest, each document also gets a one-time synopsis, and each chunk gets a short situating preamble written with the whole document in view (contextual retrieval). The preamble is embedded and keyword-indexed together with the chunk, so a chunk can be found by document-level terms it never contains
  • At query time, the user's message is embedded and used for hybrid search (pgvector cosine + tsvector keyword)
  • The top 5 matching chunks are then expanded into their neighbours (the chunks immediately before and after) and stitched back into document order, so long-form content reads linearly instead of arriving as out-of-order fragments
  • The result is injected into the system message as "[App Knowledge Base]": one header per document carrying its synopsis, then each contiguous run labelled with its position (for example "chunks 13–15 of 32"), with "…" marking a jump between windows
  • Results are cached for 60 seconds
  • If no context exists for the app, the search is skipped entirely (zero latency impact)

Video Knowledge and Clips

Long videos are described and divided at semantic scene boundaries into child clips, normally 15–60 seconds each. Each ready clip is independently searchable and can be shown in chat; the original parent is retained for its summary/transcript, future reprocessing, grouping, and one-click deletion, but is excluded from video media search once clips are ready. Short videos remain ordinary unsplit items. If semantic description or splitting fails, the original video still completes through the existing embedding path.

The owner-authenticated context list/detail responses include item_id, parent_item_id, segment_start_s, segment_end_s, and metadata. Child clips have their own context_id. Deleting a parent context item deletes its child rows and stored clip objects as well.

Image Metadata

Uploaded images get metadata extracted automatically during processing, at no extra latency:

  • Technical: width, height, format, orientation, color space, and EXIF (capture time, camera, lens, ISO, GPS) when present
  • Semantic: a caption, tags, any text visible in the image (OCR), notable objects, and dominant colors

The caption and OCR text are also indexed, so images are searchable by what they contain — not just by filename — and the retrieved context describes the image to the model. Extraction is best-effort: if it fails, the image is still embedded and marked ready, with metadata null.

Programmatic Upload (via API)

POST /v1/apps/:clientId/context/upload-url — get presigned upload URL POST /v1/apps/:clientId/context/confirm — verify the uploaded R2 object and idempotently create one durable processing job GET /v1/apps/:clientId/context — list all context items (includes metadata and video parent/segment fields) GET /v1/apps/:clientId/context/:contextId — get item status, metadata, and video parent/segment fields DELETE /v1/apps/:clientId/context/:contextId — delete item, chunks, and any child video clips

Upload lifecycle statuses are pending_upload (presigned but not confirmed), queued, processing, ready, failed, and deleting; list/detail responses include upload_expires_at while it applies. Unconfirmed uploads expire after two hours and are cleaned automatically. Confirm returns queued the first time and the current status on safe repeats; it returns 409 upload_not_found if the expected object is absent. Deletion is complete only after every retained parent, converted-original, child-clip, and preview object has been removed from storage. A temporary R2 failure returns 503 storage_deletion_pending with retry_scheduled: true while automatic reconciliation retains the storage keys and database rows.

The metadata field on a context item looks like:

{
  technical: { width: 1920, height: 1080, format: 'jpeg', exif: { captured_at, camera, gps } },
  semantic: { subject, caption, tags, ocr_text, objects, dominant_colors },
  extracted_at: '2026-08-02T00:00:00.000Z',
  extractor_version: 2
}
For videos, technical metadata includes duration, dimensions, frame rate, codecs, bitrate, and container; semantic metadata can include summary, scenes, transcript_excerpt, tags, and on_screen_text. Metadata is null when extraction produced nothing and for older items uploaded before extraction was enabled.

View plain text
Give your app domain-specific knowledge by uploading files. Content is automatically chunked, embedded, and retrieved at query time via hybrid search (semantic + keyword with Reciprocal Rank Fusion).

### Supported File Types
- PDF (.pdf)
- Word (.docx)
- PowerPoint (.pptx)
- Video (.mp4, .mov) — described, embedded, and automatically split into searchable 15–60 second clips when long enough; MOV uploads are converted to MP4 for browser playback
- Audio (.mp3) — embedded as single unit
- Images (.png, .jpg, .webp, .gif, .svg, .heic, .heif) — embedded as single unit; HEIC/HEIF is converted to JPEG during processing

### Upload Flow
1. Go to your app's detail page in the developer dashboard
2. Select or drag and drop up to 50 local files (500 MB total per batch, 100 MB per file) into the Knowledge Base section
3. Wait for status to change from "processing" to "ready"
4. Each file gets a context_id (e.g., ctx_abc123...) that you can copy

The Knowledge section provides separate Add photos and Add files controls so iPhone photo-library selection uses the native image picker. The dashboard uploads at most three files concurrently. If part of a batch fails, successful files remain stored and failed files can be retried individually or together. Confirmed images and videos are retained in R2 until explicitly deleted; HEIC/HEIF uploads are converted to a durable JPEG, and iPhone MOV uploads to browser-compatible MP4.

### Using context_ids in API Calls
By default, all app context is searched for every API call. To limit search to specific files, pass context_ids:

#### SDK Example
const response = await legion.chat(
  [{ role: 'user', content: 'What does our return policy say?' }],
  {
    model: 'gemini-2.5-flash',
    context_ids: ['ctx_abc123def456', 'ctx_789xyz'],
  }
);

#### Direct API Example
body: JSON.stringify({
  model: 'gemini-2.5-flash',
  messages: [{ role: 'user', content: 'What does our return policy say?' }],
  context_ids: ['ctx_abc123def456'],
})

### How It Works
- Files are chunked (~1000 tokens with overlap) and embedded using Gemini Embedding 2
- At ingest, each document also gets a one-time synopsis, and each chunk gets a short situating preamble written with the whole document in view (contextual retrieval). The preamble is embedded and keyword-indexed together with the chunk, so a chunk can be found by document-level terms it never contains
- At query time, the user's message is embedded and used for hybrid search (pgvector cosine + tsvector keyword)
- The top 5 matching chunks are then expanded into their neighbours (the chunks immediately before and after) and stitched back into document order, so long-form content reads linearly instead of arriving as out-of-order fragments
- The result is injected into the system message as "[App Knowledge Base]": one header per document carrying its synopsis, then each contiguous run labelled with its position (for example "chunks 13–15 of 32"), with "…" marking a jump between windows
- Results are cached for 60 seconds
- If no context exists for the app, the search is skipped entirely (zero latency impact)

### Video Knowledge and Clips
Long videos are described and divided at semantic scene boundaries into child clips, normally 15–60 seconds each. Each ready clip is independently searchable and can be shown in chat; the original parent is retained for its summary/transcript, future reprocessing, grouping, and one-click deletion, but is excluded from video media search once clips are ready. Short videos remain ordinary unsplit items. If semantic description or splitting fails, the original video still completes through the existing embedding path.

The owner-authenticated context list/detail responses include item_id, parent_item_id, segment_start_s, segment_end_s, and metadata. Child clips have their own context_id. Deleting a parent context item deletes its child rows and stored clip objects as well.

### Image Metadata
Uploaded images get metadata extracted automatically during processing, at no extra latency:
- Technical: width, height, format, orientation, color space, and EXIF (capture time, camera, lens, ISO, GPS) when present
- Semantic: a caption, tags, any text visible in the image (OCR), notable objects, and dominant colors

The caption and OCR text are also indexed, so images are searchable by what they contain — not just by filename — and the retrieved context describes the image to the model. Extraction is best-effort: if it fails, the image is still embedded and marked ready, with metadata null.

### Programmatic Upload (via API)
POST /v1/apps/:clientId/context/upload-url — get presigned upload URL
POST /v1/apps/:clientId/context/confirm — verify the uploaded R2 object and idempotently create one durable processing job
GET  /v1/apps/:clientId/context — list all context items (includes metadata and video parent/segment fields)
GET  /v1/apps/:clientId/context/:contextId — get item status, metadata, and video parent/segment fields
DELETE /v1/apps/:clientId/context/:contextId — delete item, chunks, and any child video clips

Upload lifecycle statuses are pending_upload (presigned but not confirmed), queued, processing, ready, failed, and deleting; list/detail responses include upload_expires_at while it applies. Unconfirmed uploads expire after two hours and are cleaned automatically. Confirm returns queued the first time and the current status on safe repeats; it returns 409 upload_not_found if the expected object is absent. Deletion is complete only after every retained parent, converted-original, child-clip, and preview object has been removed from storage. A temporary R2 failure returns 503 storage_deletion_pending with retry_scheduled: true while automatic reconciliation retains the storage keys and database rows.

The metadata field on a context item looks like:
{
  technical: { width: 1920, height: 1080, format: 'jpeg', exif: { captured_at, camera, gps } },
  semantic: { subject, caption, tags, ocr_text, objects, dominant_colors },
  extracted_at: '2026-08-02T00:00:00.000Z',
  extractor_version: 2
}
For videos, technical metadata includes duration, dimensions, frame rate, codecs, bitrate, and container; semantic metadata can include summary, scenes, transcript_excerpt, tags, and on_screen_text. Metadata is null when extraction produced nothing and for older items uploaded before extraction was enabled.

---

SDK Quick Start

Install the SDK

npm install @legionai/sdk

Basic Usage

import { createLegion } from '@legionai/sdk';
const legion = createLegion({
  clientId: 'YOUR_CLIENT_ID',
  // Optional: callback when tokens are auto-refreshed (persist new tokens)
  onTokenRefresh: (tokens) => saveTokens(tokens),
});
// Connect user's credits (redirectUri is passed here, not in createLegion)
legion.connect({ redirectUri: 'https://yourapp.com/callback' });
// After user returns to your callback, exchange code for tokens
const state = new URLSearchParams(window.location.search).get('state');
const tokens = await legion.exchangeCode(code, { state });
// tokens = { access_token, refresh_token, token_type, expires_in }
// Make API calls (uses your app default model if set; otherwise gemini-2.5-flash during launch)
const response = await legion.chat([
  { role: 'user', content: 'Hello!' }
]);
// Specify a different active launch model
const response2 = await legion.chat(
  [{ role: 'user', content: 'Hello!' }],
  { model: 'gemini-2.5-pro' }
);

View plain text
### Install the SDK
npm install @legionai/sdk

### Basic Usage
import { createLegion } from '@legionai/sdk';

const legion = createLegion({
  clientId: 'YOUR_CLIENT_ID',
  // Optional: callback when tokens are auto-refreshed (persist new tokens)
  onTokenRefresh: (tokens) => saveTokens(tokens),
});

// Connect user's credits (redirectUri is passed here, not in createLegion)
legion.connect({ redirectUri: 'https://yourapp.com/callback' });

// After user returns to your callback, exchange code for tokens
const state = new URLSearchParams(window.location.search).get('state');
const tokens = await legion.exchangeCode(code, { state });
// tokens = { access_token, refresh_token, token_type, expires_in }

// Make API calls (uses your app default model if set; otherwise gemini-2.5-flash during launch)
const response = await legion.chat([
  { role: 'user', content: 'Hello!' }
]);

// Specify a different active launch model
const response2 = await legion.chat(
  [{ role: 'user', content: 'Hello!' }],
  { model: 'gemini-2.5-pro' }
);

---

Next.js Quick Start (@legionai/next)

Drop-in OAuth integration for Next.js App Router apps. Handles the full connect flow, stores tokens in httpOnly cookies, and provides React hooks for connection state.

Install

npm install @legionai/next @legionai/sdk

1. Route Handler — app/api/auth/[...legion]/route.js

import { createLegionHandler } from '@legionai/next';
export const { GET, POST } = createLegionHandler({
  clientId: process.env.LEGION_CLIENT_ID,
  afterCallback: '/dashboard', // where to redirect after OAuth
});
// @legionai/next always uses S256 PKCE. Do not add a client secret here.
// The generated callback URL must still be registered as a redirect URI.

2. Layout — app/layout.js

import { LegionProvider } from '@legionai/next';
export default function Layout({ children }) {
  return <html><body><LegionProvider>{children}</LegionProvider></body></html>;
}

3. Connect Button — any client component

'use client';

import { useLegion } from '@legionai/next';
export function ConnectButton() {
  const { connected, loading, connect, disconnect } = useLegion();
  if (loading) return null;
  return (
    <button onClick={connected ? disconnect : connect}>
      {connected ? 'Disconnect' : 'Connect Legion'}
    </button>
  );
}

4. Custom API Route — app/api/chat/route.js

import { getLegionSession } from '@legionai/next';
export async function POST(request) {
  const session = getLegionSession(request);
  if (!session) return Response.json({ error: 'Not connected' }, { status: 401 });
  const { messages } = await request.json();
  const res = await fetch('https://api.legion-ai.org/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${session.accessToken}`,
    },
    body: JSON.stringify({ model: 'gemini-2.5-flash', messages }),
  });
  return Response.json(await res.json());
}

Handler Config Options

  • clientId (required): Your Legion app client ID
  • afterCallback (default: '/'): Redirect path after successful OAuth
  • basePath (default: '/api/auth/legion'): Route prefix for the catch-all handler
  • cookiePrefix (default: 'legion'): Prefix for cookie names
  • scope (default: 'chat:completions'): OAuth scope
  • authUrl (optional): Override the Legion auth server URL
  • consentUrl (optional): Override the hosted Legion consent URL

How It Works

The catch-all route handles four endpoints automatically:

  • GET /api/auth/legion/connect — Redirects to Legion consent page
  • GET /api/auth/legion/callback — Exchanges code for tokens, sets cookies
  • GET /api/auth/legion/status — Returns { connected: true/false }
  • POST /api/auth/legion/disconnect — Clears cookies

Tokens are stored in httpOnly cookies (never exposed to client JavaScript). The React hook only reflects connection state — it does not access tokens directly.


View plain text
Drop-in OAuth integration for Next.js App Router apps. Handles the full connect flow, stores tokens in httpOnly cookies, and provides React hooks for connection state.

### Install
npm install @legionai/next @legionai/sdk

### 1. Route Handler — app/api/auth/[...legion]/route.js
import { createLegionHandler } from '@legionai/next';

export const { GET, POST } = createLegionHandler({
  clientId: process.env.LEGION_CLIENT_ID,
  afterCallback: '/dashboard', // where to redirect after OAuth
});

// @legionai/next always uses S256 PKCE. Do not add a client secret here.
// The generated callback URL must still be registered as a redirect URI.

### 2. Layout — app/layout.js
import { LegionProvider } from '@legionai/next';

export default function Layout({ children }) {
  return <html><body><LegionProvider>{children}</LegionProvider></body></html>;
}

### 3. Connect Button — any client component
'use client';
import { useLegion } from '@legionai/next';

export function ConnectButton() {
  const { connected, loading, connect, disconnect } = useLegion();
  if (loading) return null;
  return (
    <button onClick={connected ? disconnect : connect}>
      {connected ? 'Disconnect' : 'Connect Legion'}
    </button>
  );
}

### 4. Custom API Route — app/api/chat/route.js
import { getLegionSession } from '@legionai/next';

export async function POST(request) {
  const session = getLegionSession(request);
  if (!session) return Response.json({ error: 'Not connected' }, { status: 401 });

  const { messages } = await request.json();
  const res = await fetch('https://api.legion-ai.org/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${session.accessToken}`,
    },
    body: JSON.stringify({ model: 'gemini-2.5-flash', messages }),
  });
  return Response.json(await res.json());
}

### Handler Config Options
- clientId (required): Your Legion app client ID
- afterCallback (default: '/'): Redirect path after successful OAuth
- basePath (default: '/api/auth/legion'): Route prefix for the catch-all handler
- cookiePrefix (default: 'legion'): Prefix for cookie names
- scope (default: 'chat:completions'): OAuth scope
- authUrl (optional): Override the Legion auth server URL
- consentUrl (optional): Override the hosted Legion consent URL

### How It Works
The catch-all route handles four endpoints automatically:
- GET /api/auth/legion/connect — Redirects to Legion consent page
- GET /api/auth/legion/callback — Exchanges code for tokens, sets cookies
- GET /api/auth/legion/status — Returns { connected: true/false }
- POST /api/auth/legion/disconnect — Clears cookies

Tokens are stored in httpOnly cookies (never exposed to client JavaScript). The React hook only reflects connection state — it does not access tokens directly.

---

Authentication

Token Lifecycle

  • Access tokens expire after 24 hours (86400 seconds)
  • The SDK automatically refreshes tokens on 401 errors using the refresh token
  • When tokens are refreshed, the onTokenRefresh callback is called so you can persist the new tokens
  • If refresh fails, the user must re-authorize via connect()
  • Refresh tokens are single-use — each refresh call returns a new refresh token (rotation)

Manual Token Refresh (Without SDK)

If you're not using the SDK, refresh tokens manually:

const res = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: 'YOUR_REFRESH_TOKEN',
  }),
});
const { access_token, refresh_token, expires_in } = await res.json();
// IMPORTANT: Save the new refresh_token — it replaces the old one (single-use rotation)
// client_id is not required for refresh requests

Restoring Sessions

// Load saved tokens (both access and refresh tokens)
legion.setTokens({
  accessToken: saved.access_token,
  refreshToken: saved.refresh_token,
});

View plain text
### Token Lifecycle
- Access tokens expire after 24 hours (86400 seconds)
- The SDK automatically refreshes tokens on 401 errors using the refresh token
- When tokens are refreshed, the onTokenRefresh callback is called so you can persist the new tokens
- If refresh fails, the user must re-authorize via connect()
- Refresh tokens are single-use — each refresh call returns a new refresh token (rotation)

### Manual Token Refresh (Without SDK)
If you're not using the SDK, refresh tokens manually:

const res = await fetch('https://auth.legion-ai.org/v1/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'refresh_token',
    refresh_token: 'YOUR_REFRESH_TOKEN',
  }),
});
const { access_token, refresh_token, expires_in } = await res.json();
// IMPORTANT: Save the new refresh_token — it replaces the old one (single-use rotation)
// client_id is not required for refresh requests

### Restoring Sessions
// Load saved tokens (both access and refresh tokens)
legion.setTokens({
  accessToken: saved.access_token,
  refreshToken: saved.refresh_token,
});

---

Error Handling

StatusCodeAction
401invalid_tokenToken expired. SDK auto-refreshes; if that fails, call connect()
403connection_inactiveUser deactivated. Call connect() to reauthorize
402limit_exceededCall connect({ authorizationIntent: 'increase_cap' })
402payment_requiredCall connect({ authorizationIntent: 'payment_method' })

Recommended Pattern

For any authentication or connection error, call connect() to restart the OAuth flow:

try {
  const response = await legion.chat(messages);
} catch (error) {
  if (error.requiresReauth() || error.isDeactivated()) {
    // Reconnect - this handles reauth and reactivates deactivated connections
    legion.connect({ redirectUri: 'https://yourapp.com/callback' });
  } else if (error.isCapError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'increase_cap',
    });
  } else if (error.isPaymentMethodError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'payment_method',
    });
  }
}

View plain text
Status | Code | Action
401 | invalid_token | Token expired. SDK auto-refreshes; if that fails, call connect()
403 | connection_inactive | User deactivated. Call connect() to reauthorize
402 | limit_exceeded | Call connect({ authorizationIntent: 'increase_cap' })
402 | payment_required | Call connect({ authorizationIntent: 'payment_method' })

### Recommended Pattern
For any authentication or connection error, call connect() to restart the OAuth flow:

try {
  const response = await legion.chat(messages);
} catch (error) {
  if (error.requiresReauth() || error.isDeactivated()) {
    // Reconnect - this handles reauth and reactivates deactivated connections
    legion.connect({ redirectUri: 'https://yourapp.com/callback' });
  } else if (error.isCapError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'increase_cap',
    });
  } else if (error.isPaymentMethodError()) {
    legion.connect({
      redirectUri: 'https://yourapp.com/callback',
      authorizationIntent: 'payment_method',
    });
  }
}

---

Connection States

Active: Normal operation. Make API calls freely. Inactive: User deactivated. Call connect() - reauthorization automatically reactivates. Revoked: Permanently disconnected. Call connect() to start fresh.


View plain text
Active: Normal operation. Make API calls freely.
Inactive: User deactivated. Call connect() - reauthorization automatically reactivates.
Revoked: Permanently disconnected. Call connect() to start fresh.

---

API Reference

createLegion(config)

legion.connect(options)

  • Start OAuth flow (redirects user to consent page).
  • Options: redirectUri, state, scope (default: 'chat:completions'), popup, authorizationIntent (authorize, increase_cap, payment_method)

legion.exchangeCode(code, { state })

  • Exchange authorization code for tokens.
  • Public clients retrieve the PKCE verifier associated with the returned state and send it as code_verifier.
  • Token exchange sends grant_type, code, client_id, and code_verifier. It does not send redirect_uri or client_secret.
  • Sending only grant_type, code, and client_id is not sufficient for public-client codes created with PKCE.
  • Advanced backend clients may instead configure clientSecret and exchange a code issued without PKCE.
  • Returns: { access_token, refresh_token, token_type, expires_in }

legion.setTokens({ accessToken, refreshToken })

  • Set tokens directly (e.g., from storage).

legion.chat(messages, options)

  • Make chat completion request.
  • Default model: your app default if set, otherwise gemini-2.5-flash during launch. Auto-refreshes token on 401.
  • Options: model, temperature, max_tokens, autoRefresh (default: true), user_preferences, user_information, user_context, context_ids, platform_funded

legion.refreshAccessToken()

  • Manually refresh access token using refresh token.
  • Calls onTokenRefresh callback with new tokens.

legion.isConnected()

  • Returns true if SDK has an access token.

legion.disconnect()

  • Clear all tokens from SDK.

View plain text
createLegion(config)
- Create a Legion SDK instance.
- Config: clientId, authUrl (default: https://auth.legion-ai.org), connectUrl (default: https://legion-ai.org/connect), apiUrl (default: https://api.legion-ai.org), onTokenRefresh

legion.connect(options)
- Start OAuth flow (redirects user to consent page).
- Options: redirectUri, state, scope (default: 'chat:completions'), popup, authorizationIntent (authorize, increase_cap, payment_method)

legion.exchangeCode(code, { state })
- Exchange authorization code for tokens.
- Public clients retrieve the PKCE verifier associated with the returned state and send it as code_verifier.
- Token exchange sends grant_type, code, client_id, and code_verifier. It does not send redirect_uri or client_secret.
- Sending only grant_type, code, and client_id is not sufficient for public-client codes created with PKCE.
- Advanced backend clients may instead configure clientSecret and exchange a code issued without PKCE.
- Returns: { access_token, refresh_token, token_type, expires_in }

legion.setTokens({ accessToken, refreshToken })
- Set tokens directly (e.g., from storage).

legion.chat(messages, options)
- Make chat completion request.
- Default model: your app default if set, otherwise gemini-2.5-flash during launch. Auto-refreshes token on 401.
- Options: model, temperature, max_tokens, autoRefresh (default: true), user_preferences, user_information, user_context, context_ids, platform_funded

legion.refreshAccessToken()
- Manually refresh access token using refresh token.
- Calls onTokenRefresh callback with new tokens.

legion.isConnected()
- Returns true if SDK has an access token.

legion.disconnect()
- Clear all tokens from SDK.

---

LegionError

All SDK errors are instances of LegionError with these properties:

  • error.message: Human-readable message
  • error.code: Error code (e.g., 'invalid_token')
  • error.status: HTTP status code
  • error.action: Financial remediation action when supplied
  • error.currentSpendCents / error.currentCapCents: Server-authoritative cap context for limit_exceeded
  • error.metadata: Raw structured API error payload

Helper Methods

  • error.requiresReauth() - Returns true if user needs to reconnect
  • error.isDeactivated() - Returns true if user deactivated the connection
  • error.isBillingError() - Backward-compatible check for any HTTP 402
  • error.isCapError() - Returns true when increase_cap authorization is required
  • error.isPaymentMethodError() - Returns true when payment_method authorization is required

View plain text
All SDK errors are instances of LegionError with these properties:
- error.message: Human-readable message
- error.code: Error code (e.g., 'invalid_token')
- error.status: HTTP status code
- error.action: Financial remediation action when supplied
- error.currentSpendCents / error.currentCapCents: Server-authoritative cap context for limit_exceeded
- error.metadata: Raw structured API error payload

Helper Methods
- error.requiresReauth() - Returns true if user needs to reconnect
- error.isDeactivated() - Returns true if user deactivated the connection
- error.isBillingError() - Backward-compatible check for any HTTP 402
- error.isCapError() - Returns true when increase_cap authorization is required
- error.isPaymentMethodError() - Returns true when payment_method authorization is required

---

AI Agent Quick Start

End-to-end flow for AI agents integrating a new app with Legion:

1. Setup

npm install -g @legionai/cli
legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --margin 50

Accredited partners creating apps for clients should first run:

legion partner-claim

2. Save Credentials

The create-app output includes a public client_id and a one-time client_secret. Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id, then use a registered redirect URI plus S256 PKCE during authorization. Save client_secret only when building an advanced confidential backend OAuth flow.

3. Integrate

npm install @legionai/sdk
Then use createLegion({ clientId }) in your app. See "SDK Quick Start" above for full usage.

4. What Happens by Default on Every API Call

When your app makes a POST /v1/chat/completions call, Legion automatically:

  • Injects user context: user_preferences, user_information, and user_context all default to true. The user's AI preferences, demographics, and query patterns are added to the system message.
  • Searches app knowledge base (RAG): If you've uploaded files (PDF, DOCX, PPTX, video, audio) to your app's knowledge base via the dashboard, every call automatically searches this content. The top 5 matching chunks, their immediate neighbours, and each source document's synopsis are injected as [App Knowledge Base] in the system message, grouped per document and in document order. If no files are uploaded, this is skipped (zero latency).
  • Applies owner margin: Your configured markup on model costs (default 50%). Legion separately adds a fixed 20% fee on model cost. You keep 100% of the owner margin.
  • Applies partner attribution: if your app was registered with an accredited partner slug, Legion attributes the partner share automatically.
  • Routes to the active launch provider: all responses are OpenAI-compatible; during launch the active provider catalog is Google-only.

5. How to Control Defaults

  • Opt out of user context: set user_preferences: false, user_information: false, or user_context: false in the request body
  • Limit RAG search to specific files: pass context_ids: ['ctx_abc123'] in the request body
  • Change margin: legion update-app --client-id <id> --margin <pct>
  • Change default model: configure in the dashboard, or pass model in each request
  • Invite a non-technical client owner for payouts: legion invite-owner <client_id> --email client@example.com

6. View Full Docs

Run "legion docs" to print the complete documentation in your terminal.

7. (Optional) Connect Railway Log Source

legion connect-source --name "my-api" --project-id <id> --environment-id <id> --project-token <token>
View plain text
End-to-end flow for AI agents integrating a new app with Legion:

### 1. Setup
npm install -g @legionai/cli
legion login
legion create-app --name "My App" --redirect-uris "https://myapp.com/callback" --margin 50

Accredited partners creating apps for clients should first run:
legion partner-claim

### 2. Save Credentials
The create-app output includes a public client_id and a one-time client_secret.
Standard SDK, Next.js, WordPress, and Drupal integrations configure only client_id, then use a registered redirect URI plus S256 PKCE during authorization.
Save client_secret only when building an advanced confidential backend OAuth flow.

### 3. Integrate
npm install @legionai/sdk
Then use createLegion({ clientId }) in your app. See "SDK Quick Start" above for full usage.

### 4. What Happens by Default on Every API Call
When your app makes a POST /v1/chat/completions call, Legion automatically:
- Injects user context: user_preferences, user_information, and user_context all default to true. The user's AI preferences, demographics, and query patterns are added to the system message.
- Searches app knowledge base (RAG): If you've uploaded files (PDF, DOCX, PPTX, video, audio) to your app's knowledge base via the dashboard, every call automatically searches this content. The top 5 matching chunks, their immediate neighbours, and each source document's synopsis are injected as [App Knowledge Base] in the system message, grouped per document and in document order. If no files are uploaded, this is skipped (zero latency).
- Applies owner margin: Your configured markup on model costs (default 50%). Legion separately adds a fixed 20% fee on model cost. You keep 100% of the owner margin.
- Applies partner attribution: if your app was registered with an accredited partner slug, Legion attributes the partner share automatically.
- Routes to the active launch provider: all responses are OpenAI-compatible; during launch the active provider catalog is Google-only.

### 5. How to Control Defaults
- Opt out of user context: set user_preferences: false, user_information: false, or user_context: false in the request body
- Limit RAG search to specific files: pass context_ids: ['ctx_abc123'] in the request body
- Change margin: legion update-app --client-id <id> --margin <pct>
- Change default model: configure in the dashboard, or pass model in each request
- Invite a non-technical client owner for payouts: legion invite-owner <client_id> --email client@example.com

### 6. View Full Docs
Run "legion docs" to print the complete documentation in your terminal.

### 7. (Optional) Connect Railway Log Source
legion connect-source --name "my-api" --project-id <id> --environment-id <id> --project-token <token>

MCP Server (AI Agents)

Manage apps and access deployment logs from AI agents like Claude Code, Cursor, or Windsurf.

Transport: stdio (the MCP server communicates over stdin/stdout)

Quick Setup

  1. Install CLI: npm install -g @legionai/cli
  2. Install MCP server: npm install -g @legionai/mcp
  3. Login: legion login (opens browser, saves token to ~/.legion/config.json)
  4. Add the config below to your editor's MCP settings file
  5. Restart your editor / reload MCP servers

Installation

npm install -g @legionai/cli    # CLI commands (login, app management, log sources)
npm install -g @legionai/mcp    # MCP server (for AI agent integration)

Configuration (Login Required)

Generate your MCP access token in the logged-in dashboard: Developer > Documentation > MCP Server > Get MCP Config

Claude Code (~/.config/claude/mcp.json)

{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

Cursor (~/.cursor/mcp.json)

{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

Windsurf (~/.windsurf/mcp.json)

{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

Available Tools

OAuth App Management:

  • legion_list_oauth_apps: List all OAuth apps on your account
  • legion_get_oauth_app: Get details of an OAuth app by client ID
  • legion_create_oauth_app: Create a new OAuth app
  • legion_update_oauth_app: Update OAuth app name or redirect URIs
  • legion_delete_oauth_app: Delete an OAuth app (revokes all connections)
  • legion_rotate_app_secret: Rotate an OAuth app's client secret (new secret shown once)
  • legion_get_oauth_app_stats: Get usage stats (users, tokens, earnings)
  • legion_get_oauth_app_transactions: Get transaction history (prompts, costs)

API Documentation:

  • legion_get_api_docs: Get full API docs (request format, models, errors)

Deployment Logs (Railway):

  • legion_list_railway_sources: List connected Railway log sources
  • legion_connect_railway_source: Connect a Railway deployment as a log source
  • legion_get_deployment_logs: Fetch deployment logs with filters (level, limit)
  • legion_get_deployment_errors: Fetch error-level deployment logs only
  • legion_disconnect_railway_source: Remove a Railway log source connection

CLI Equivalents (install @legionai/cli)

All MCP tools have CLI counterparts. Examples:

  legion list-apps
  legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
  legion get-app lgn_abc123
  legion rotate-secret lgn_abc123
  legion app-stats lgn_abc123
  legion partner-claim
  legion partner-status
  legion partner-connect-start
  legion partner-connect-status
  legion invite-owner lgn_abc123 --email client@example.com
  legion cancel-owner-invite lgn_abc123
  legion list-sources
  legion get-logs --source-id conn_xyz
  legion get-errors --source-id conn_xyz

Add --verbose or --debug to any command for HTTP request/response details:

  legion list-apps --verbose

Run "legion --help" for the full command reference.

Example Prompts

  • "List my OAuth apps and their stats"
  • "Create a new OAuth app called my-saas with redirect URI https://myapp.com/callback"
  • "Check my deployment logs for errors"
  • "Connect my Railway deployment for debugging"

View plain text
Manage apps and access deployment logs from AI agents like Claude Code, Cursor, or Windsurf.

Transport: stdio (the MCP server communicates over stdin/stdout)

### Quick Setup
1. Install CLI: npm install -g @legionai/cli
2. Install MCP server: npm install -g @legionai/mcp
3. Login: legion login (opens browser, saves token to ~/.legion/config.json)
4. Add the config below to your editor's MCP settings file
5. Restart your editor / reload MCP servers

### Installation
npm install -g @legionai/cli    # CLI commands (login, app management, log sources)
npm install -g @legionai/mcp    # MCP server (for AI agent integration)

### Configuration (Login Required)
Generate your MCP access token in the logged-in dashboard:
Developer > Documentation > MCP Server > Get MCP Config

#### Claude Code (~/.config/claude/mcp.json)
{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

#### Cursor (~/.cursor/mcp.json)
{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

#### Windsurf (~/.windsurf/mcp.json)
{
  "mcpServers": {
    "legion": {
      "command": "legion-mcp",
      "env": {
        "LEGION_AUTH_SERVER_URL": "https://auth.legion-ai.org",
        "LEGION_ACCESS_TOKEN": "<LOGIN_REQUIRED_TOKEN>"
      }
    }
  }
}

### Available Tools

OAuth App Management:
- legion_list_oauth_apps: List all OAuth apps on your account
- legion_get_oauth_app: Get details of an OAuth app by client ID
- legion_create_oauth_app: Create a new OAuth app
- legion_update_oauth_app: Update OAuth app name or redirect URIs
- legion_delete_oauth_app: Delete an OAuth app (revokes all connections)
- legion_rotate_app_secret: Rotate an OAuth app's client secret (new secret shown once)
- legion_get_oauth_app_stats: Get usage stats (users, tokens, earnings)
- legion_get_oauth_app_transactions: Get transaction history (prompts, costs)

API Documentation:
- legion_get_api_docs: Get full API docs (request format, models, errors)

Deployment Logs (Railway):
- legion_list_railway_sources: List connected Railway log sources
- legion_connect_railway_source: Connect a Railway deployment as a log source
- legion_get_deployment_logs: Fetch deployment logs with filters (level, limit)
- legion_get_deployment_errors: Fetch error-level deployment logs only
- legion_disconnect_railway_source: Remove a Railway log source connection

### CLI Equivalents (install @legionai/cli)
All MCP tools have CLI counterparts. Examples:
  legion list-apps
  legion create-app --name "My App" --redirect-uris "https://myapp.com/callback"
  legion get-app lgn_abc123
  legion rotate-secret lgn_abc123
  legion app-stats lgn_abc123
  legion partner-claim
  legion partner-status
  legion partner-connect-start
  legion partner-connect-status
  legion invite-owner lgn_abc123 --email client@example.com
  legion cancel-owner-invite lgn_abc123
  legion list-sources
  legion get-logs --source-id conn_xyz
  legion get-errors --source-id conn_xyz

Add --verbose or --debug to any command for HTTP request/response details:
  legion list-apps --verbose

Run "legion --help" for the full command reference.

### Example Prompts
- "List my OAuth apps and their stats"
- "Create a new OAuth app called my-saas with redirect URI https://myapp.com/callback"
- "Check my deployment logs for errors"
- "Connect my Railway deployment for debugging"

---

CMS Integrations (WordPress & Drupal)

Add AI chat to your WordPress or Drupal site with zero code. Site visitors authorize through Legion before chatting, and you earn your full configured owner margin.

How It Works

  1. Install the Legion plugin on your site
  2. Enter your Client ID in the plugin settings
  3. Site visitors choose Connect to AI before chatting
  4. You earn your full configured owner margin on every API call

Download Plugins

WordPress plugin details, installation instructions, source-build directions, and external-service disclosures are available at https://legion-ai.org/wordpress.

WordPress Plugin

Installation

  1. Install "Legion AI Connect" from the WordPress.org plugin directory
  2. In WordPress Admin: Plugins > Add New and search for Legion AI Connect
  3. Install and activate "Legion AI Connect"

Configuration

  1. Go to Settings > Legion AI
  2. Enter your Client ID (get it from legion-ai.org/developer)
  3. Copy the displayed Redirect URI
  4. Add this Redirect URI to your Legion app settings
  5. Save changes

Always register the exact URI displayed by WordPress. Sites using plain permalinks use /?legion_callback=1; other sites use /legion-callback.

Usage

Add the chat widget using the shortcode: [legion_chat]

Shortcode options:

  • position: bottom-right, bottom-left, button, inline (default: bottom-right)
  • theme: dark, light, auto (default: dark)
  • placeholder: Custom input placeholder text
  • greeting: Custom welcome message
  • accent_color: Hex color for accent elements
  • trigger_text: Text shown for article-button mode
  • context_ids: Comma or space-separated context IDs searched for this widget only
  • context_text: Inline context sent with each request for this widget only; not added to persistent Legion knowledge
  • context_label: Label for inline context (default: Page context)

Example: [legion_chat position="inline" theme="light" placeholder="Ask me anything..."]

Article-button example: [legion_chat position="button" trigger_text="Chat with this article"]

Article-specific context example: [legion_chat position="inline" context_ids="ctx_abc123" context_text="Paste article context here"]

For longer pasted context or text containing quotation marks, put the context between opening and closing shortcode tags: [legion_chat position="inline"]Paste article context here[/legion_chat]

Drupal Module

Installation

  1. Download legion-drupal.zip from the link above
  2. Extract to /modules/contrib/legion_integration
  3. Enable: drush en legion_integration

Configuration

  1. Go to Admin > Configuration > Services > Legion AI
  2. Enter your Client ID
  3. Copy the Redirect URI and add to your Legion app
  4. Save configuration

Usage

  1. Go to Structure > Block Layout
  2. Place the "Legion AI Chat" block in your desired region
  3. Configure block settings (position, theme, placeholder, greeting, accent color, article-button text)

Drupal content can also embed the widget with the built-in Legion AI chat text filter: [legion_chat position="button" trigger_text="Chat with this article"]

Enable the "Legion AI chat shortcode" filter on trusted text formats before using shortcode embeds in content.

Creating a CMS App

When creating your app in the Legion dashboard:

  1. Select "WordPress" or "Drupal" as the app type
  2. WordPress normally suggests /legion-callback; sites using plain permalinks display /?legion_callback=1 instead
  3. Enter the exact Redirect URI displayed on the plugin settings page

Widget Features

  • Automatic session management (tokens stored in localStorage)
  • Same-tab OAuth flow that returns visitors to the originating page
  • Automatic same-tab reauthorization and return when a saved connection is revoked or expires
  • Automatic same-tab increase-cap and payment-method remediation with explicit user approval
  • Animated thinking indicator while an AI response is pending
  • Safe Markdown formatting for assistant responses
  • Light/dark/auto theme support
  • Floating, inline, and compact article-button display modes
  • Responsive design (works on mobile)
  • Auto error handling (silent token refresh, automatic reauthorization, billing alerts)
  • Zero backend required — public client OAuth, no server-side code
  • No default public-facing credit link in the CMS plugins; marketplace front-end credits remain opt-in

Security Notes

  • Uses Public Client OAuth flow (S256 PKCE, no client_secret in PHP)
  • Tokens stored in browser localStorage
  • WordPress and Drupal OAuth state combine a server-verifiable CMS nonce with a per-tab cryptographically random value
  • CMS callbacks verify the nonce before accepting authorization input; the browser verifies the complete state value before exchanging codes
  • Revoked or expired saved connections clear stale tokens and use a same-tab PKCE redirect back to the originating page
  • Strict redirect URI matching enforced
  • CORS enabled for cross-origin requests

View plain text
Add AI chat to your WordPress or Drupal site with zero code. Site visitors authorize through Legion before chatting, and you earn your full configured owner margin.

### How It Works
1. Install the Legion plugin on your site
2. Enter your Client ID in the plugin settings
3. Site visitors choose Connect to AI before chatting
4. You earn your full configured owner margin on every API call

### Download Plugins
- WordPress: https://wordpress.org/plugins/legion-ai-connect/ (requires WordPress 6.3+ / PHP 7.4+)
- Drupal: https://legion-ai.org/downloads/legion-drupal.zip (requires Drupal 10+ / PHP 8.1+)

WordPress plugin details, installation instructions, source-build directions, and external-service disclosures are available at https://legion-ai.org/wordpress.

### WordPress Plugin

#### Installation
1. Install "Legion AI Connect" from the WordPress.org plugin directory
2. In WordPress Admin: Plugins > Add New and search for Legion AI Connect
3. Install and activate "Legion AI Connect"

#### Configuration
1. Go to Settings > Legion AI
2. Enter your Client ID (get it from legion-ai.org/developer)
3. Copy the displayed Redirect URI
4. Add this Redirect URI to your Legion app settings
5. Save changes

Always register the exact URI displayed by WordPress. Sites using plain permalinks use `/?legion_callback=1`; other sites use `/legion-callback`.

#### Usage
Add the chat widget using the shortcode:
`[legion_chat]`

Shortcode options:
- position: bottom-right, bottom-left, button, inline (default: bottom-right)
- theme: dark, light, auto (default: dark)
- placeholder: Custom input placeholder text
- greeting: Custom welcome message
- accent_color: Hex color for accent elements
- trigger_text: Text shown for article-button mode
- context_ids: Comma or space-separated context IDs searched for this widget only
- context_text: Inline context sent with each request for this widget only; not added to persistent Legion knowledge
- context_label: Label for inline context (default: Page context)

Example:
`[legion_chat position="inline" theme="light" placeholder="Ask me anything..."]`

Article-button example:
`[legion_chat position="button" trigger_text="Chat with this article"]`

Article-specific context example:
`[legion_chat position="inline" context_ids="ctx_abc123" context_text="Paste article context here"]`

For longer pasted context or text containing quotation marks, put the context between opening and closing shortcode tags:
`[legion_chat position="inline"]Paste article context here[/legion_chat]`

### Drupal Module

#### Installation
1. Download legion-drupal.zip from the link above
2. Extract to /modules/contrib/legion_integration
3. Enable: drush en legion_integration

#### Configuration
1. Go to Admin > Configuration > Services > Legion AI
2. Enter your Client ID
3. Copy the Redirect URI and add to your Legion app
4. Save configuration

#### Usage
1. Go to Structure > Block Layout
2. Place the "Legion AI Chat" block in your desired region
3. Configure block settings (position, theme, placeholder, greeting, accent color, article-button text)

Drupal content can also embed the widget with the built-in Legion AI chat text filter:
`[legion_chat position="button" trigger_text="Chat with this article"]`

Enable the "Legion AI chat shortcode" filter on trusted text formats before using shortcode embeds in content.

### Creating a CMS App

When creating your app in the Legion dashboard:
1. Select "WordPress" or "Drupal" as the app type
2. WordPress normally suggests /legion-callback; sites using plain permalinks display /?legion_callback=1 instead
3. Enter the exact Redirect URI displayed on the plugin settings page

### Widget Features
- Automatic session management (tokens stored in localStorage)
- Same-tab OAuth flow that returns visitors to the originating page
- Automatic same-tab reauthorization and return when a saved connection is revoked or expires
- Automatic same-tab increase-cap and payment-method remediation with explicit user approval
- Animated thinking indicator while an AI response is pending
- Safe Markdown formatting for assistant responses
- Light/dark/auto theme support
- Floating, inline, and compact article-button display modes
- Responsive design (works on mobile)
- Auto error handling (silent token refresh, automatic reauthorization, billing alerts)
- Zero backend required — public client OAuth, no server-side code
- No default public-facing credit link in the CMS plugins; marketplace front-end credits remain opt-in

### Security Notes
- Uses Public Client OAuth flow (S256 PKCE, no client_secret in PHP)
- Tokens stored in browser localStorage
- WordPress and Drupal OAuth state combine a server-verifiable CMS nonce with a per-tab cryptographically random value
- CMS callbacks verify the nonce before accepting authorization input; the browser verifies the complete state value before exchanging codes
- Revoked or expired saved connections clear stale tokens and use a same-tab PKCE redirect back to the originating page
- Strict redirect URI matching enforced
- CORS enabled for cross-origin requests

---

Chat AI (Hosted Chat Apps)

Create a hosted chat app on Legion — no SDK integration needed. Users chat directly at https://legion-ai.org/chat/:slug and authorize it through Legion's standard consent screen on first use.

Creating a Chat App

  1. Go to https://legion-ai.org/developer and click "Create a Chat AI"
  2. Or use the public registration page at https://legion-ai.org/register-chat
  3. Configure a system prompt in the app detail view
  4. In Admin → Appearance, optionally upload a chat icon/background and choose the font and text color used by conversation messages
  5. Share the chat link: https://legion-ai.org/chat/your-app-name

Chat apps use app_type='chat' and appear with a purple "Chat" badge in the developer dashboard.

Each chat app gets a readable slug derived from its name (for example "Chef Bot" becomes chef-bot; if that slug is taken, a short suffix is added like chef-bot-a7f3). The slug is fixed when the app is created, so renaming the app does not change its URL — the UUID is the permanent link, and keeping the slug stable keeps already-shared links alive. The app's UUID also still works in the URL: any older https://legion-ai.org/chat/UUID link redirects to the slug URL (server-side, preserving the query string), so shared links and social unfurls resolve to the canonical address.

Chat icons and background images are presentation-only uploads associated with the chat and authenticated admin. They are stored separately from knowledge uploads: Legion does not extract metadata, create embeddings, index them for search, or expose them as creator posts. Replacing or removing a managed image also removes the superseded stored object. Icon uploads accept PNG, JPEG, WebP, GIF, or SVG; backgrounds accept PNG, JPEG, WebP, or GIF; both are limited to 5 MB.

Message text colors use six-digit hex values and affect assistant/user message bodies plus assistant media captions; bubbled user messages remain white. When a background image is present, the hosted chat automatically selects a light or dark overlay based on the configured message text color to preserve contrast.

The message-font selector includes System, Serif, Monospace, Rounded, Inter, Roboto, Open Sans, Lato, Nunito, Space Grotesk, Merriweather, Playfair Display, Lora, Pirata One (blackletter Gothic), and League Gothic. The choice affects user and assistant message content only—not chat navigation, prompts, composer controls, or typography elsewhere in Legion.

Chat API Endpoints

All endpoints are under /v1/chat/:appId on the auth server (https://auth.legion-ai.org). The :appId segment accepts either the app's slug or its UUID.

GET /v1/chat/:appId/info

  • Public (no auth required)
  • Returns chat app metadata: name, description, whether a system prompt is configured

POST /v1/chat/:appId/connect

  • Requires Clerk authentication
  • Returns an access token for users with an active app connection
  • Returns a hosted consent URL when the user needs to authorize or re-authorize the chat app

POST /v1/chat/:appId/conversations

  • Requires auth
  • Creates a new conversation, returns conversation ID

GET /v1/chat/:appId/conversations

  • Requires auth
  • Lists the user's conversations for this chat app

GET /v1/chat/:appId/conversations/:id/messages

  • Requires auth
  • Returns the most recent messages for a conversation, in chronological order
  • Optional ?limit (default 200, max 500) — the newest N messages are returned
  • Response: { messages, total, truncated } where truncated is true if the

conversation has more messages than were returned

POST /v1/chat/:appId/conversations/:id/messages

  • Requires auth
  • Saves messages (user + assistant) to a conversation
View plain text
Create a hosted chat app on Legion — no SDK integration needed. Users chat directly at https://legion-ai.org/chat/:slug and authorize it through Legion's standard consent screen on first use.

### Creating a Chat App
1. Go to https://legion-ai.org/developer and click "Create a Chat AI"
2. Or use the public registration page at https://legion-ai.org/register-chat
3. Configure a system prompt in the app detail view
4. In Admin → Appearance, optionally upload a chat icon/background and choose the font and text color used by conversation messages
5. Share the chat link: https://legion-ai.org/chat/your-app-name

Chat apps use app_type='chat' and appear with a purple "Chat" badge in the developer dashboard.

Each chat app gets a readable slug derived from its name (for example "Chef Bot" becomes chef-bot; if that slug is taken, a short suffix is added like chef-bot-a7f3). The slug is fixed when the app is created, so renaming the app does not change its URL — the UUID is the permanent link, and keeping the slug stable keeps already-shared links alive. The app's UUID also still works in the URL: any older https://legion-ai.org/chat/UUID link redirects to the slug URL (server-side, preserving the query string), so shared links and social unfurls resolve to the canonical address.

Chat icons and background images are presentation-only uploads associated with the chat and authenticated admin. They are stored separately from knowledge uploads: Legion does not extract metadata, create embeddings, index them for search, or expose them as creator posts. Replacing or removing a managed image also removes the superseded stored object. Icon uploads accept PNG, JPEG, WebP, GIF, or SVG; backgrounds accept PNG, JPEG, WebP, or GIF; both are limited to 5 MB.

Message text colors use six-digit hex values and affect assistant/user message bodies plus assistant media captions; bubbled user messages remain white. When a background image is present, the hosted chat automatically selects a light or dark overlay based on the configured message text color to preserve contrast.

The message-font selector includes System, Serif, Monospace, Rounded, Inter, Roboto, Open Sans, Lato, Nunito, Space Grotesk, Merriweather, Playfair Display, Lora, Pirata One (blackletter Gothic), and League Gothic. The choice affects user and assistant message content only—not chat navigation, prompts, composer controls, or typography elsewhere in Legion.

### Chat API Endpoints

All endpoints are under /v1/chat/:appId on the auth server (https://auth.legion-ai.org). The :appId segment accepts either the app's slug or its UUID.

GET /v1/chat/:appId/info
- Public (no auth required)
- Returns chat app metadata: name, description, whether a system prompt is configured

POST /v1/chat/:appId/connect
- Requires Clerk authentication
- Returns an access token for users with an active app connection
- Returns a hosted consent URL when the user needs to authorize or re-authorize the chat app

POST /v1/chat/:appId/conversations
- Requires auth
- Creates a new conversation, returns conversation ID

GET /v1/chat/:appId/conversations
- Requires auth
- Lists the user's conversations for this chat app

GET /v1/chat/:appId/conversations/:id/messages
- Requires auth
- Returns the most recent messages for a conversation, in chronological order
- Optional ?limit (default 200, max 500) — the newest N messages are returned
- Response: { messages, total, truncated } where truncated is true if the
  conversation has more messages than were returned

POST /v1/chat/:appId/conversations/:id/messages
- Requires auth
- Saves messages (user + assistant) to a conversation