# Moreflow API guide

> Issue an account-bound token, discover the current public API, and make your first safe integration call.

## Documentation links

- [Agent quickstart (Markdown)](https://api.moreflow.io/v1/operations/docs/quickstart.md): The complete machine-readable onboarding guide.
- [Getting-started guide (HTML)](https://api.moreflow.io/v1/operations/docs/guide): The same guide rendered for people and browser-based agents.
- [Interactive Swagger reference](https://api.moreflow.io/v1/operations/docs): Try the API and inspect the live OpenAPI surface.
- [OpenAPI JSON](https://api.moreflow.io/v1/operations/openapi.json): The generated contract for tools and client generation.
- [API consumer changelog](https://api.moreflow.io/v1/operations/docs/changelog.md): Deterministic release notes generated from public contract snapshot diffs.
- [MCP Streamable HTTP endpoint](https://api.moreflow.io/mcp): Connect an MCP client to the dedicated API host.
- [OAuth protected-resource metadata](https://api.moreflow.io/.well-known/oauth-protected-resource): RFC 9728 metadata that names the API host as the OAuth 2.1 authorization server for /mcp.
- [OAuth authorization-server metadata](https://api.moreflow.io/.well-known/oauth-authorization-server): RFC 8414 metadata: authorize, token, registration, and JWKS endpoints with PKCE (S256) required.
- [API token settings](https://app.moreflow.io/settings/integrations/api-key): Create and rotate account-bound integration tokens.
- [Webhook correlation and verification follow-up (moreflow-hjbpv.21)](https://github.com/valie-media-gmbh/moreflow-v2/issues?q=is%3Aissue+moreflow-hjbpv.21): Per-event versions, correlation fields, and the evolving signature contract.

## Use the live contract

This guide explains the workflow, not a copied operation catalog. Start with the authenticated `operations.discovery.list` and `operations.discovery.get` operations, then use the [live OpenAPI document](/v1/operations/openapi.json) for the exact fields, validation, examples, error codes, and agent-safety metadata available to your account. The OpenAPI agent annotations derive from each operation’s `agent.effect` and its declared confirmation and reversibility.

> Public operation ids begin with `resource.action` (for example `clients.list`). Integration tokens grant exact operation ids, never prefixes or wildcards.

Canonical public operation ids use `module.resource[.subresource].verb`. New ids end in `list`, `detail`, `create`, `update`, or `delete`. Lists return `{ items, nextCursor }`; detail reads return one resource; mutations use the matching write scope and the standard success/error envelope plus idempotency rules. Existing documented grandfathered ids remain authoritative when discovered.

## Contract lifecycle and release changes

The public contract is versioned per operation. Read the [API consumer changelog](/v1/operations/docs/changelog.md) before upgrading an integration; it is generated from the same committed snapshots that drive the live schemas and the `@moreflow/api` release notes.

For the euro AI allowance migration, read the permanent [billing.entitlements.get v4 notice](/v1/operations/docs/guide#ai-usage-v4) below, including the replacement metric and its units.

<a id="ai-usage-v4"></a>

## billing.entitlements.get v4: euro AI allowance / AI-Kontingent in Euro

### English

`billing.entitlements.get` now uses contract v4. Replace `ai_requests` and `ai_credits` with `ai_usage` in the request’s `usage` selector and read the response at `usage.ai_usage`. The retired metric names are no longer accepted.

AI amounts are integer micro-euros: 1 EUR = 1,000,000 micro-euros. When present, `usage.ai_usage.limit` is the effective allowance for the usage period, including purchased top-ups; `usage.ai_usage.current` is usage so far; and `usage.ai_usage.remaining` is the available allowance. Divide numeric values by 1,000,000 to display euros. These values are neither request counts nor credit units.

A present `null` limit or remaining value means unlimited allowance. Numeric fields can be omitted when a decision is denied or usage is unavailable; do not interpret an absent field as zero or unlimited. Use `allowed` and `reason` to handle the decision, and rediscover contract v4 before updating your client.

### Deutsch

`billing.entitlements.get` verwendet jetzt Vertrag v4. Ersetzen Sie `ai_requests` und `ai_credits` durch `ai_usage` im `usage`-Selektor der Anfrage und lesen Sie die Antwort unter `usage.ai_usage`. Die bisherigen Metriknamen werden nicht mehr akzeptiert.

AI-Beträge sind ganzzahlige Mikro-Euro: 1 EUR = 1.000.000 Mikro-Euro. Wenn vorhanden, enthält `usage.ai_usage.limit` das verfügbare Gesamtkontingent des Nutzungszeitraums einschließlich gekaufter Aufladungen; `usage.ai_usage.current` enthält den bisherigen Verbrauch und `usage.ai_usage.remaining` das verbleibende Kontingent. Teilen Sie Zahlenwerte durch 1.000.000, um Euro anzuzeigen. Diese Werte sind weder Anfragezahlen noch Credit-Einheiten.

Ein vorhandener `null`-Wert für das Limit oder verbleibende Kontingent bedeutet unbegrenztes Kontingent. Zahlenfelder können bei einer Ablehnung oder nicht verfügbarem Verbrauch fehlen; behandeln Sie fehlende Felder weder als null Euro noch als unbegrenzt. Beachten Sie `allowed` und `reason` und rufen Sie vor der Client-Aktualisierung Vertrag v4 über die Discovery ab.

```json
{ "usage": ["ai_usage"] }
```

## 1. Choose a credential: OAuth for hosted clients, a token for headless agents

For a direct API integration, sign in as an account owner or admin and open the [API token settings page](https://app.moreflow.io/settings/integrations/api-key) (Settings → Integrations → API tokens). Choose a read or write scope bundle, review its operation picker, and create the token. The UI resolves each selected bundle to the exact public `canonical` operation ids at issuance time; changing a bundle later never changes an existing token.

A newly created secret is shown once—store it in a secret manager. Never paste a secret into a URL, source repository, or log. The session-only `api.tokens.*` operations behind this page are never grantable to a credential.

Headless agents can manage their own credentials without the settings page. An owner or admin issues one bootstrap API token with the `admin:tokens` scope bundle. That credential may call `api.tokens.agent.create` to mint narrower tokens, `api.tokens.agent.rotate` to rotate one, `api.tokens.agent.revoke` to revoke one, and `api.tokens.agent.list` to find token ids. Scopes never widen: every exact operation id a minted or rotated token receives must already be held by the calling credential, so a token can only include `admin:tokens` when its issuer holds it, and a credential can never revoke or rotate itself. Every mint is audited and listed on the settings page with its origin credential, and `admin:tokens` is the only bundle that can grant these operations. OAuth grants never carry it, so an MCP or OAuth client cannot manage credentials.

```bash
curl --silent --show-error -X POST "$MOREFLOW_API_BASE/v1/operations/api.tokens.agent.create" \
  -H "Authorization: Bearer $MOREFLOW_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label":"Nightly sync agent","scopes":["clients:read"],"idempotencyKey":"nightly-sync-2026-09-03"}'
```

Every integration token is bound to the account that issued it. Do not include `accountId`, `account_id`, or `x-moreflow-account-id` to select another account: the API derives the account from the verified bearer token.

OAuth 2.1 is the default path for hosted MCP clients such as Claude.ai, ChatGPT, and Claude Desktop. Point the client at the [Moreflow MCP endpoint](/mcp). A 401 response points it at the [protected-resource metadata](/.well-known/oauth-protected-resource), which names the API host as the authorization server. The API serves [authorization-server metadata](/.well-known/oauth-authorization-server), `/oauth2/authorize`, `/oauth2/token`, `/oauth2/register`. Authorization codes require PKCE (S256). Moreflow uses revocable opaque access tokens, so authorization-server metadata intentionally does not advertise a `jwks_uri`. During consent you sign in, pick the Moreflow account the grant is bound to, and approve one scope bundle; the issued access token carries exactly that account and bundle, and every token can be revoked from the API token settings page. No manual bearer is needed for a client that completes OAuth. Moreflow does not advertise or accept the OAuth `client_credentials` grant: this server has no service-principal/account-binding model, so use a static account-bound token or a user-consented OAuth grant instead.

Client identity: clients may present an HTTPS client-id metadata URL (CIMD, MCP profile 2026-07-28) without prior registration, or use RFC 7591 dynamic registration. Unauthenticated dynamic registration is only open on the development API host (`dev-api.moreflow.io`) and on local loopback runtimes; on production, registration requires an authenticated Moreflow session, so a production client without CIMD support must be registered while signed in as an account admin or fall back to a pre-issued token. In every environment, verify that the requested resource is exactly the API host’s `/mcp` URL (for production `https://api.moreflow.io/mcp`) and never approve a different audience.

Static account-bound tokens remain the path for headless agents, servers, CLIs, and any client that cannot complete a browser consent. Both credential kinds resolve to the same account binding, the same exact-operation scopes, and the same rate limits.

## 2. Send your first public API call

Use the bearer token in the `Authorization` header. This read uses the public `clients.list` operation; its exact input and output evolve through the live contract linked above.

```bash
export MOREFLOW_API_BASE="https://api.moreflow.io"
export MOREFLOW_API_TOKEN="mfo_replace_with_your_secret"

curl --silent --show-error "$MOREFLOW_API_BASE/v1/operations/clients.list" \
  -H "Authorization: Bearer $MOREFLOW_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"limit":25}'
```

Successful public API calls return the operation output directly as the JSON response body. There is no `{ success: true, data }` success wrapper; for this list operation, the top-level body follows the discovered output schema:

```json
{
  "items": [],
  "nextCursor": null
}
```

_The token needs the exact `clients.list` scope. Add `operations.discovery.list` and `operations.discovery.get` when your integration or agent will discover contracts at runtime._

## 3. Discover before you invoke

Do not hardcode a catalog from this guide. List the public surface, inspect one operation, and construct requests from its returned schema and examples. A token can discover only the exact operation ids it is allowed to use.

```bash
# Find public operations that the token can discover.
curl --silent --show-error "$MOREFLOW_API_BASE/v1/operations/operations.discovery.list" \
  -H "Authorization: Bearer $MOREFLOW_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{}'

# Inspect one live input/output/error contract and its safety metadata.
curl --silent --show-error "$MOREFLOW_API_BASE/v1/operations/operations.discovery.get" \
  -H "Authorization: Bearer $MOREFLOW_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"id":"clients.list"}'
```

Use `operations.discovery.search` for a keyword search and `operations.modules.list` to narrow the browse path. The [Swagger reference](/v1/operations/docs) renders the same public surface, while the [OpenAPI JSON](/v1/operations/openapi.json) is suitable for generated clients.

## 4. Follow the error envelope and recovery

Failures use the canonical `{ success: false, error: { code, message, details, recovery } }` envelope. Treat `error.recovery` as the next navigational action instead of matching human-readable messages. Preserve its operation id and safe arguments when they are present.

```json
{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "Integration token does not include the requested operation scope.",
    "details": { "operationId": "clients.create" },
    "recovery": {
      "operation": "operations.discovery.list",
      "args": {}
    }
  }
}
```

| Signal | Meaning | Recovery |
| --- | --- | --- |
| `401 AUTH_ERROR` | The bearer is missing, invalid, expired, or revoked. | Create or rotate an account-bound token and send it as Authorization: Bearer. |
| `403 FORBIDDEN` | The token or session cannot use this operation or account. | Discover the allowed public operation ids; request the missing exact scope from an account administrator. |
| `404 NOT_FOUND` | The operation is unknown or not externally discoverable. | Call operations.discovery.list, then describe a returned id instead of guessing. |
| `402 UPGRADE_REQUIRED` | The account monthly api_requests allowance is exhausted. | Follow the pricing recovery in the error and check billing.entitlements.get. |
| `429 RATE_LIMIT` | A short-window protective limit was reached. | Honor Retry-After and RateLimit-* headers, then retry safely. |

## 5. Cursor pagination

Public list contracts return `{ items, nextCursor }`. Their input uses an optional `cursor` and a `limit` from 1 to 100, defaulting to 25. Send the opaque cursor returned by the previous page as the next request’s `cursor`; never decode, construct, or reuse a cursor with different filters.

1. Start without a cursor and choose a limit within the discovered range.
2. Read items from the response and continue only when `nextCursor` is not null.
3. Send that exact opaque value with the same filters; stop when `nextCursor` is null.

```json
{"limit":25,"cursor":"opaque-next-cursor"}
```

<a id="idempotency"></a>

## 6. Mutation idempotency

Every idempotent public mutation has one durable lifecycle: **one key = one logical request**. Generate a key for a new payload, reuse it only for a byte-identical retry, and generate a new key when the user edits the payload. Send the key either as the `Idempotency-Key` header or as the JSON `idempotencyKey` field when the operation schema exposes it; if both are supplied, keep them identical.

### Created

The request claimed the key and executed. Store the successful result if the user needs it; clear the local pending attempt only after success.

### Replayed

The identical request reused a stored terminal result. HTTP responses include `Idempotency-Replayed: true`; MCP responses expose `replayed: true` in structured content.

### Conflict

`409 IDEMPOTENCY_CONFLICT` means the key already belongs to another fingerprint. Rotate the key for the edited/new payload; never clear a key and retry a changed request under that same value.

### Idempotency recovery table

| Signal | Meaning | Next action |
| --- | --- | --- |
| `IDEMPOTENCY_CONFLICT (409)` | The key is bound to a different request. | Use a new key for the new payload. Reuse the old key only if restoring the exact original payload. |
| `IDEMPOTENCY_IN_PROGRESS (425)` | An identical request is still running or its `600-second pending lease` has not expired. | Honor Retry-After and retry the identical request with the same key. After the lease expires, use the documented safe re-execution option if available. |
| `Idempotency-Replayed: true` | The stored terminal success or failure was returned. | Treat the response as the result of the original attempt. Do not submit again with another key. |

Successes and terminal failures are retained for `30 days`. A failed request therefore replays the same failure during that window; rotate only when the request itself is being changed. After expiry the key may be reused. Pending claims have a `600-second lease`; an active claim returns the exact remaining delay in `Retry-After`, while an expired claim reports unknown outcome unless safe re-execution is explicitly enabled.

### Copy-paste browser recipe

This framework-agnostic helper keeps the key and its payload fingerprint together in `sessionStorage`. It reuses a key for a timeout retry, rotates it for an edited payload, and clears it after success. It calls the API directly from the page, with no token: the public form endpoints answer CORS requests from any site, and the form’s allowed domains decide which sites may submit.

```javascript
async function sha256(value) {
  const bytes = new TextEncoder().encode(value);
  const digest = await crypto.subtle.digest('SHA-256', bytes);
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
}

async function submitPublicForm(formId, data) {
  const storageKey = 'moreflow:idempotency:' + formId;
  const payload = { formId, data };
  const payloadHash = await sha256(JSON.stringify(payload));
  const previous = JSON.parse(sessionStorage.getItem(storageKey) || 'null');
  const key = previous && previous.payloadHash === payloadHash
    ? previous.key
    : crypto.randomUUID();
  sessionStorage.setItem(storageKey, JSON.stringify({ key, payloadHash }));

  const response = await fetch('https://api.moreflow.io/v1/forms/' + encodeURIComponent(formId) + '/submissions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Idempotency-Key': key },
    body: JSON.stringify({ data, idempotencyKey: key })
  });
  if (response.ok) sessionStorage.removeItem(storageKey);
  if (response.headers.get('Idempotency-Replayed') === 'true') {
    console.info('Moreflow returned the stored result for this retry.');
  }
  if (!response.ok) throw new Error('Moreflow submission failed: ' + response.status);
  return response.json();
}
```

For production clients, preserve the same request body and key across network retries, and inspect the structured `error.code`/`error.recovery` envelope instead of matching messages. For a new custom browser form, install [@moreflow/forms@0.9.2](https://www.npmjs.com/package/@moreflow/forms) and call `submitWithRetry({ formId, input: { data }, acquisitionTracking: () => consentGiven })`; use `normalizePhoneNumber()` only for phone fields. The helper supplies replay-safe idempotency without imposing a UI framework. Browser forms call https://api.moreflow.io directly with no API token; configure their allowed domains.

<a id="webhook-signature-verification"></a>

## 7. Verify webhook signatures

Create a webhook endpoint with `webhooks.endpoints.create` and store the signing secret exactly once. Moreflow sends the raw JSON body with an `x-moreflow-signature` header containing `t=<unix-seconds>, v1=<hex-hmac>`. The signed payload is the exact timestamp, a period, and the exact raw body: `timestamp + "." + rawBody`. Every envelope also carries its causing invocation’s `requestId`, `operationId`, and `actor: { type, id }`. Verify before JSON parsing or reserialization, compare in constant time, reject timestamps outside the 300-second replay window (five minutes), and accept the previous secret only during the documented rotation overlap. Call `webhooks.events.list` with `{ level: 2 }` for each event’s version, occurrence key, and payload schema. See the [webhook correlation and verification follow-up (moreflow-hjbpv.21)](https://github.com/valie-media-gmbh/moreflow-v2/issues?q=is%3Aissue+moreflow-hjbpv.21) for the evolving event envelope and versioned catalog contract.

> Test vector: secret `whsec_test`; timestamp `1700000000`; raw body `{"id":"evt_test","type":"client.created"}`; expected v1 HMAC `98b6c5a0e2aeac155732a1bb05a3cdd112ea587c57a154cfe67e4bceae47fe5a`.

### Node.js

```javascript
import { createHmac, timingSafeEqual } from 'node:crypto';

const REPLAY_WINDOW_SECONDS = 300;

export function verifyMoreflowWebhook(
  rawBody,
  signatureHeader,
  secret,
  nowSeconds = Math.floor(Date.now() / 1000)
) {
  const values = signatureHeader.split(',').map((value) => value.trim());
  const timestamp = values.find((value) => /^t=\d+$/u.test(value))?.slice(2);
  const signatures = values
    .filter((value) => /^v1=[0-9a-f]{64}$/iu.test(value))
    .map((value) => value.slice(3).toLowerCase());
  if (!timestamp || signatures.length === 0) return false;

  const timestampSeconds = Number(timestamp);
  if (!Number.isSafeInteger(timestampSeconds) ||
      Math.abs(Math.floor(nowSeconds) - timestampSeconds) > REPLAY_WINDOW_SECONDS) {
    return false;
  }

  const signedPayload = Buffer.concat([Buffer.from(timestamp + '.', 'ascii'), rawBody]);
  const expected = createHmac('sha256', secret).update(signedPayload).digest();
  return signatures.some((signature) => {
    const received = Buffer.from(signature, 'hex');
    return received.length === expected.length && timingSafeEqual(expected, received);
  });
}

// Read the request bytes once, verify them, then JSON.parse(rawBody.toString('utf8')).
```

### Python

```python
import hashlib
import hmac
import re
import time

REPLAY_WINDOW_SECONDS = 300

def verify_moreflow_webhook(
    raw_body: bytes,
    signature_header: str,
    secret: str,
    now_seconds: int | None = None,
) -> bool:
    values = [value.strip() for value in signature_header.split(',')]
    timestamp = next(
        (value[2:] for value in values if re.fullmatch(r't=\d+', value)),
        None,
    )
    signatures = [
        value[3:].lower()
        for value in values
        if re.fullmatch(r'v1=[0-9a-f]{64}', value, re.IGNORECASE)
    ]
    if timestamp is None or not signatures:
        return False

    timestamp_seconds = int(timestamp)
    current_seconds = int(time.time()) if now_seconds is None else now_seconds
    if abs(current_seconds - timestamp_seconds) > REPLAY_WINDOW_SECONDS:
        return False

    signed_payload = timestamp.encode('ascii') + b'.' + raw_body
    expected = hmac.new(secret.encode('utf-8'), signed_payload, hashlib.sha256).digest()
    return any(
        hmac.compare_digest(expected, bytes.fromhex(signature))
        for signature in signatures
    )

# Flask: raw_body = request.get_data(cache=False, as_text=False)
# Read the bytes once, verify them, then json.loads(raw_body).
```

### curl / OpenSSL test

```bash
secret='whsec_test'
timestamp='1700000000'
raw_body='{"id":"evt_test","type":"client.created"}'
signed_payload="$timestamp.$raw_body"
expected=$(printf '%s' "$signed_payload" | openssl dgst -sha256 -hmac "$secret" | awk '{print $2}')
test "$expected" = '98b6c5a0e2aeac155732a1bb05a3cdd112ea587c57a154cfe67e4bceae47fe5a'
# This shell equality is a test-vector assertion only; receivers must use
# timingSafeEqual (Node.js) or hmac.compare_digest (Python).

curl --request POST 'https://receiver.example/webhooks/moreflow' \
  --header 'Content-Type: application/json' \
  --header 'x-moreflow-signature: t=1700000000, v1=98b6c5a0e2aeac155732a1bb05a3cdd112ea587c57a154cfe67e4bceae47fe5a' \
  --data-binary "$raw_body"
```

## 8. Limits, allowance, and retries

### 402 UPGRADE_REQUIRED

The account’s monthly `api_requests` allowance is exhausted. It is an account plan entitlement, consumed when an external operation is admitted and not refunded if that operation later fails. Follow the returned pricing recovery.

### 429 rate limited

A short-window protective rate limit was reached. It is independent from the monthly allowance: honor `Retry-After`, `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset`, then retry safely.

Discovery calls are unmetered. A successful `operations.discovery.list`, `operations.discovery.get`, `operations.discovery.search`, or `billing.entitlements.get` call does not consume the monthly API-request allowance; invoking a business operation does.

<a id="mcp"></a>

## 9. MCP Streamable HTTP quickstart

Moreflow exposes a stateless Streamable HTTP MCP server at `/mcp` on the dedicated API host. Configure a remote MCP client with the same account-bound bearer token and API base URL:

```json
{
  "mcpServers": {
    "moreflow": {
      "url": "https://api.moreflow.io/mcp",
      "headers": { "Authorization": "Bearer mfo_replace_with_your_secret" }
    }
  }
}
```

The server exposes exactly eleven meta-tools: `list_operations`, `list_modules`, `describe_operation`, `query_operation`, the write tools `create_record`, `update_record`, `delete_record`, and `run_action`, and `get_entitlements`, plus `search` and `fetch`. It does not generate one MCP tool per API operation.

`https://app.moreflow.io/mcp` is deliberately `404`; only the API host's `/mcp` endpoint is live. The API host also serves host-derived protected-resource metadata at `/.well-known/oauth-protected-resource` and authorization-server metadata at `/.well-known/oauth-authorization-server`, so hosted clients complete OAuth 2.1 with PKCE and account selection; CLI, desktop, and headless clients can configure a static account-bound bearer directly instead.

Use list, search, fetch, and describe to obtain the current public contract. Call `query_operation` for a read-only operation and the write tool that `describe_operation` names in its `tool` field for a mutation, with its `operationId`, `input`, and a caller-generated `idempotencyKey`. Each write tool accepts only its own operations (create, update in place, delete or irreversible removal, or an action such as sending, publishing, or a job); a mismatch is rejected with structured recovery naming the right tool. When the discovered agent metadata says `confirmation: "always"`, the invocation must include `confirm: true` after explicit user confirmation.

Listing, describing, searching, and entitlement reads are unmetered. The write tools delegate to the same API dispatcher as HTTP calls, so they apply token scopes, account binding, rate limits, audit records, canonical recovery failures, and the monthly `api_requests` meter.

Besides tools, the server advertises MCP resources and prompts. `resources/list` returns the quickstart (`moreflow://docs/quickstart`), the OpenAPI document (`moreflow://openapi`), and one contract document per module; `resources/templates/list` exposes `moreflow://operations/{operationId}/contract` and `moreflow://operations/{operationId}/output-schema`. Reading a resource is unmetered and needs no tool call. `prompts/list` offers the canonical workflows `onboard_client`, `quote_to_invoice`, and `schedule_meeting`. The tool list can change when the account policy changes: `initialize` advertises `tools.listChanged`, and a client that opens `GET /mcp` with `Accept: text/event-stream` receives `notifications/tools/list_changed` and should then call `tools/list` again.

Result envelope: a successful `query_operation` or write tool call returns the operation output itself as `structuredContent` (never wrapped in `{ success, data }`), and a failure returns the canonical `{ success: false, error }` envelope with `isError: true`. Every result carries `_meta.operationId` and `_meta.outputSchema.uri`, the resource whose JSON Schema the successful structuredContent conforms to, so a client can validate results without calling `describe_operation` first.

## 10. Per-client MCP setup

Use the client-specific installation option below. OAuth clients complete Moreflow OAuth 2.1 in the client; bearer clients use the secret-free placeholder in the generated snippet. Never commit a bearer token to a config file.

### Claude.ai

Add the Moreflow MCP URL and complete the OAuth 2.1 consent flow; no bearer token is pasted. `https://api.moreflow.io/mcp` Or use the one-click install link: [Claude.ai](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Moreflow&connectorUrl=https%3A%2F%2Fapi.moreflow.io%2Fmcp)

### Claude Code

Use this static bearer installation snippet and replace the token placeholder locally: `claude mcp add --transport http moreflow https://api.moreflow.io/mcp --header "Authorization: Bearer {{MOREFLOW_API_TOKEN}}"`

### ChatGPT

Add the Moreflow MCP URL and complete the OAuth 2.1 consent flow; no bearer token is pasted. `https://api.moreflow.io/mcp`

### Cursor

Use this static bearer installation snippet and replace the token placeholder locally: `{
  "mcpServers": {
    "moreflow": {
      "url": "https://api.moreflow.io/mcp",
      "headers": {
        "Authorization": "Bearer {{MOREFLOW_API_TOKEN}}"
      }
    }
  }
}` Or use the one-click install link: [Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=moreflow&config=eyJ1cmwiOiJodHRwczovL2FwaS5tb3JlZmxvdy5pby9tY3AiLCJoZWFkZXJzIjp7IkF1dGhvcml6YXRpb24iOiJCZWFyZXIge3tNT1JFRkxPV19BUElfVE9LRU59fSJ9fQ%3D%3D)

### VS Code

Use this static bearer installation snippet and replace the token placeholder locally: `{
  "servers": {
    "moreflow": {
      "type": "http",
      "url": "https://api.moreflow.io/mcp",
      "headers": {
        "Authorization": "Bearer {{MOREFLOW_API_TOKEN}}"
      }
    }
  }
}` Or use the one-click install link: [VS Code](vscode:mcp/install?%7B%22name%22%3A%22moreflow%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fapi.moreflow.io%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20%7B%7BMOREFLOW_API_TOKEN%7D%7D%22%7D%7D)

After connection, initialize the session, call `list_operations`, use `query_operation` for reads, and call `describe_operation` for the exact operation you need before using the write tool it names for a mutation. Preserve the canonical error envelope, safety metadata, and idempotency rules inside MCP just as you would for HTTP.

<a id="sdks"></a>

## SDKs and agent plugins

The browser SDKs call the same public operations as this guide and contain no UI, so you keep your own markup and styles. Each README has a complete example. They call https://api.moreflow.io directly from the visitor’s browser: the public form, chatbot, and booking endpoints answer CORS requests from any site, so you need no proxy and no API token. Never put an API token in browser code; `@moreflow/api` is a server-only client for token-authenticated calls.

Route the browser SDKs through your own server only when your Content Security Policy cannot list the API host. Preserving per-visitor rate limiting would require forwarding the real client IP through infrastructure Moreflow trusts. Cloudflare currently rejects caller-supplied CF-Connecting-IP and the public API does not trust X-Forwarded-For, so all visitors share one rate limit at your server’s IP. Forward the browser’s `Origin` header unchanged: dropping or spoofing it defeats Allowed Domains enforcement (forms reject a missing Origin; chatbots skip the allowlist without Origin or Referer). Never set a fixed Origin or add an API token in the proxy.

| Package | Purpose | Install | Docs |
| --- | --- | --- | --- |
| [@moreflow/forms](https://www.npmjs.com/package/@moreflow/forms) | Load, validate, and submit a custom Moreflow form, including Turnstile, the honeypot, and phone verification. | `npm install @moreflow/forms@0.9.2` | [README](https://github.com/moreflow-io/forms#readme) |
| [@moreflow/meetings](https://www.npmjs.com/package/@moreflow/meetings) | Build a native booking widget: meeting type, slots, reservation, and booking. | `npm install @moreflow/meetings@0.2.2` | [README](https://github.com/moreflow-io/meetings#readme) |
| [@moreflow/chatbots](https://www.npmjs.com/package/@moreflow/chatbots) | Build a native chatbot client with visitor and thread continuity. | `npm install @moreflow/chatbots@0.4.1` | [README](https://github.com/moreflow-io/chatbots#readme) |
| [@moreflow/tracking](https://www.npmjs.com/package/@moreflow/tracking) | Capture acquisition data (click ids, UTM, referrer, Meta and TikTok cookies); forms, meetings, and chatbots use it only with acquisitionTracking consent and otherwise send just the page URL's UTM parameters and click ids, without cookies or storage. | `npm install @moreflow/tracking@0.2.0` | [README](https://github.com/moreflow-io/tracking#readme) |
| [@moreflow/api](https://www.npmjs.com/package/@moreflow/api) | SERVER-ONLY typed client for every public operation, generated from the contract snapshots. Never ship its token to browsers. | `npm install @moreflow/api@0.2.0` | [README](https://github.com/moreflow-io/api#readme) |
| `@moreflow/analytics` | Website analytics and A/B testing: loads the moreflow tracker, queues events, and reads experiment variants, with optional React bindings. | Not published yet. | [README](https://github.com/moreflow-io/analytics#readme) |
| `@moreflow/site-kit` | Sections, SEO helpers, and analytics bindings for TanStack Start websites on the moreflow site platform or your own Cloudflare account. | Not published yet. | [README](https://github.com/moreflow-io/site-kit#readme) |

Use `baseUrl` for the API origin and `fetchImpl` for a custom transport across SDKs. Browser clients default to https://api.moreflow.io; use `baseUrl: ''` only for the optional CSP proxy described above. Their acquisition override is `trackingCapture`: an options object or a callback returning that SDK’s tracking metadata, called only while `acquisitionTracking` allows capture. Explicit payload tracking remains independent. Forms can bind these defaults once with `createFormsClient({ baseUrl, acquisitionTracking, trackingCapture, fetchImpl })`. Server code passes a static bearer or OAuth access token through `createMoreflowApiClient({ token, fetchImpl, baseUrl })`; never expose that token to a browser. A server client using an empty base URL needs a custom fetch implementation that resolves relative URLs to a trusted origin.

The agent plugins connect the [hosted MCP server](/v1/operations/docs/guide#mcp) and add skills for reading records, onboarding a client, quote to invoice, and scheduling a meeting. Install the Claude Code plugin with `claude plugin install moreflow --marketplace moreflow-io/agent-plugins`, then run `/mcp` to sign in. For Codex run `codex plugin marketplace add moreflow-io/agent-plugins` and `codex plugin add moreflow@moreflow`. Source and issues: [moreflow-io/agent-plugins](https://github.com/moreflow-io/agent-plugins).
