# 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.