> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cleo-pay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect AI Agents to Cleo Pay with the MCP Server

> Connect Claude Code, Cursor, VS Code, or any MCP client to the Cleo Pay MCP server. Authenticate with an API key and manage payables, payments, contacts, and bank accounts.

Cleo Pay runs a hosted **Model Context Protocol (MCP) server** that lets AI agents operate your Cleo Pay account with the same capabilities as the REST API. Connect Claude Code, Cursor, VS Code, or any MCP client that supports Streamable HTTP, and your agent can list and create payables, send ACH payments, manage vendor contacts, and link bank accounts — limited to exactly the scopes you grant its API key.

The server is remote and stateless: there is nothing to install or host. Point your client at the endpoint, pass your API key in the `Authorization` header, and it's ready.

<Note>
  New to Cleo Pay? Read the [Quickstart](/quickstart) first — the MCP tools operate on the same payables, payments, contacts, and bank accounts as the REST API.
</Note>

***

## Environments

| Environment | MCP endpoint                   | API key prefix | Money movement                  |
| ----------- | ------------------------------ | -------------- | ------------------------------- |
| **Sandbox** | `https://mcp.stg.cleo-pay.com` | `cleo_test_`   | Simulated — no real money moves |
| **Live**    | `https://mcp.cleo-pay.com`     | `cleo_live_`   | Real ACH transfers              |

Both environments also accept an explicit path form: `https://api.stg.cleo-pay.com/v1/mcp` (Sandbox) and `https://api.cleo-pay.com/v1/mcp` (Live).

API keys are **environment-bound**: a `cleo_test_` key only authenticates against Sandbox, and a `cleo_live_` key only against Live. Sandbox is a fully separate environment with its own data — nothing you create there appears in Live, and payments run against a simulated banking network.

<Note>
  Start in Sandbox. Wire up your agent with a `cleo_test_` key, rehearse your flows end to end, then switch the endpoint and key to go live.
</Note>

***

## Get an API key

<Steps>
  <Step title="Create a key in the dashboard">
    In the Cleo Pay dashboard, go to **Settings → Developers** and click **Create API key**. You need admin access on the business. Give the key a name, select the scopes it should have (see [Scopes and tool visibility](#scopes-and-tool-visibility) below), and optionally set an expiration date.
  </Step>

  <Step title="Copy the secret">
    The full key (for example `cleo_test_a1b2c3d4e5f6g7h8_...`) is shown **once**, at creation time. Store it in a secrets manager or environment variable — it cannot be retrieved again. If you lose it, rotate the key to mint a new secret.
  </Step>
</Steps>

<Warning>
  Treat API keys like passwords. Anyone holding the key can act on your business within its scopes. Never commit keys to version control, and rotate immediately if you suspect a leak — rotation atomically mints a new secret and revokes the old one.
</Warning>

Your business must have completed identity verification before API keys will authenticate. Until verification is finished, requests are rejected with `401 Unauthorized`.

***

## Connect your client

The examples below use the Sandbox endpoint and a `cleo_test_` key. For Live, swap in `https://mcp.cleo-pay.com` and a `cleo_live_` key.

<Tabs>
  <Tab title="Claude Code">
    ```bash theme={null}
    claude mcp add --transport http cleo https://mcp.stg.cleo-pay.com \
      --header "Authorization: Bearer cleo_test_YOUR_KEY"
    ```

    Then ask Claude something like *"list my Cleo payables"* — it discovers and calls the tools automatically.
  </Tab>

  <Tab title="Cursor">
    Add to `~/.cursor/mcp.json`:

    ```json theme={null}
    {
      "mcpServers": {
        "cleo": {
          "url": "https://mcp.stg.cleo-pay.com",
          "headers": {
            "Authorization": "Bearer cleo_test_YOUR_KEY"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="VS Code">
    Add to `.vscode/mcp.json`:

    ```json theme={null}
    {
      "servers": {
        "cleo": {
          "type": "http",
          "url": "https://mcp.stg.cleo-pay.com",
          "headers": {
            "Authorization": "Bearer cleo_test_YOUR_KEY"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="curl">
    The server speaks standard JSON-RPC over Streamable HTTP. List the tools available to your key:

    ```bash theme={null}
    curl -s -X POST https://mcp.stg.cleo-pay.com \
      -H "Authorization: Bearer cleo_test_YOUR_KEY" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    ```
  </Tab>
</Tabs>

<Warning>
  MCP client config files contain your API key in plain text. Keep them out of version control — in particular, add `.vscode/mcp.json` to `.gitignore` if you put a key in it, or use your client's secret-input mechanism.
</Warning>

Any MCP client that supports **Streamable HTTP with custom headers** works. Clients that only support OAuth-based remote connectors (such as Claude Desktop's one-click connectors) are not yet supported.

***

## Scopes and tool visibility

Each tool requires a scope. The server only advertises the tools your key is allowed to call — `tools/list` omits everything else, so your agent never sees a tool it can't use. Calling an out-of-scope tool directly returns a JSON-RPC *Access denied* error.

| Scope                 | Tools unlocked                                                             |
| --------------------- | -------------------------------------------------------------------------- |
| `payables:read`       | `list_payables`, `get_payable`                                             |
| `payables:write`      | `create_payable`                                                           |
| `payments:initiate`   | `create_payment`                                                           |
| `contacts:read`       | `list_contacts`, `get_contact`                                             |
| `contacts:write`      | `create_contact`, `update_contact`, `archive_contact`, `unarchive_contact` |
| `bank-accounts:read`  | `list_bank_accounts`, `get_bank_account`                                   |
| `bank-accounts:write` | `add_bank_account`, `verify_micro_deposits`, `remove_bank_account`         |

Grant the minimum scopes your integration needs. A read-only agent (reporting, reconciliation) needs only the `:read` scopes; keep `payments:initiate` on a separate, tightly-held key.

Requests are rate-limited to **100 requests per minute per API key**. Exceeding the limit returns HTTP `429`.

***

## Tools

All tools return JSON. Amounts are integers in **cents**; dates use `YYYY-MM-DD`.

### Payables

| Tool             | Scope            | Description                                                                 |
| ---------------- | ---------------- | --------------------------------------------------------------------------- |
| `list_payables`  | `payables:read`  | List payables (bills) with pagination, status filter, and free-text search. |
| `get_payable`    | `payables:read`  | Fetch a single payable, including line items.                               |
| `create_payable` | `payables:write` | Create a **draft** payable. Always a draft — never auto-sent or auto-paid.  |

<AccordionGroup>
  <Accordion title="list_payables — parameters">
    | Parameter  | Type          | Required | Description                                                                    |
    | ---------- | ------------- | -------- | ------------------------------------------------------------------------------ |
    | `page`     | integer ≥ 1   | No       | Page number. Default `1`.                                                      |
    | `pageSize` | integer 1–100 | No       | Items per page. Default `25`.                                                  |
    | `status`   | enum          | No       | One of `draft`, `pending_approval`, `approved`, `declined`, `blocked`, `void`. |
    | `search`   | string        | No       | Free-text search.                                                              |

    Returns `items` (without line items) and `totalCount`.
  </Accordion>

  <Accordion title="get_payable — parameters">
    | Parameter   | Type   | Required | Description       |
    | ----------- | ------ | -------- | ----------------- |
    | `payableId` | string | Yes      | The payable's ID. |

    Returns the payable with its full line items.
  </Accordion>

  <Accordion title="create_payable — parameters">
    | Parameter          | Type         | Required | Description                                                                     |
    | ------------------ | ------------ | -------- | ------------------------------------------------------------------------------- |
    | `contact.name`     | string       | Yes      | Vendor name shown on the draft (free text — not linked to a contact record).    |
    | `contact.email`    | string       | No       | Vendor email.                                                                   |
    | `totalAmountCents` | integer ≥ 1  | Yes      | Total amount in cents.                                                          |
    | `dueDate`          | `YYYY-MM-DD` | No       | Due date.                                                                       |
    | `issueDate`        | `YYYY-MM-DD` | No       | Issue date.                                                                     |
    | `customNumber`     | string       | No       | Your invoice/bill number.                                                       |
    | `description`      | string       | No       | Memo.                                                                           |
    | `items`            | array        | No       | Line items: `name`, `description`, `quantity` (default `1`), `unitAmountCents`. |

    **Not idempotent** — a retry creates a second draft. See [Idempotency and retries](#idempotency-and-retries).
  </Accordion>
</AccordionGroup>

### Payments

| Tool             | Scope               | Description                                                                       |
| ---------------- | ------------------- | --------------------------------------------------------------------------------- |
| `create_payment` | `payments:initiate` | Initiate an ACH payment — pay an existing payable, or send directly to a contact. |

<Warning>
  In the Live environment, `create_payment` **moves real money**. Scope keys carefully and rehearse in Sandbox first.
</Warning>

<AccordionGroup>
  <Accordion title="create_payment — parameters">
    | Parameter        | Type               | Required    | Description                                                                                   |
    | ---------------- | ------------------ | ----------- | --------------------------------------------------------------------------------------------- |
    | `idempotencyKey` | string, 1–64 chars | Yes         | Unique key for this payment attempt. Retrying with the same key is safe and never pays twice. |
    | `payableId`      | UUID               | One of      | Pay an existing payable. Provide exactly one of `payableId` or `contactId`.                   |
    | `contactId`      | UUID               | One of      | Send money directly to a contact.                                                             |
    | `amountCents`    | integer ≥ 1        | Conditional | Required with `contactId`. Optional with `payableId` to make a partial payment.               |
    | `bankAccountId`  | UUID               | Yes         | Verified bank account that funds the payment.                                                 |
    | `clearing`       | enum               | No          | `STANDARD` or `NEXT_AVAILABLE`.                                                               |
    | `scheduledAt`    | `YYYY-MM-DD`       | No          | Schedule for a future date — executes at 9:00 AM New York time.                               |
    | `description`    | string             | No          | Payment memo.                                                                                 |
    | `customNumber`   | string ≤ 21 chars  | No          | Custom payment reference.                                                                     |
    | `categoryId`     | UUID               | No          | Accounting category (direct-to-contact payments only).                                        |

    Returns the affected `payableId` and a `payments` array — large amounts may split into multiple ACH transfers. Payment statuses: `pending`, `scheduled`, `processing`, `held`, `completed`, `failed`, `cancelled`, `refunded`.

    Payments to a payee without a linked bank account are **held** until the payee provides their details; scheduling is not supported for held payments.
  </Accordion>
</AccordionGroup>

### Bank accounts

| Tool                    | Scope                 | Description                                                                                   |
| ----------------------- | --------------------- | --------------------------------------------------------------------------------------------- |
| `list_bank_accounts`    | `bank-accounts:read`  | List linked bank accounts (masked to last 4 digits).                                          |
| `get_bank_account`      | `bank-accounts:read`  | Fetch one bank account, including removed ones.                                               |
| `add_bank_account`      | `bank-accounts:write` | Link an account by account + routing number. Micro-deposit verification starts automatically. |
| `verify_micro_deposits` | `bank-accounts:write` | Confirm the two micro-deposit amounts to finish verification.                                 |
| `remove_bank_account`   | `bank-accounts:write` | Unlink a bank account.                                                                        |

<AccordionGroup>
  <Accordion title="add_bank_account — parameters">
    | Parameter       | Type                | Required | Description                                              |
    | --------------- | ------------------- | -------- | -------------------------------------------------------- |
    | `holderName`    | string              | Yes      | Legal name of the account holder.                        |
    | `name`          | string              | Yes      | Friendly label (alphanumeric, commas, periods, hyphens). |
    | `accountNumber` | string, digits only | Yes      | Full account number. **Write-only** — never echoed back. |
    | `routingNumber` | string, 9 digits    | Yes      | ABA routing number. **Write-only**.                      |
    | `type`          | enum                | Yes      | `checking` or `savings`.                                 |

    The account starts in `pending_verification`; it becomes usable after `verify_micro_deposits` succeeds. **Not idempotent** — do not auto-retry on timeout.
  </Accordion>

  <Accordion title="verify_micro_deposits — parameters">
    | Parameter       | Type                  | Required | Description                             |
    | --------------- | --------------------- | -------- | --------------------------------------- |
    | `bankAccountId` | string                | Yes      | The pending bank account's ID.          |
    | `amount1`       | string, e.g. `"0.05"` | Yes      | First micro-deposit amount in dollars.  |
    | `amount2`       | string, e.g. `"0.07"` | Yes      | Second micro-deposit amount in dollars. |

    Maximum **3 attempts** — a wrong pair consumes one. After 3 failures the account becomes permanently `unverified`.
  </Accordion>

  <Accordion title="list_bank_accounts / get_bank_account / remove_bank_account — parameters">
    `list_bank_accounts` takes no parameters and returns all linked accounts.

    `get_bank_account` and `remove_bank_account` take a single required `bankAccountId` (string).

    The business's **default verified account cannot be removed** — set another account as default in the Cleo Pay dashboard first. Account statuses: `pending_verification`, `verified`, `unverified`, `removed`.
  </Accordion>
</AccordionGroup>

### Contacts

| Tool                | Scope            | Description                                                                       |
| ------------------- | ---------------- | --------------------------------------------------------------------------------- |
| `list_contacts`     | `contacts:read`  | List contacts with pagination, type/status filters, and search.                   |
| `get_contact`       | `contacts:read`  | Fetch one contact, including bank-account and tax-info summaries.                 |
| `create_contact`    | `contacts:write` | Create a vendor/payee, optionally with a receiving bank account and W-9 tax info. |
| `update_contact`    | `contacts:write` | Update a contact's editable fields.                                               |
| `archive_contact`   | `contacts:write` | Archive a contact (idempotent).                                                   |
| `unarchive_contact` | `contacts:write` | Restore an archived contact (idempotent).                                         |

<AccordionGroup>
  <Accordion title="list_contacts — parameters">
    | Parameter  | Type          | Required | Description                                                              |
    | ---------- | ------------- | -------- | ------------------------------------------------------------------------ |
    | `page`     | integer ≥ 1   | No       | Page number. Default `1`.                                                |
    | `pageSize` | integer 1–100 | No       | Items per page. Default `25`.                                            |
    | `type`     | enum          | No       | `business` or `payee`.                                                   |
    | `status`   | enum          | No       | `active`, `archived`, or `revoked`. Default returns active and archived. |
    | `search`   | string        | No       | Free-text search.                                                        |
  </Accordion>

  <Accordion title="create_contact — parameters">
    | Parameter         | Type                   | Required | Description                                                                                                                                       |
    | ----------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `displayName`     | string                 | Yes      | Contact display name.                                                                                                                             |
    | `emails`          | array of emails, max 5 | No       | Contact emails.                                                                                                                                   |
    | `phone`           | string                 | No       | Phone number.                                                                                                                                     |
    | `address`         | object                 | No       | `streetOne`, `streetTwo`, `city`, `stateCode` (2 letters), `zipCode`.                                                                             |
    | `bankAccount`     | object                 | No       | Receiving account: `accountNumber` (digits, **write-only**), `routingNumber` (9 digits, **write-only**), `accountType` (`checking` or `savings`). |
    | `taxInfo`         | object                 | No       | W-9 info: `legalName`, `taxIdType` (`ein` or `ssn`), `taxId` (**write-only** — only the last 4 are ever returned).                                |
    | `allowDuplicates` | boolean                | No       | Default `false`. A likely duplicate (matching email or name) is rejected unless set to `true`.                                                    |

    **Not idempotent** — do not auto-retry on timeout.
  </Accordion>

  <Accordion title="update_contact / archive_contact / unarchive_contact — parameters">
    All three take a required `contactId` (string).

    `update_contact` additionally accepts: `displayName`, `emails` (replaces **all** emails, max 5), `phone`, `address` — these apply to **payee-type contacts only** — and `netTerms` (integer ≥ 0), which applies to any contact.

    `archive_contact` and `unarchive_contact` are idempotent — safe to call on an already-archived or already-active contact. Revoked contacts cannot be unarchived.
  </Accordion>
</AccordionGroup>

***

## Idempotency and retries

Agents retry things. Here is what is safe:

| Tools                                                  | Retry behavior                                                                                                                          |
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `create_payment`                                       | **Safe** — the required `idempotencyKey` guarantees the same key never pays twice. Reuse the same key when retrying a timed-out call.   |
| `create_payable`, `add_bank_account`, `create_contact` | **Not safe** — a retry creates a second record. On timeout, check whether the record exists (e.g. `list_payables`) before trying again. |
| `archive_contact`, `unarchive_contact`                 | **Safe** — idempotent by design.                                                                                                        |
| All `list_*` / `get_*` tools                           | **Safe** — read-only.                                                                                                                   |

***

## Errors

| Scenario                                                                        | What you get                                                                           |
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| Missing `Authorization` header                                                  | HTTP `401` — `Missing API key`                                                         |
| Invalid, expired, revoked, or wrong-environment key                             | HTTP `401` — `Invalid API key`                                                         |
| Rate limit exceeded                                                             | HTTP `429` (100 requests/minute per key)                                               |
| MCP server temporarily disabled                                                 | HTTP `503` — `Partner API disabled`                                                    |
| Tool call without the required scope                                            | JSON-RPC error `-32600` — `Access denied`                                              |
| Unknown tool name                                                               | JSON-RPC error `-32601`                                                                |
| Invalid parameters or business error (validation, not found, duplicate contact) | Successful JSON-RPC response with `isError: true` and a message describing the problem |

Remember that tools outside your key's scopes don't appear in `tools/list` at all — if a tool seems missing, check the key's scopes.

***

## Data protection

* Bank account and routing numbers are **write-only**: responses only ever include the last 4 digits.
* Tax IDs are **write-only**: responses include a mask (for example `***6789`).
* Responses are explicit allowlists — internal fields are never serialized.
* Every write is recorded in an append-only action log attributed to your API key, with request ID and IP address.

***

## Next steps

* Rehearse a full flow in Sandbox: [link a bank account](/guides/link-bank-account), [create a contact](/guides/manage-contacts), then [send a payment](/guides/send-a-payment).
* Subscribe to [webhooks](/reference/webhooks) to track payment and verification events your agent kicks off.
* Prefer raw REST? The same capabilities are in the [API Reference](/api-reference/payables/list).
