← TwinForge

Your twin, callable from anywhere.

Everything your twin does in the app — draft in your voice, run structured skills, look things up in your knowledge base and quote your data exactly — is available two ways: a REST API for your own code and automations, and an MCP server that plugs your twin into Claude, ChatGPT, Cursor, and any other MCP-speaking tool. Both need a Pro or Team plan and a linked bring-your-own model key — programmatic generation always runs on your key, so your automation volume is between you and your model provider, with no platform token cap in the way.

How it fits together

Everything programmatic converges on two entry points — REST for your own code, MCP for agent tooling — and everything they produce is grounded in the same three stores the web app uses. Outbound side effects never fire directly: they queue as approvals and only execute after a human decision, which is also what the webhooks narrate.

  YOUR SIDE                        TWINFORGE                            OUT
  ─────────                        ─────────                            ───
  your code / scripts   ─REST──▶   POST /api/v1/generate  ┐
  Zapier / Make / cron  ─REST──▶   (bearer key · rate     │   grounded generation
                                    limits per plan)      ├─▶ voice profile        ─▶ JSON + sources
  Claude / ChatGPT /    ─MCP───▶   /api/mcp               │   knowledge base           (complete, not
  Cursor / your agents             (same auth, as tools)  ┘   data tables (exact $)     streamed)

  Gmail · Drive ──read-only──▶     connectors (Pro/Team)
  Outlook · OneDrive               PII stripped, then stored ─▶ knowledge base

  drafted outbound actions ──▶     approval queue (draft-first,   ─▶ webhooks:
  (email replies, calendar)        a human approves every send)      approval.requested
                                                                     approval.decided

Two consequences worth designing around: generation is synchronous and can be slow (budget up to ~300s for long deliverables), and anything that touches the outside world is asynchronous by construction — you request it, a human approves it, and the approval.decided webhook tells you what happened.

Base URL & conventions

https://twinforge-six.vercel.app/
  • All requests are HTTPS. Bodies and responses are JSON (Content-Type: application/json).
  • Responses are complete, not streamed — you get the full output in one JSON body. Generation can take up to ~300s for long deliverables.
  • The API is versioned under /api/v1. Changes are additive: new response fields may appear; existing ones won't change meaning.
  • Timestamps are ISO-8601 UTC. Token counts are integers.

Authentication

Two keys are involved, and you need both:

  1. A TwinForge API key — create it in Settings → API access. Shown once, stored only as a hash. Send it on every request:
    Authorization: Bearer tk_live_…
    Keys are account-scoped, act as your twin, and can be revoked instantly. Treat them like passwords.
  2. A bring-your-own model key — add one in Settings → Bring your own key for the provider your twin is set to (Anthropic, OpenAI, Google, or Moonshot). All programmatic generation runs on this key. Without it, generate and the MCP generation tools return 403.

Quickstart — a draft in your voice in one call

cURL:

curl -X POST https://twinforge-six.vercel.app//api/v1/generate \
  -H "Authorization: Bearer $TWINFORGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "deliverable",
    "template": "email_reply",
    "input": "Thread: [paste the email thread]. Outcome: decline politely, offer a call in August."
  }'

Node (fetch, 18+):

const res = await fetch("https://twinforge-six.vercel.app//api/v1/generate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TWINFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    mode: "deliverable",
    template: "email_reply",
    input: "Thread: … Outcome: decline politely, offer a call in August.",
  }),
});
const data = await res.json();
console.log(data.output);

Python (requests):

import os, requests

r = requests.post(
    "https://twinforge-six.vercel.app//api/v1/generate",
    headers={"Authorization": f"Bearer {os.environ['TWINFORGE_API_KEY']}"},
    json={
        "mode": "deliverable",
        "template": "email_reply",
        "input": "Thread: … Outcome: decline politely, offer a call in August.",
    },
)
print(r.json()["output"])

Response (all modes share this shape):

{
  "output": "Hi Sarah, …",
  "model": "claude-sonnet-4-6",
  "brain_version": 12,
  "usage": { "input_tokens": 4210, "output_tokens": 187 }
}

POST /api/v1/generate

One endpoint, two modes. Every generation is grounded in your compiled Brain, your knowledge base, and (for data-bound skills) your exact data tables — the same context the app uses.

Request body

FieldTypeDescription
modestring (required)"deliverable" or "chat".
templatestringDeliverable mode only. A skill/template id (see GET /api/v1/templates). Required when mode="deliverable".
inputstringDeliverable mode only. The raw material for the deliverable. Required when mode="deliverable".
messagesarrayChat mode only. [{ "role": "user" | "assistant", "content": string }]. Only the last 40 are used.

Deliverable mode

{ "mode": "deliverable", "template": "strategy_memo", "input": "…raw material…" }

Chat mode

Freeform conversation with your twin (you hold the history; we store none):

{
  "mode": "chat",
  "messages": [
    { "role": "user", "content": "Rewrite this in my voice: …" }
  ]
}

Response (200)

FieldTypeDescription
outputstringThe generated text (markdown).
modelstringThe model that produced it (your BYO provider's model).
brain_versionintegerVersion of your compiled Brain used for this generation.
sourcesstring[]Knowledge sources and data tables the output was grounded in.
usage.input_tokensintegerPrompt tokens billed by your provider.
usage.output_tokensintegerCompletion tokens billed by your provider.

Skills, templates & data

In the app these are called Skills; in the API the id you pass as template is the same thing. There are two kinds, used identically:

  • Built-in — a fixed set shipped with every account: strategy_memo, jira_epics, code_review, board_update, email_reply, linkedin_post, client_proposal, client_update, meeting_followup.
  • Custom — skills you compile in the app from a description or your own documents. They appear in GET /api/v1/templates with "custom": true and are called by their id exactly like a built-in.

A custom skill can be bound to a data table (a price list, rate card, product catalog) in the app. When you call such a skill, those exact figures are injected into the generation automatically — so quotes and pricing come out verbatim from your data, never invented. No extra API parameter is needed; the binding travels with the skill. Skills and data tables are created and managed in the app, not via the API.

Skills with input forms: a skill that shows a form in the app (client, scope, quantity…) accepts plain text over the API and MCP — put the values in input as Label: valuelines, plus any extra context. The skill's input_label describes what it expects:

{
  "mode": "deliverable",
  "template": "fixed_price_quote",
  "input": "Client: Acme Pty Ltd\nScope: supply and install 12 standard doors\nQuantity: 12\nDue date: 2026-08-01\n\nAdditional context: existing frames, ground floor."
}

GET /api/v1/templates

Lists every skill this key can run — built-ins plus your custom skills — with a description and the input each expects.

curl https://twinforge-six.vercel.app//api/v1/templates \
  -H "Authorization: Bearer $TWINFORGE_API_KEY"
{
  "templates": [
    {
      "id": "strategy_memo",
      "name": "Strategy memo",
      "description": "A decision-ready memo: context, options, recommendation, risks.",
      "input_label": "Paste the situation, data points, and any constraints",
      "custom": false
    },
    {
      "id": "fixed_price_quote",
      "name": "Fixed-price quote",
      "description": "An itemised quote grounded in your rate card.",
      "input_label": "The client, the scope, and any quantities",
      "custom": true
    }
  ]
}

GET /api/v1/usage

Current billing-month consumption for the key's account, so your integration can meter itself.

{
  "plan": "pro",
  "period_start": "2026-07-01T00:00:00.000Z",
  "output_tokens_used": 412000,
  "output_token_cap": 6000000,
  "output_tokens_remaining": 5588000,
  "byok_output_tokens": 1200000,
  "rate_limit_per_minute": 30,
  "by_kind": {
    "api": { "requests": 61, "input_tokens": 240000, "output_tokens": 39000 }
  }
}
FieldTypeDescription
planstring"individual" | "pro" | "team".
period_startstringStart of the current billing month (UTC).
output_tokens_usedintegerPlatform-key output tokens used this month.
output_token_capintegerYour plan's monthly platform-key cap.
output_tokens_remainingintegerCap minus used.
byok_output_tokensintegerOutput tokens run on your own key (don't count toward the cap — API/MCP generation is always here).
rate_limit_per_minuteintegerRequests/minute for this plan.
by_kindobjectPer-surface totals: requests, input_tokens, output_tokens (keys: chat, deliverable, api, …).

Rate limits & errors

Requests per minute: 30 on Pro, 60 on Team, shared across the REST API and the MCP server. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 adds Retry-After(seconds). Because generation runs on your own model key, it never consumes your plan's monthly output-token cap — your provider bills the inference directly.

FieldTypeDescription
400Bad requestMalformed body or missing field — the message says what's wrong.
401UnauthorizedMissing, revoked, or wrong-plan API key.
403ForbiddenNo linked BYO model key. Add one in Settings → Bring your own key.
409ConflictYour twin has no Brain or knowledge yet — train it first.
429Too many requestsRate limit or monthly platform cap reached; see Retry-After.
500Server errorGeneration failed transiently — safe to retry.

All errors return { "error": "message" }.

MCP server — your twin inside the AI tools you already use

TwinForge speaks the Model Context Protocol over Streamable HTTP (stateless, JSON-RPC 2.0) at https://twinforge-six.vercel.app//api/mcp. Connect it once and any MCP client gains four tools:

FieldTypeDescription
write_as_twin{ prompt }Freeform drafting in your voice, grounded in your knowledge.
create_deliverable{ template, input }Run a named skill (built-in or custom, incl. data-bound). template enum reflects the account's skills.
list_templates{ }List the skills this twin can run, with descriptions and expected input.
search_knowledge{ query }Retrieve excerpts from the knowledge base. Read-only; no generation.

The tool list is per-twin: your custom skills appear in create_deliverable's template options automatically. Auth is the same tk_live_ bearer key, and calls count against the same rate limit and run on your BYO model key.

Claude Code:

claude mcp add --transport http twinforge https://twinforge-six.vercel.app//api/mcp \
  --header "Authorization: Bearer tk_live_…"

Cursor / other clients (mcp.json):

{
  "mcpServers": {
    "twinforge": {
      "url": "https://twinforge-six.vercel.app//api/mcp",
      "headers": { "Authorization": "Bearer tk_live_…" }
    }
  }
}

Claude Desktop and claude.ai accept the same URL as a custom connector (Settings → Connectors) with the key as a bearer header. The server is read-only with respect to your account: clients can generate drafts and search knowledge, never modify your Brain, skills, or data.

Agent / LLM quick reference & discovery

Two machine-readable entry points let agents consume the API without scraping this page:

  • GET https://twinforge-six.vercel.app//api/v1 — a public, unauthenticated JSON discovery document (endpoints, auth, skills, MCP tools, errors).
  • https://twinforge-six.vercel.app//llms.txt — the same summary as plain text, in the llms.txt convention (shown below verbatim).

Both are generated from the live spec, so they never drift:

# TwinForge API

> Call a Digital Twin — a compiled, owned model of one professional's voice, knowledge, and
> structured data — from code (REST) or any MCP client. This file is a machine-readable summary;
> full docs at https://twinforge-six.vercel.app//developers and a JSON discovery document at https://twinforge-six.vercel.app//api/v1.

BASE: https://twinforge-six.vercel.app/
AUTH: header  Authorization: Bearer tk_live_...   (create in Settings > API access; Pro/Team plan)
      Generation also requires a BYO model key (Settings > Bring your own key), else HTTP 403.
FORMAT: JSON in/out. Responses are complete, not streamed. Up to ~300s per call. Versioning is additive.

## Endpoints

GET /api/v1                      Public JSON discovery document (this API, structured).
POST /api/v1/generate            Generate a deliverable or chat. Auth. Runs on your BYO key.
  deliverable: {"mode":"deliverable","template":"<skill_id>","input":"<text>"}
  chat:        {"mode":"chat","messages":[{"role":"user|assistant","content":"<text>"}]}  (last 40 used)
  200 -> {"output":"<markdown>","model":"<id>","brain_version":<int>,"sources":["<name>",...],
          "usage":{"input_tokens":<int>,"output_tokens":<int>}}
GET /api/v1/templates            List runnable skills. Auth.
  200 -> {"templates":[{"id","name","description","input_label","custom":<bool>}]}
GET /api/v1/usage                Billing-month consumption. Auth.
  200 -> {"plan","period_start","output_tokens_used","output_token_cap",
          "output_tokens_remaining","byok_output_tokens","rate_limit_per_minute","by_kind":{...}}
POST /api/mcp                    MCP server (JSON-RPC 2.0, Streamable HTTP). Same bearer key.
  tools: write_as_twin{prompt} | create_deliverable{template,input}
         | list_templates{} | search_knowledge{query}

## Skills (the "template" id)

Built-in ids: strategy_memo, jira_epics, code_review, board_update, email_reply, linkedin_post, client_proposal, client_update, meeting_followup
Custom skills (custom:true from GET /api/v1/templates) are created in the app and called by id
like built-ins. A skill bound to a data table (price list, rate card) injects those exact figures
into generation automatically — quotes come out verbatim, never invented. No extra parameter.
Skills that use an input form in the app accept plain text over the API/MCP: send the values as
"Label: value" lines in input (input_label describes what the skill expects).

## Rate limits & errors

RATE LIMIT: 30/min Pro, 60/min Team (REST + MCP shared).
  Headers: X-RateLimit-Limit, X-RateLimit-Remaining; 429 adds Retry-After.
ERRORS ({"error":"..."}): 400 bad request | 401 bad key | 403 no BYO key
  | 409 twin not trained | 429 rate/cap | 500 retryable

Data handling

API and MCP traffic follows the same rules as the app: nothing you send is used to train models, chat history is never stored server-side, and generation content is sent to the model provider (US-based unless you bring your own regional key) only for the request you make. Details in our Privacy & Security Statement.