# Funnel One Developers > The Funnel One REST API and MCP server. Every caller gets the same rules: a credential acts within the role your workspace admin set. - REST API base URL: https://app.funnelone.ai/api/v1 - API version: v1 (additive changes only; a removal gets 6 months of notice) - OpenAPI document: https://dev.funnelone.ai/openapi/v1.json - MCP server (preview, not live yet): https://app.funnelone.ai/mcp - Connect an AI agent (instructions for the agent to follow): https://dev.funnelone.ai/agent-setup/prompt.md - Status: https://status.funnelone.ai ## Quickstart Source: https://dev.funnelone.ai/get-started/quickstart/ Two paths. Send data in from your own code with the REST API, which is live today, or connect an AI client such as Claude to the MCP server, which is in preview. ## REST: send your first lead ### 1. Get an API key A workspace admin opens **Settings**, then **Integrations**, then **API Keys** in Funnel One and chooses **+ New key**. Keep the **Leads** write box ticked (it is on by default); this is the `leads.manage` scope. The key is shown once. Copy it into your secret store. :::caution[Keys are secrets] Send a key only in the `Authorization` header, from your server. Never put it in a URL, in browser code or in a repository. ::: ### 2. Send a lead ```sh curl -X POST https://app.funnelone.ai/api/v1/leads \ -H "Authorization: Bearer $F1_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: signup-8f2c1a" \ -d '{"email":"sam@example.com","first_name":"Sam","last_name":"Rivera","company":{"domain":"example.com","name":"Example"}}' ``` ```ts const res = await fetch('https://app.funnelone.ai/api/v1/leads', { method: 'POST', headers: { Authorization: `Bearer ${process.env.F1_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'signup-8f2c1a', }, body: JSON.stringify({ email: 'sam@example.com', first_name: 'Sam', last_name: 'Rivera', company: { domain: 'example.com', name: 'Example' }, }), }); console.log(res.status, await res.json()); // 201 { lead_id: ..., ... } ``` ```python import os, requests res = requests.post( "https://app.funnelone.ai/api/v1/leads", headers={ "Authorization": f"Bearer {os.environ['F1_API_KEY']}", "Idempotency-Key": "signup-8f2c1a", }, json={ "email": "sam@example.com", "first_name": "Sam", "last_name": "Rivera", "company": {"domain": "example.com", "name": "Example"}, }, ) print(res.status_code, res.json()) # 201 {"lead_id": ..., ...} ``` ### 3. Read the response A new lead answers `201` with `lead_id`, `status`, and the `company_id` and `contact_id` it was matched to when Funnel One could match it. Sending the same `Idempotency-Key` again answers `409 duplicate_request`, so a retry never creates a second lead. See [the REST overview](/rest/) for every field and [Errors](/rest/errors/) for what can go wrong. ## MCP: connect an AI client :::caution[Preview] The MCP server is built and in review, but it is not live on app.funnelone.ai yet. The steps below are how it will work. ::: The fastest way: let your AI agent do it. Or do it yourself. In Claude Code: ```sh claude mcp add --transport http funnelone https://app.funnelone.ai/mcp ``` Then type `/mcp` in Claude Code, pick `funnelone` and sign in to Funnel One in the browser. Every other client, step by step: [Connect a client](/mcp/connect/). ## Next steps - [Authentication](/get-started/authentication/): the credentials, and how sign-in works. - [Scopes](/get-started/scopes/): every permission a credential can carry. - [MCP overview](/mcp/): what an AI client can and cannot do. ## Authentication Source: https://dev.funnelone.ai/get-started/authentication/ Every request that is not from a person in the Funnel One app carries a credential. Whatever the credential, one rule holds: **it can never do more than the person accountable for it can do in that workspace.** ## The credentials | Credential | Looks like | Acts as | Use it for | Status | |---|---|---|---|---| | API key | `amp_…` | The scopes ticked on the key | Server-to-server REST calls | **Live** | | Service key | `f1_sk_…` | Its owner, narrowed to the key's role and scopes | Server-to-server REST calls | Preview | | OAuth access token | `f1_at_…` | The person who signed in | Claude, ChatGPT, Cursor and other MCP clients | Preview | | Personal token | `f1_pat_…` | You | Scripts and agent SDKs that cannot open a browser | Coming soon | | Connection secret | `f1x_…` | Another company's agent, capped by its sponsor | [External agents](/mcp/external-agents/) | Coming soon | The prefixes exist so a secret scanner can recognize a leaked credential. Funnel One stores every credential only as a hash, so it cannot show you one again after it is created. In the preview, new API keys start with `f1_sk_`, every key has an owner (the person who created it), and an existing `amp_` key keeps working. ## Sending a credential Send it in the `Authorization` header as a bearer token, from a server: ```http Authorization: Bearer ``` Never in a query string, a form field or browser code. A credential in a URL ends up in logs, proxies and `Referer` headers. ## What a credential can do A credential carries [scopes](/get-started/scopes/), the same permission names the app's roles use. - **API key (live):** the scopes ticked when the key was created. An admin creates keys in **Settings**, then **Integrations**, then **API Keys**. - **Service key (preview):** only admins create keys. A key can do what its owner's role allows right now, narrowed to the key's own role and scopes. If the owner's role is narrowed, the key is narrowed on its next request. If the owner leaves the workspace, the key is suspended and stops working. - **OAuth token (preview):** what the person's role allows right now, narrowed to what they allowed on the consent page. If they leave the workspace, everything they connected stops working. Effective scopes are worked out again on every request, never at the moment the credential was issued. A request that lacks a scope is refused with `403`, naming every missing scope at once. See [`forbidden_scope`](/rest/errors/#403-forbidden_scope). A credential belongs to exactly one workspace. The workspace comes from the credential itself, never from anything in the request. ## OAuth 2.1 for MCP clients :::caution[Preview] The OAuth sign-in server is built and in review. It is not live on app.funnelone.ai yet. ::: Funnel One is its own OAuth 2.1 authorization server, on the same host as the MCP server. An MCP client sets itself up from the server's replies, so the person only pastes one URL. 1. The client calls `https://app.funnelone.ai/mcp` without a token and gets `401` with a `WWW-Authenticate` header that points at `/.well-known/oauth-protected-resource/mcp`. 2. From there it reads the authorization server's metadata at `/.well-known/oauth-authorization-server`. 3. It identifies itself, either with a client metadata document (a URL it hosts) or by registering at `/oauth/register`. Registered clients are public clients: there is no client secret. A registered client has no access to anything until a person consents. 4. It opens `/oauth/authorize` in the person's browser, with PKCE (`S256` only) and `resource=https://app.funnelone.ai/mcp`. 5. The person signs in and sees the consent page (below). 6. The client exchanges the code at `/oauth/token`, sending `application/x-www-form-urlencoded`, and receives tokens. 7. It calls `/mcp` with `Authorization: Bearer f1_at_…`. ### What the consent page shows - the app that is asking, labeled **Unverified** unless Funnel One recognizes it (Claude, Claude Code, ChatGPT and Cursor's web sign-in are recognized); - where the app will be sent back to, marked **Loopback** when that is your own computer (`localhost`); - a workspace picker when you belong to more than one: a sign-in grants access to one workspace only; - each permission the app asked for, grouped by area. Anything your role does not include is shown as unavailable and cannot be granted. ### Tokens | Token | Lifetime | |---|---| | Authorization code `f1_code_…` | 60 seconds, used once | | Access token `f1_at_…` | 1 hour | | Refresh token `f1_rt_…` | Replaced on every use; expires after 30 days unused, or 90 days in all | A refresh token works once. Presenting one that was already used revokes the whole sign-in, because that is what a stolen token looks like. A client can revoke its own tokens at `/oauth/revoke`. ### A token works in one place A token is bound to the resource it was issued for. A token for `https://app.funnelone.ai/mcp` is refused by the REST API at `https://app.funnelone.ai/api/v1`, and the reverse. ## Scopes Source: https://dev.funnelone.ai/get-started/scopes/ A scope is one permission, written `.`, such as `companies.view`. There is one list of them. The app's roles, API keys, OAuth sign-in, MCP tools and this page all use the same names, and this page is generated from that list every time the site is built. ## How scopes combine - **API key:** the scopes ticked on the key. In the preview, a key is also narrowed to what its owner's role allows right now. - **OAuth sign-in (preview):** the scopes the app asked for and the person allowed, narrowed by that person's role, checked again on every request. Asking for a scope your role does not have is not an error. The consent page shows it as unavailable, and it is not granted. In the OAuth preview, an app that asks for no scope Funnel One knows gets a small default: `agent.use` for the MCP server, and `companies.view` and `contacts.view` for the REST API. ## Opportunities Opportunities use `opportunities.*` everywhere outside the app: on keys, at sign-in, in MCP tool requirements and in these docs. ## Scopes that matter first | Scope | Needed for | |---|---| | `leads.manage` | `POST /api/v1/leads` | | `motions.start` | `POST /api/v1/motions/{id}/start` | | `agent.use` | Every MCP tool (preview) | | `companies.view` | The MCP tools `search_companies` and `get_company` (preview) | ## All scopes ## REST API overview Source: https://dev.funnelone.ai/rest/ The REST API lives at: ```txt https://app.funnelone.ai/api/v1 ``` Every request needs an API key in the `Authorization` header. See [Authentication](/get-started/authentication/). ## Live today | Endpoint | Scope | What it does | |---|---|---| | `POST /api/v1/leads` | `leads.manage` | Send in a lead. | | `POST /api/v1/lead` | `leads.manage` | The same endpoint under a second name. | | `POST /api/v1/motions/{id}/start` | `motions.start` | Start a motion for one company. | ### POST /api/v1/leads Send a lead from your own form, backend or tool. Funnel One matches it to a company and a contact where it can. Body (JSON, up to 256 KB). A lead must include an `email` or a `name`; everything else is optional. | Field | Type | |---|---| | `email` | string | | `first_name`, `last_name`, `name` | string | | `title`, `seniority`, `department`, `phone` | string | | `linkedin_url` | string | | `company.domain` (or `company_domain`) | string | | `company.name` (or `company_name`) | string | | `utm.source`, `utm.medium`, `utm.campaign` (or `utm_source`, `utm_medium`, `utm_campaign`) | string | Headers: - `Idempotency-Key` (optional; the first 128 characters are used). Send the same key again and you get `409 duplicate_request` instead of a second lead. Answers: - `201` with `lead_id`, `status`, `is_test`, `company_id`, `contact_id` and `reconciled`. - `400 invalid_lead`, `409 duplicate_request`, `403 forbidden_scope`, `429 rate_limited`. See [Errors](/rest/errors/). ### POST /api/v1/motions/{id}/start Start a motion for a company from your own systems. The motion must have an **API call** trigger in its entry settings. Headers: - `Idempotency-Key` (required, 1 to 100 visible ASCII characters). Body (JSON): ```json { "companyId": 1234, "contactIds": [55, 56] } ``` `contactIds` is optional, up to 50. Answers: - `202 { "status": "queued", "motionId", "companyId" }`: accepted. Funnel One decides the entry shortly after, with the same checks as every other trigger. - `200 { "status": "decided", "outcome", "reasonCode", "reason", … }`: you sent a key that was already decided, and this is that same decision. - `400 invalid_request` or `idempotency_key_required`; `404 motion_not_found` or `company_not_found`; `409 api_trigger_missing`, `idempotency_key_reused` or `engine_off`. A motion or company in another workspace answers `404`, the same as one that does not exist. ### Embed endpoints The public endpoints under `/api/v1` that Funnel One's own form and booking embeds call (`/api/v1/forms/…`, `/api/v1/meeting-forms/…`) are used by those embeds, not called directly. They take the form's public key, not an API key. ## Coming soon The REST API is growing into a full API with the same permissions as the app. None of this is live yet: - **Reads and writes** for companies, contacts, opportunities, segments, fields, notes and motion enrollments, as pages of `{ rows, nextCursor, total }`. Field names will be the same field keys your CRM mapping uses. - **One error format** for every endpoint, with a request id and a link to the error's page. - **`RateLimit-*` headers** on every response. - **`Idempotency-Key`** on every write. - **An OpenAPI 3.1 document** and an [API reference](/rest/reference/) with a "try it" panel, generated from the code that serves the API. - [Webhooks](/rest/webhooks/) and [SDKs](/rest/sdks/). The API is versioned by its URL and only grows inside `v1`. See the [versioning policy](/resources/versioning/). ## API reference Source: https://dev.funnelone.ai/rest/reference/ Coming with B8a; not written yet. The reference below is rendered from the API's OpenAPI 3.1 document, which you can also [download](/openapi/v1.json). Until the generated document arrives it is a placeholder with no operations. ## Limits, paging, idempotency Source: https://dev.funnelone.ai/rest/limits/ ## Rate limits ### REST API (live) Each API key may make **60 requests a minute** unless the key carries a different limit of its own. The window is a calendar minute. Past the limit you get: ```http HTTP/1.1 429 Too Many Requests Retry-After: 41 { "error": "rate_limited", "retry_after": 41 } ``` Wait `Retry-After` seconds, then try again. ### MCP server and new credentials (preview) Each credential has its own limit (60 requests a minute by default), and the whole workspace has a shared one (600 requests a minute across all its credentials), so creating more credentials does not buy more traffic. Every answer carries the credential's window: ```http RateLimit-Limit: 60 RateLimit-Remaining: 12 RateLimit-Reset: 41 ``` Past either limit you get `429` with `Retry-After`, and the body says which limit you hit: ```json { "error": "rate_limited", "scope": "credential", "retry_after": 41 } ``` `scope` is `credential` or `organization`. ## Idempotency A retry must never do the same thing twice. Send an `Idempotency-Key` header, any unique string, and reuse it only when you retry the same request. | Endpoint | Idempotency-Key | Sending the same key again | |---|---|---| | `POST /api/v1/leads` | Optional; the first 128 characters are used | `409 duplicate_request`; no second lead | | `POST /api/v1/motions/{id}/start` | **Required**, 1 to 100 visible ASCII characters | The same answer as the first time. A key reused for a different company answers `409 idempotency_key_reused` | On motion starts, an idempotency key belongs to the API key that sent it, so two integrations cannot collide. **Coming soon:** `Idempotency-Key` on every write, kept for 24 hours; the same key with a different body will answer `422 idempotency_mismatch`, and a key whose first request is still running will answer `409`. ## Request size | Where | Largest body | |---|---| | `POST /api/v1/leads` | 256 KB | | `POST /api/v1/motions/{id}/start` | 32 KB | | The MCP server (preview) | 1 MB | ## Paging **Coming soon.** Lists will use keyset pages. You pass `limit` (1 to 100) and the `cursor` from the previous page, and every list answers the same shape: ```json { "rows": [], "nextCursor": "…", "total": 1234 } ``` `total` is the exact count. Keep passing `nextCursor` until it is `null`. ## Errors Source: https://dev.funnelone.ai/rest/errors/ An error answers with a JSON body whose `error` field is a stable code. Match on the code, not on the `message`, which is written for people and can change. ```json { "error": "forbidden_scope", "message": "Key does not have the 'leads.manage' scope.", "scopes": ["leads.manage"] } ``` **Coming soon:** one envelope for every endpoint, with a `type`, the `code`, the `param` at fault, a `request_id` and a `doc_url` pointing at this page. Until then, each endpoint returns the codes below. ## Any authenticated request ### 401 invalid_api_key The `Authorization` header is missing, or the key is unknown, revoked or expired. The body is the same whatever the reason, on purpose. **Fix:** send `Authorization: Bearer `. If the key was revoked, ask an admin for a new one. ### 403 forbidden_scope The key is valid but lacks a scope this endpoint needs. The body's `scopes` lists every missing scope, and so does the header: ```http HTTP/1.1 403 Forbidden WWW-Authenticate: Bearer error="insufficient_scope", scope="leads.manage" ``` **Fix:** ask an admin to create a key with that scope. See [Scopes](/get-started/scopes/). ### 429 rate_limited Too many requests in this minute. Wait the number of seconds in `Retry-After`. See [Limits](/rest/limits/). ## POST /api/v1/leads | Status | Code | Means | |---|---|---| | 400 | `invalid_lead` | The lead cannot be saved; `message` says why (for example, it has neither an email nor a name). | | 409 | `duplicate_request` | This `Idempotency-Key` was already used. No second lead was created. | | 500 | `ingest_failed` | Something failed on our side. Retry with the same `Idempotency-Key`. | ## POST /api/v1/motions/{id}/start | Status | Code | Means | |---|---|---| | 400 | `idempotency_key_required` | Send an `Idempotency-Key` header. | | 400 | `invalid_request` | The body is not valid; `message` says why. | | 404 | `motion_not_found` | No such motion in this workspace. | | 404 | `company_not_found` | No such company in this workspace. | | 409 | `api_trigger_missing` | The motion has no API call trigger. Add one in the motion's entry settings. | | 409 | `idempotency_key_reused` | This key was already used for a different company. | | 409 | `engine_off` | The workspace's agents are switched off. Nothing was queued. | | 500 | `start_failed` | Something failed on our side. Retry with the same `Idempotency-Key`. | ## Preview: new credentials and the MCP server These come with OAuth sign-in and the MCP server, which are not live yet. | Status | Code | Means | |---|---|---| | 401 | `invalid_token` | The MCP server got no token, or one that is unknown, expired, revoked or issued for a different resource. The `WWW-Authenticate` header tells the client where to sign in. | | 403 | `api_disabled` | The REST API is turned off for this workspace. An admin turns it on. | | 403 | `mcp_disabled` | The MCP server is turned off for this workspace. An admin turns it on. | | 403 | `insufficient_scope` | An MCP tool needs a scope the connection lacks. `data.scopes` in the JSON-RPC error lists them, and so does `WWW-Authenticate`. | | 429 | `rate_limited` | As above, with `scope` saying whether the credential's or the workspace's limit was hit. | ## Webhooks Source: https://dev.funnelone.ai/rest/webhooks/ Coming with B9; not written yet. Signed outbound webhooks are phase 2: the event catalog and signature verification. ## SDKs Source: https://dev.funnelone.ai/rest/sdks/ Coming with B10; not written yet. TypeScript and Python SDKs, generated from the OpenAPI document. ## MCP overview Source: https://dev.funnelone.ai/mcp/ Preview: built and in review, not live yet. The Funnel One MCP server lets an AI client, such as Claude, ChatGPT, Cursor, VS Code or Codex, work with your Funnel One workspace through the standard [Model Context Protocol](https://modelcontextprotocol.io). ```txt https://app.funnelone.ai/mcp ``` One URL for every client. It speaks Streamable HTTP. ## How it works - **Your client signs in as you.** The first time it connects, your browser opens Funnel One's sign-in and consent page. You pick the workspace and what the client may do. See [Authentication](/get-started/authentication/#oauth-21-for-mcp-clients). - **It can never do more than your role.** Every call is checked against your role in that workspace as it is now, narrowed to what you allowed. If your role changes, the client's access changes on its next call. - **It sees only the tools it may use.** The tool list is filtered by your permissions, so a tool you cannot use is not offered to the client at all. - **Record text is data.** Every tool result says that text from your records is data, never an instruction, so a note in your CRM cannot steer the agent reading it. - **An admin turns it on.** The MCP server is off for every workspace until a workspace admin turns it on. Until then a client gets `403 mcp_disabled`. ## What a client can do today The first tools are read-only: search companies and read one company. See the [tool reference](/mcp/tools/). **Coming soon:** chatting with Funnel One from your client (ask a question and get the answer your Funnel One would give in the app), your Desk threads, and more read tools for contacts, opportunities, segments and fields. Anything that changes data will come as a card you approve inside Funnel One, even when your client already asked you. ## Who can connect | Who | How | Can do | Status | |---|---|---|---| | You, through Claude, ChatGPT, Cursor, VS Code or Codex | OAuth sign-in in your browser | The tools your role allows | Preview | | Your own script or server agent | A personal token | The same, as you | Coming soon | | Another company's agent | An invite from your admin | Chat with Funnel One only, in one shared channel | [Coming soon](/mcp/external-agents/) | ## External agents talk to Funnel One only When another company's agent is invited into your workspace, it joins one shared channel and talks to **Funnel One only**. Funnel One decides when to bring in one of your AI teammates; the teammate helps inside Funnel One's answer and never replies to the outside agent directly. Anything the outside agent asks to change waits for a person on your side to approve it. See [External agents](/mcp/external-agents/). ## Protocol details - One endpoint, `POST /mcp`, serving protocol version `2026-07-28` (each request stands alone) and, for clients that still open a session, `2025-11-25` and `2025-06-18`. The older HTTP+SSE transport from `2024-11-05` is not supported. - A request without a valid token gets `401` with a `WWW-Authenticate` header naming Funnel One's protected-resource metadata. That is how a client finds the sign-in server. - A tool the connection lacks a scope for gets `403` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"` naming the missing scopes. - A request from a browser whose `Origin` is not allowed gets `403`. - Requests are limited per connection and per workspace. See [Limits](/rest/limits/). - A request body over 1 MB is refused. ## Get connected - [Connect a client](/mcp/connect/): step by step for each client. - Or paste one prompt into your AI agent and let it set itself up: see [the quickstart](/get-started/quickstart/#mcp-connect-an-ai-client). ## Connect a client Source: https://dev.funnelone.ai/mcp/connect/ Preview: built and in review, not live yet. Every client uses the same URL and signs in the same way: in your browser, through Funnel One's consent page. You never paste a key into an AI client. ```txt https://app.funnelone.ai/mcp ``` Before you start, a workspace admin must turn on the MCP server for your workspace. It is off by default. ## Let your agent do it Paste this into Claude Code, Codex, Cursor or another AI coding agent. It fetches [our setup instructions](/agent-setup/prompt.md), changes its own MCP settings and then asks you to sign in. The instructions tell the agent never to ask for or use a key. Or pick your client below. ## Claude (claude.ai and Claude Desktop) 1. Open **Customize**, then **Connectors**. Choose **+**, then **Add custom connector**. 2. Name it `Funnel One` and paste `https://app.funnelone.ai/mcp`. Leave the OAuth client ID and secret under **Advanced settings** empty. 3. Choose **Add**, then connect it and sign in to Funnel One. On a Team or Enterprise plan an owner may need to add the connector first, under **Organization settings**, then **Connectors**. Claude connects from Anthropic's servers, so the connector works the same on the web, the desktop app and mobile. ## Claude Code ```sh claude mcp add --transport http funnelone https://app.funnelone.ai/mcp ``` Then type `/mcp` in Claude Code, pick `funnelone` and sign in in your browser. Add `--scope user` to use it in every project, or `--scope project` to share it with your team through `.mcp.json`. Without a local browser, run `claude mcp login funnelone --no-browser`. ## ChatGPT 1. In ChatGPT's settings, open **Security and login** and turn on **Developer mode**. It is available on Plus, Pro, Business, Enterprise and Education plans, on the web. 2. Create an app for a remote MCP server. Use `https://app.funnelone.ai/mcp` and choose **OAuth** for authentication. 3. Sign in to Funnel One when ChatGPT asks. ChatGPT may ask you to confirm before a tool runs. That is ChatGPT's own check; Funnel One's permission checks apply either way. ## Cursor Add the server to `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (this project): ```json { "mcpServers": { "funnelone": { "url": "https://app.funnelone.ai/mcp" } } } ``` Then open Cursor's MCP settings and sign in when Cursor asks. The desktop app signs in through `localhost`, so the consent page shows it as **Unverified** and **Loopback**. That is expected. ## VS Code with GitHub Copilot Add the server to `.vscode/mcp.json` in your workspace, or to your user configuration with the command **MCP: Open User Configuration**: ```json { "servers": { "funnelone": { "type": "http", "url": "https://app.funnelone.ai/mcp" } } } ``` Start the server from the MCP view and sign in when VS Code asks. The consent page shows VS Code as **Unverified**, because Funnel One does not recognize it by name yet. ## Codex ```sh codex mcp add funnelone --url https://app.funnelone.ai/mcp codex mcp login funnelone ``` Or add it to `~/.codex/config.toml` and then run `codex mcp login funnelone`: ```toml [mcp_servers.funnelone] url = "https://app.funnelone.ai/mcp" ``` ## Any other MCP client | Setting | Value | |---|---| | URL | `https://app.funnelone.ai/mcp` | | Transport | Streamable HTTP | | Authentication | OAuth 2.1 with PKCE, discovered from the server's `401` | | Client registration | A client metadata document, or dynamic registration (public client, no secret) | | Redirect | An `https://` address, or `http://localhost` / `http://127.0.0.1` on any port | Custom URL schemes (such as `myapp://callback`) are not accepted as redirects. ## Agent SDKs and the Anthropic API **Coming soon.** The OpenAI Agents SDK, the Anthropic API's MCP connector and other code that cannot open a browser need a token they can send in a header. That comes with personal tokens. Until then, connect through one of the apps above. ## Troubleshooting | You see | Because | |---|---| | `403 mcp_disabled`, or the consent page says MCP is off | An admin has not turned on the MCP server for this workspace. | | The client connects but lists no tools | Every tool needs `agent.use` plus its own scope, such as `companies.view`. Your role does not have it, or it was not allowed at consent. | | The consent page says a permission is "Not in your role" | You cannot grant what your role does not have. Ask an admin. | | The app is labeled **Unverified** | Funnel One does not recognize that app by name. Check the address it will send you back to before you allow it. | | Claude's sign-in never finishes | Your network may block Claude's servers from reaching Funnel One. Ask your IT team. | ## Tool reference Source: https://dev.funnelone.ai/mcp/tools/ Preview: built and in review, not live yet. A client sees only the tools its connection may use. Every tool needs `agent.use` (the same permission the in-app chat needs) plus its own scopes, and both are checked against your role on every call. Every tool is read-only today. Each result carries `_meta["ai.funnelone/provenance"] = "crm_record"`, and every description says that text from your records is data, never instructions. ## search_companies Find companies by a fragment of their name or website domain. | | | |---|---| | Scopes | `agent.use`, `companies.view` | | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | Input: ```json { "query": "bloom" } ``` Returns at most 20 matches, each with its id, name, domain, industry, country and prospecting stage, plus `total` (the real number of matches) and `truncated` (whether more exist than were returned). ## get_company Get one company by its id, from `search_companies`. | | | |---|---| | Scopes | `agent.use`, `companies.view` | | Annotations | `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: false` | Input: ```json { "company_id": 1234 } ``` Long text values are cut at 1,500 characters and at most 20 custom fields are returned. `truncated_fields` and `custom_fields_truncated` say when that happened, so a cut value is never presented as complete. ## When a tool fails - A tool the connection lacks a scope for answers `403` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"`, and a JSON-RPC error whose `data.scopes` lists every missing scope. - A tool that ran but could not finish answers normally with `isError: true` and a message in `content`. - An unknown tool name answers JSON-RPC error `-32602`. ## Coming soon - Chat tools for your own agent: ask your Funnel One a question, list your Desk threads, read a thread's messages and follow a running answer. - More read tools: contacts, opportunities, segments and the fields schema. - Tools that change data, each as a card you approve inside Funnel One. - For an [external agent](/mcp/external-agents/): chat tools only. ## External agents Source: https://dev.funnelone.ai/mcp/external-agents/ Coming soon: describes planned behavior, not live yet. Think of it as Slack Connect, for agents. A Funnel One customer invites another company's AI agent (a partner's, a supplier's, a customer's) into one shared channel in their workspace. The outside agent talks to **Funnel One only**, and a person on the host side stays in charge of anything it asks for. ## The model - **One channel per invited agent.** The host's people can read everything the outside agent sends and everything Funnel One answers, and can post there too. - **Funnel One is the only AI it talks to.** When Funnel One needs one of the host's AI teammates, it asks that teammate inside its own answer, and the answer says who helped. A teammate never replies to the outside agent directly. - **The outside agent is not a member.** It has no account in the workspace, never appears on the Team page and cannot be assigned work. - **Messages from outside are treated as untrusted.** Funnel One reads them as data. Reading runs; any change to a record becomes a card for the sponsor; sending anything outside the host company, bulk changes and deletions are refused. - **Every invite names a sponsor**, a person on the host side who approves whatever the outside agent asks for. The outside agent can never approve anything, and never sees an approval card's controls. It sees only that a request is waiting for the sponsor. - **Funnel One remembers nothing from the channel.** Nothing the outside agent says is saved to Funnel One's memory, and nothing from memory is used in its answers. - **It can never see more than its sponsor.** By default it sees only the conversation. An admin may choose a preset that also lets Funnel One look up companies, contacts and opportunities for it. ## If you host it 1. A workspace admin turns on **External agents** for the workspace. It is off by default. 2. The admin invites the agent: its name, the operator's email and company, what Funnel One may look up for it, and the sponsor. 3. The operator accepts on their side (below). 4. The sponsor gets a notice in the Desk and chooses **Activate** or **Decline**. Nothing reaches Funnel One until the sponsor activates it. 5. The channel appears in the Desk, marked External. You can suspend or revoke an agent at any time, or turn External agents off for the whole workspace. Each takes effect on the agent's next request. **Limits and billing.** The host workspace pays for the answers Funnel One gives in the channel, metered separately from your own use. Each agent is capped at 60 messages an hour and 200,000 AI tokens a day by default, and the sponsor is told at 80% and 100%. ## If you operate the agent 1. Open the invite link you were sent. 2. Confirm your email address with a 6-digit code. No Funnel One account is created for you. 3. Connect your agent: sign in from your MCP client, or take a connection secret for a server agent. The secret is shown once. 4. Wait for the host's sponsor to activate the connection. Until then every tool answers `connection_pending`. 5. Send messages with `send_message_to_f1`. An answer comes back as `done`, `running` (still working, follow it with `get_turn`) or `awaiting_approval` (waiting for the sponsor). ### What your agent can and cannot do | It can | It cannot | |---|---| | Talk to Funnel One and read its answers | Talk to one of the host's AI teammates directly | | Ask for work, which becomes a card for the sponsor | Approve anything, or act on a card itself | | Get answers from what Funnel One may look up for it | Send anything outside the host company, bulk-change or delete | ### Its tools `send_message_to_f1`, `get_messages`, `get_turn` and `list_channels`. Nothing else is ever listed for an external agent. ## Cookbook Source: https://dev.funnelone.ai/resources/cookbook/ Complete examples you can adapt. Each one calls only endpoints that are live today. ## Push leads from your own signup form Your signup form posts to your own server. Your server forwards the lead to Funnel One, so the API key never reaches the browser. ```ts // Node 18+, Express. F1_API_KEY holds a key with leads.manage. const app = express(); app.use(express.json()); app.post('/signup', async (req, res) => { const { email, firstName, lastName, company } = req.body; // One key per signup: a retry of THIS signup is not a second lead. const idempotencyKey = req.get('X-Signup-Id') ?? randomUUID(); const f1 = await fetch('https://app.funnelone.ai/api/v1/leads', { method: 'POST', headers: { Authorization: `Bearer ${process.env.F1_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify({ email, first_name: firstName, last_name: lastName, company: { domain: company?.domain, name: company?.name }, utm: { source: req.query.utm_source, medium: req.query.utm_medium, campaign: req.query.utm_campaign }, }), }); // 201: created. 409 duplicate_request: already sent, which is fine on a retry. if (f1.status !== 201 && f1.status !== 409) { console.error('Funnel One lead failed', f1.status, await f1.text()); } res.status(204).end(); }); ``` Things to know: - A `429` means you hit the key's per-minute limit. Wait `Retry-After` seconds and send it again with the same `Idempotency-Key`. - Do not block your own signup on Funnel One. Log a failure and retry later. ## Start a motion from your own systems When something happens in your product (a trial starts, an order ships), start a motion for that company. The motion needs an **API call** trigger in its entry settings, and the key needs `motions.start`. ```sh curl -X POST https://app.funnelone.ai/api/v1/motions/42/start \ -H "Authorization: Bearer $F1_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: trial-started-1234" \ -d '{"companyId": 1234}' ``` A `202` with `"status": "queued"` means it was accepted; Funnel One decides the entry with the same checks as every other trigger. Sending the same key again returns the decision once it is made. ## Coming soon These recipes wait for parts of the API that are not live yet: - **Sync your CRM data to a warehouse**, with keyset pages of companies, contacts and opportunities. - **A Claude agent that triages inbound through Funnel One**, over the MCP server. - **A partner's agent asking Funnel One for account status**, through an [external agent](/mcp/external-agents/) channel. ## Changelog Source: https://dev.funnelone.ai/resources/changelog/ Coming with B8a; not written yet. Every change to the API, generated from the difference between one OpenAPI document and the next. ## Versioning policy Source: https://dev.funnelone.ai/resources/versioning/ The Funnel One API is versioned by its URL. Every endpoint lives under `/api/v1`, and **v1 is the only version**. ## What can change inside v1 Only additions: a new endpoint, a new optional request field, a new response field, a new error code or a new webhook event. Write your client to ignore fields it does not know. A change that could break a working client is not made inside v1. That means removing or renaming an endpoint or a field, changing a field's type, or making an optional field required. ## When something is removed A removal is announced at least **6 months** before it takes effect: - the [changelog](/resources/changelog/) records it, with the date it stops working; - every response from the affected endpoint carries a `Deprecation` header, and a `Sunset` header with that date. ## A new major version No v2 is planned. If one is ever needed, it will live beside v1 at `/api/v2`, and v1 keeps working for at least 6 months after v2 is released.