[Cotlet](https://cotlet.dev/index.md) · [Integrations](https://cotlet.dev/integrations.md) · [How it works](https://cotlet.dev/how-it-works.md) · [Use cases](https://cotlet.dev/use-cases.md) · [Security](https://cotlet.dev/security.md) · [For developers](https://cotlet.dev/for-developers.md) · [For automation agencies](https://cotlet.dev/for-agencies.md) · [Blog](https://cotlet.dev/blog.md) · [API docs](https://cotlet.dev/docs.md) · [Help center](https://cotlet.dev/help.md) · [Changelog](https://cotlet.dev/changelog.md) · [Contact](https://cotlet.dev/contact.md) · [Pricing](https://cotlet.dev/pricing.md) · [Sign up](https://cotlet.dev/signup)

# Pennylane API and MCP server

> Yes, Pennylane has a public REST API (Company API v2) with read and write scopes for customers, customer and supplier invoices, transactions, categories and ledger entries (checked 14 Oct 2026). It needs the Essentiel plan or higher and an API token, and the official Pennylane MCP server is read-only for now: it cannot create or send an invoice.

French accounting and invoicing platform for small businesses and accounting firms: customer and supplier invoices, quotes, bank transactions, categories and the ledger in one account.

## What is the Pennylane API?

Cotlet's Pennylane API exposes 14 endpoints for your own Pennylane account: list customers, list customer invoices, get a customer invoice, list supplier invoices, list suppliers, list bank transactions, list categories, create a customer, create a customer invoice, finalize a customer invoice, send a customer invoice, import a supplier invoice, categorise a bank transaction and match a transaction to an invoice. The list_customers endpoint returns customers in your Pennylane account with name, type and email, optionally filtered by name. Each endpoint takes simple JSON inputs like limit, cursor or invoice_id and works both as an operation of the pennylane_read or pennylane_write MCP tool for agents and as a REST call for scripts.

- **Category:** [Invoicing & freelance CRM](https://cotlet.dev/integrations/category/invoicing-freelance-crm.md)
- **Endpoints:** 14 (7 read, 7 write)
- **Updated:** 2026-10-18

Yes, Pennylane has a public REST API (Company API v2) with read and write scopes for customers, customer and supplier invoices, transactions, categories and ledger entries (checked 14 Oct 2026). Cotlet is a hosted MCP server and REST API for Pennylane with 14 endpoints (7 read, 7 write): you sign in once in your own cloud browser, your AI agent does the work.

## Does Pennylane have an official API?

Yes, Pennylane has a public REST API (Company API v2) with read and write scopes for customers, customer and supplier invoices, transactions, categories and ledger entries (checked 14 Oct 2026). It needs the Essentiel plan or higher and an API token, and the official Pennylane MCP server is read-only for now: it cannot create or send an invoice.

| Task | Pennylane official | Cotlet |
|---|---|---|
| Create customers | Yes: customers:all scope | Yes |
| Create customer invoices, as draft or final | Yes: customer_invoices:all scope; amounts are sent as strings | Yes |
| Finalize a draft invoice | Yes: A finalized invoice can no longer be edited | Yes |
| Send an invoice by email | Yes: No CC or BCC; returns 409 until the PDF is generated | Yes |
| List unpaid and late invoices | Partly: Status and remaining amount are returned, but the list has no status or due date filter | Yes |
| Import a supplier invoice PDF | Yes: PDF only; the same file imported twice is rejected | Yes |
| Categorise a bank transaction | Yes: transactions:all scope; weights in one category group sum to 1 | Yes |
| Match a bank transaction to an invoice | Yes: Matched transactions endpoint, for payments made outside Pennylane | Yes |

Sources: [Pennylane API: Introduction](https://pennylane.readme.io/docs/getting-started), [Pennylane API: Generating a Company API token](https://pennylane.readme.io/docs/generating-my-api-token), [Pennylane API: Scopes](https://pennylane.readme.io/docs/v2-scopes), [Pennylane MCP: Getting started](https://pennylane-mcp.readme.io/docs/getting-started)

## For AI agents

```text
Yes, Pennylane has a public REST API (Company API v2) with read and write scopes for customers, customer and supplier invoices, transactions, categories and ledger entries (checked 14 Oct 2026). It needs the Essentiel plan or higher and an API token, and the official Pennylane MCP server is read-only for now: it cannot create or send an invoice.
Cotlet provides a hosted MCP server and REST API for Pennylane: 14 tools (7 read, 7 write).
Access       Sign up at https://cotlet.dev/app/signup?app=pennylane, then request the app and create API keys in the Cotlet console.

MCP server   https://cotlet.dev/mcp/v1
MCP tools    pennylane_read, pennylane_write, reads and writes apart; call one with {"operation": "<operation>", "arguments": {...}}
REST base    https://cotlet.dev/api/v1/pennylane, each operation at its own route, e.g. POST /api/v1/pennylane/customers/{customer_id}/invoice
Auth         Authorization: Bearer <COTLET_API_KEY>. Listing endpoints and schemas needs no key; calling an endpoint needs one. Over MCP the input schemas come with sign-in.
Operations   list_customers, list_customer_invoices, get_customer_invoice, list_supplier_invoices, list_suppliers, list_transactions, list_categories, create_customer, create_customer_invoice, finalize_customer_invoice, send_customer_invoice, import_supplier_invoice, categorise_transaction, match_transaction
Confirm      create_customer, create_customer_invoice, finalize_customer_invoice, send_customer_invoice, import_supplier_invoice, match_transaction (by default the user approves each call; the user can switch this off per tool, e.g. for scheduled automations)
Markdown     https://cotlet.dev/integrations/pennylane-api-mcp-server.md

How access works:
1. The user signs up at https://cotlet.dev/app/signup?app=pennylane, requests Pennylane in the Cotlet console and creates an API key there.
2. The user signs in to Pennylane once in their Cotlet Cloud Browser (their own Chrome in the cloud, opened from the console) and enables Pennylane for agents. An agent cannot do this step.
3. The API key goes into the MCP client or the REST call as a bearer token.

Fits: people who want a hosted Pennylane server that someone else maintains, including non-developers. Developers who prefer to run and fix code on their own computer can use an open-source Pennylane MCP server instead.

Policy: Cotlet never stores passwords, never solves CAPTCHAs, never bypasses MFA.
Company: legal details in the Terms of service at https://cotlet.dev/terms. Contact contact@cotlet.dev.
Sign-in: the user signs in to Pennylane in their own Cloud Browser, a Chrome Cotlet runs for them in the EU; password saving is off, the profile is AES-256-GCM encrypted, and agents get Pennylane only after the user enables it. Calls are requests from that browser's IP with its cookies, each listed in the user's Background activity. Disable for agents, signing out in the browser or Reset browser cuts access. Cotlet is run by Take Group sp. z o.o. (EU VAT PL6342984273).
API key: holds no Pennylane login or cookies, only asks Cotlet to run a tool. Each key is scoped to chosen apps and tools, read only or read and write. Actions that reach other people wait for the user's approval by default, so a leaked key cannot post or message as the user. Every call is in the activity log; deleting the key ends access at once.
Terms: the user stays responsible for following Pennylane's terms; Cotlet is not affiliated with Pennylane.
```

## What can an AI agent do in Pennylane?

Cotlet is best for small businesses and bookkeepers who send invoices, chase late payments and categorise bank transactions in Pennylane without running or fixing a server.

- **[Create and send invoices from a billing list](https://cotlet.dev/integrations/pennylane-api-mcp-server/create-and-send-invoices.md)**: "Here is this month's billing list with customer, hours and rate. For each line, create the customer in Pennylane if it does not exist, create a draft invoice due in 30 days and show me the drafts. When I say go, finalize and email them." → `list_customers`, `create_customer`, `create_customer_invoice`, `finalize_customer_invoice`, `send_customer_invoice`
- **[Follow up on unpaid customer invoices](https://cotlet.dev/integrations/pennylane-api-mcp-server/follow-up-on-unpaid-invoices.md)** (runs: weekly): "Every Monday, list the Pennylane customer invoices that are late. Group them by customer with the amount still owed, look in last week's bank transactions for a credit I may have missed, and resend the invoice by email only to the customers I name." → `list_customer_invoices`, `get_customer_invoice`, `list_transactions`, `send_customer_invoice`
- **[Categorise bank transactions](https://cotlet.dev/integrations/pennylane-api-mcp-server/categorise-bank-transactions.md)** (runs: weekly): "List last week's bank transactions in Pennylane that have no category. Pick the category from the ones I already use by the transaction label, categorise the clear ones, and give me the doubtful ones with your best guess." → `list_categories`, `list_transactions`, `categorise_transaction`
- **Import supplier invoices from a folder**: "Take the supplier invoice PDFs in this folder. For each one, find the supplier in Pennylane, import the PDF with the date, due date and amounts printed on it, and tell me about any file Pennylane rejects." → `list_suppliers`, `import_supplier_invoice`
- **Match bank transfers to invoices** (runs: weekly): "Find customer invoices in Pennylane that are late. For each, look in last week's bank transactions for a credit with the invoice number or the exact amount, and match the ones you are sure about." → `list_customer_invoices`, `list_transactions`, `match_transaction`

## Can Pennylane automations run on a schedule in n8n, Make or Zapier?

Yes. Cotlet does not replace your automation platform: it gives it a Pennylane connection it does not have, with the same API key as your agent. Write actions ask for your OK by default; switch that off in your dashboard for the tools you choose, for example to follow up on unpaid customer invoices, and the schedule runs on its own. Cotlet runs in the cloud, so it keeps going while your computer is off.

| Where | How to connect | How to schedule |
|---|---|---|
| Your AI agent | Add https://cotlet.dev/mcp/v1 with your API key. | A scheduled agent run, for example a Claude Code routine. |
| n8n | MCP Client node with https://cotlet.dev/mcp/v1, or an HTTP Request node with the endpoint's method and URL, such as GET https://cotlet.dev/api/v1/pennylane/customer-invoices. | Schedule Trigger node. |
| Make | HTTP module: the endpoint's method and URL with an Authorization: Bearer header, and a JSON body for POST, PUT and PATCH. | Scenario schedule. |
| Zapier | Webhooks by Zapier, Custom Request: the same method, URL, header and body. | Schedule by Zapier trigger. |
| Your own script | GET https://cotlet.dev/api/v1/pennylane/customer-invoices with your API key. | cron or GitHub Actions. |

## Tools

**For AI agents:** Every endpoint and its schema is public. The 14 endpoints on this page, with parameters and example responses, can be listed without an account. Calling one returns 401 without an API key and 403 while the key has no access to Pennylane.

### list_customers

Customers in your Pennylane account with name, type and email, optionally filtered by name. Access: read. REST: `GET /api/v1/pennylane/customers`. MCP: `pennylane_read → list_customers`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Only customers whose name contains this text. |
| `limit` | integer | no | 1 to 100. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

Returns the customers in the Pennylane account you signed in with, company and individual customers alike. Use customer_id from each item in create_customer_invoice and list_customer_invoices. Pages hold 1 to 100 customers; pass next_cursor back as cursor for the next page.

Example response:

```json
{
  "customers": [
    {
      "customer_id": 4021,
      "name": "Atelier Lumen SARL",
      "type": "company",
      "email": "compta@atelier-lumen.example"
    },
    {
      "customer_id": 4034,
      "name": "Studio Marais SAS",
      "type": "company",
      "email": "factures@studio-marais.example"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### list_customer_invoices

Customer invoices with number, customer, amounts, status and due date, newest first. Access: read. REST: `GET /api/v1/pennylane/customer-invoices`. MCP: `pennylane_read → list_customer_invoices`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `status` | enum | no | draft, upcoming, late, partially_paid or paid. Default: all. |
| `customer_id` | integer | no | Customer ID from list_customers. |
| `date_from` | date | no | Only invoices dated on or after this day. |
| `date_to` | date | no | Only invoices dated on or before this day. |
| `external_reference` | string | no | Only the invoice created with this external reference. |
| `limit` | integer | no | 1 to 100. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

Lists customer invoices, newest first. Status is one of draft, upcoming, late, partially_paid or paid, and remaining_amount_with_tax is the amount still owed on that invoice. Pennylane's own invoice list cannot filter by status or due date, so Cotlet applies the status filter for you. Pages hold 1 to 100 invoices; pass next_cursor back as cursor for the next page.

Example response:

```json
{
  "invoices": [
    {
      "invoice_id": 118342,
      "invoice_number": "F-2026-0148",
      "customer": {
        "customer_id": 4021,
        "name": "Atelier Lumen SARL"
      },
      "date": "2026-09-15",
      "deadline": "2026-09-30",
      "amount": "2400.00",
      "remaining_amount_with_tax": "2400.00",
      "status": "late",
      "currency": "EUR"
    },
    {
      "invoice_id": 118301,
      "invoice_number": "F-2026-0139",
      "customer": {
        "customer_id": 4034,
        "name": "Studio Marais SAS"
      },
      "date": "2026-09-20",
      "deadline": "2026-10-05",
      "amount": "960.00",
      "remaining_amount_with_tax": "960.00",
      "status": "late",
      "currency": "EUR"
    },
    {
      "invoice_id": 118277,
      "invoice_number": "F-2026-0131",
      "customer": {
        "customer_id": 4012,
        "name": "Brasserie du Port"
      },
      "date": "2026-09-23",
      "deadline": "2026-10-08",
      "amount": "1368.00",
      "remaining_amount_with_tax": "1368.00",
      "status": "late",
      "currency": "EUR"
    }
  ],
  "total_remaining_with_tax": "4728.00",
  "has_more": false,
  "next_cursor": null
}
```

### get_customer_invoice

One customer invoice with its lines, payments and matched bank transactions. Access: read. REST: `GET /api/v1/pennylane/invoices/{invoice_id}/customer-invoice`. MCP: `pennylane_read → get_customer_invoice`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | integer | yes | Invoice ID from list_customer_invoices or create_customer_invoice. |

Returns one invoice with every line, the payments Pennylane recorded and the bank transactions matched to it. A payment is one made through Pennylane, such as the payment link; a matched transaction is one made outside Pennylane, such as a bank transfer. An invoice sent through e-invoicing also returns its e-invoicing status.

Example response:

```json
{
  "invoice_id": 118277,
  "invoice_number": "F-2026-0131",
  "customer": {
    "customer_id": 4012,
    "name": "Brasserie du Port"
  },
  "status": "late",
  "deadline": "2026-10-08",
  "amount": "1368.00",
  "remaining_amount_with_tax": "1368.00",
  "lines": [
    {
      "label": "Menu consulting, September",
      "quantity": 12,
      "unit": "hour",
      "unit_price": "95.00",
      "vat_rate": 20,
      "amount_before_tax": "1140.00"
    }
  ],
  "payments": [],
  "matched_transactions": [
    {
      "transaction_id": 5529874,
      "date": "2026-10-10",
      "label": "VIR BRASSERIE DU PORT F-2026-0131",
      "amount": "1368.00"
    }
  ]
}
```

### list_supplier_invoices

Supplier invoices with number, supplier, amounts and due date, newest first. Access: read. REST: `GET /api/v1/pennylane/supplier-invoices`. MCP: `pennylane_read → list_supplier_invoices`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `supplier_id` | integer | no | Supplier ID from list_suppliers. |
| `date_from` | date | no | Only invoices dated on or after this day. |
| `date_to` | date | no | Only invoices dated on or before this day. |
| `limit` | integer | no | 1 to 100. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

Lists the supplier invoices in Pennylane, newest first, with the supplier, the date, the due date and the total. Use it to check whether a PDF was already imported before you import it again. Pages hold 1 to 100 invoices; pass next_cursor back as cursor for the next page.

Example response:

```json
{
  "invoices": [
    {
      "invoice_id": 90412,
      "invoice_number": "2026-3381",
      "supplier": {
        "supplier_id": 612,
        "name": "OVH SAS"
      },
      "date": "2026-10-01",
      "deadline": "2026-10-31",
      "amount": "74.40",
      "currency": "EUR"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### list_suppliers

Suppliers in your Pennylane account with name and ID, optionally filtered by name. Access: read. REST: `GET /api/v1/pennylane/suppliers`. MCP: `pennylane_read → list_suppliers`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Only suppliers whose name contains this text. |
| `limit` | integer | no | 1 to 100. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

Returns the suppliers in the account. Use supplier_id from each item in import_supplier_invoice and list_supplier_invoices. IDs differ per Pennylane company, so look the supplier up again if you switch accounts. Pages hold 1 to 100 suppliers; pass next_cursor back as cursor for the next page.

Example response:

```json
{
  "suppliers": [
    {
      "supplier_id": 612,
      "name": "OVH SAS"
    },
    {
      "supplier_id": 640,
      "name": "Notion Labs Inc."
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### list_transactions

Bank transactions with date, label, amount, categories and matched invoices, newest first. Access: read. REST: `GET /api/v1/pennylane/transactions`. MCP: `pennylane_read → list_transactions`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `date_from` | date | no | Only transactions dated on or after this day. |
| `date_to` | date | no | Only transactions dated on or before this day. |
| `bank_account_id` | integer | no | Bank account ID from a previous list_transactions result. |
| `limit` | integer | no | 1 to 100. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

Lists bank transactions, newest first. Each item has the label from the bank, the amount in euros, the original currency amount, its categories with weights and the invoices matched to it. A transaction with attachment_required set to true still needs an invoice matched. Pennylane filters transactions by id, bank account, journal and date only. Pages hold 1 to 100 transactions; pass next_cursor back as cursor for the next page.

Example response:

```json
{
  "transactions": [
    {
      "transaction_id": 5530021,
      "date": "2026-10-09",
      "label": "PRLV OVH SAS 4417",
      "amount": "-74.40",
      "currency": "EUR",
      "bank_account_id": 18,
      "categories": [],
      "matched_invoices": [],
      "attachment_required": true
    },
    {
      "transaction_id": 5529874,
      "date": "2026-10-10",
      "label": "VIR BRASSERIE DU PORT F-2026-0131",
      "amount": "1368.00",
      "currency": "EUR",
      "bank_account_id": 18,
      "categories": [],
      "matched_invoices": [],
      "attachment_required": true
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### list_categories

Categories you use to analyse transactions and invoices, with their group and direction. Access: read. REST: `GET /api/v1/pennylane/categories`. MCP: `pennylane_read → list_categories`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `category_group_id` | integer | no | Only categories in this group. |
| `limit` | integer | no | 1 to 100. Default: 20. |
| `cursor` | string | no | Next page cursor from the previous call. |

Returns every category with its label, its category group and an optional analytical code. Treasury categories have a direction of cash_in or cash_out; other categories have none. Read this before categorise_transaction so you only pick categories that exist. Pages hold 1 to 100 categories; pass next_cursor back as cursor for the next page.

Example response:

```json
{
  "categories": [
    {
      "category_id": 59,
      "label": "Hosting",
      "category_group_id": 7,
      "direction": "cash_out",
      "analytical_code": null
    },
    {
      "category_id": 33,
      "label": "Bank fees",
      "category_group_id": 7,
      "direction": "cash_out",
      "analytical_code": null
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

### create_customer

Add a company or individual customer to your Pennylane customer list. Access: write, asks the user first. REST: `POST /api/v1/pennylane/customers`. MCP: `pennylane_write → create_customer`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes | Company name, or the full name of an individual. |
| `type` | enum | no | company or individual. Default: company. |
| `email` | string | no | Email address the invoice is sent to. |

Creates a customer and returns its customer_id for create_customer_invoice. Search with list_customers first, because this call does not check for a customer with the same name. It reaches nobody: the customer is not told. It changes your accounting data, so it asks for your OK. By default you confirm each call; you can switch that off for this tool.

Example response:

```json
{
  "customer_id": 4058,
  "name": "Bureau Nord",
  "type": "company",
  "email": "compta@bureau-nord.example"
}
```

### create_customer_invoice

Create an invoice for a customer, as a draft by default. Access: write, asks the user first. REST: `POST /api/v1/pennylane/customers/{customer_id}/invoice`. MCP: `pennylane_write → create_customer_invoice`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `customer_id` | integer | yes | Customer ID from list_customers or create_customer. |
| `date` | date | yes | Invoice date. |
| `deadline` | date | yes | Payment due date. |
| `lines` | array | yes | Invoice lines. Each has label, quantity, unit, unit_price as a string and vat_rate in percent. |
| `draft` | boolean | no | true keeps the invoice a draft, false finalizes it. Default: true. |
| `external_reference` | string | no | Your own reference, e.g. 2026-10-atelier-lumen. Use it to avoid duplicates. |

Creates an invoice with one or more lines and returns its invoice_id and amounts. Draft defaults to true and keeps the invoice editable; with draft set to false Pennylane finalizes it at once and it can no longer be edited. Amounts are strings, and the call fails when the totals do not add up or the VAT rate is not one Pennylane accepts. Without a ledger account on a line, Pennylane applies the company's default mapping. Set external_reference so a repeated run finds the invoice instead of creating it twice. A draft reaches nobody; a final invoice is a legal document. By default you confirm each call; you can switch that off for this tool.

Example response:

```json
{
  "invoice_id": 118390,
  "customer": {
    "customer_id": 4021,
    "name": "Atelier Lumen SARL"
  },
  "status": "draft",
  "draft": true,
  "date": "2026-10-31",
  "deadline": "2026-11-30",
  "external_reference": "2026-10-atelier-lumen",
  "amount_before_tax": "1140.00",
  "tax": "228.00",
  "amount": "1368.00",
  "currency": "EUR"
}
```

### finalize_customer_invoice

Turn a draft invoice into a final one with an invoice number. Access: write, asks the user first. REST: `POST /api/v1/pennylane/invoices/{invoice_id}/customer-invoice/finalize`. MCP: `pennylane_write → finalize_customer_invoice`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | integer | yes | Invoice ID from list_customer_invoices or create_customer_invoice. |

Finalizes a draft invoice and returns its invoice number. Once finalized, the invoice can no longer be edited. The call fails with an error when the entry lines are not balanced, and it does not send the invoice to the customer; use send_customer_invoice for that. Finalizing issues a legal document, so it asks for your OK. By default you confirm each call; you can switch that off for this tool.

Example response:

```json
{
  "invoice_id": 118390,
  "invoice_number": "F-2026-0151",
  "status": "upcoming",
  "draft": false,
  "amount": "1368.00",
  "deadline": "2026-11-30"
}
```

### send_customer_invoice

Email a customer invoice to the customer or to the addresses you give. Access: write, asks the user first. REST: `POST /api/v1/pennylane/invoices/{invoice_id}/customer-invoices`. MCP: `pennylane_write → send_customer_invoice`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | integer | yes | Invoice ID from list_customer_invoices or create_customer_invoice. |
| `recipients` | array | no | Email addresses to send to. Default: the email on the customer. |

Emails the invoice PDF. Without recipients it goes to the email on the customer. If the customer has none, the call fails and asks for one. Pennylane generates the PDF first, which can take a few minutes, so a call made right after creation can fail with a conflict; try again later. CC and BCC are not available, and the email uses Pennylane's default invoice template in French or English by the customer's billing language. The email reaches your customer. By default you confirm each call; you can switch that off for this tool.

Example response:

```json
{
  "invoice_id": 118390,
  "invoice_number": "F-2026-0151",
  "sent": true,
  "recipients": [
    "compta@atelier-lumen.example"
  ]
}
```

### import_supplier_invoice

Upload a supplier invoice PDF with its date, due date and amounts. Access: write, asks the user first. REST: `POST /api/v1/pennylane/suppliers/{supplier_id}/invoice`. MCP: `pennylane_write → import_supplier_invoice`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `file_url` | string | yes | HTTPS link to the PDF, or a PDF sent with the request. |
| `supplier_id` | integer | yes | Supplier ID from list_suppliers. |
| `date` | date | yes | Invoice date printed on the PDF. |
| `deadline` | date | yes | Payment due date. |
| `amount_before_tax` | string | yes | Total before tax, e.g. 62.00. |
| `tax` | string | yes | Total tax, e.g. 12.40. |
| `amount` | string | yes | Total including tax; must equal amount_before_tax plus tax. |

Uploads the PDF and creates the supplier invoice, which then appears under Invoicing > Supplier Invoices and posts entries to the Purchases journal. Only PDF files work, up to 100 MB. Pennylane rejects a PDF that already exists in the account, so importing the same file twice fails. Amounts are strings and the lines must add up to the total. It books an expense in your accounts, so it asks for your OK. By default you confirm each call; you can switch that off for this tool.

Example response:

```json
{
  "invoice_id": 90455,
  "supplier": {
    "supplier_id": 612,
    "name": "OVH SAS"
  },
  "date": "2026-10-01",
  "deadline": "2026-10-31",
  "amount_before_tax": "62.00",
  "tax": "12.40",
  "amount": "74.40",
  "currency": "EUR"
}
```

### categorise_transaction

Set the categories of a bank transaction, with a weight for each. Access: write. REST: `POST /api/v1/pennylane/transactions/{transaction_id}/categorise`. MCP: `pennylane_write → categorise_transaction`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `transaction_id` | integer | yes | Transaction ID from list_transactions. |
| `categories` | array | yes | List of category_id with weight as a string from 0 to 1. |

Replaces the categories of one bank transaction and returns the new list. Each category has a weight from 0 to 1; weights inside one category group must sum to 1, while categories from different groups combine freely. The call fails when the weights do not balance. It reaches nobody and changes only the analysis of one transaction, so it does not ask for your OK, and you undo it by calling it again with the earlier categories, which list_transactions shows.

Example response:

```json
{
  "transaction_id": 5530021,
  "categories": [
    {
      "category_id": 59,
      "label": "Hosting",
      "weight": "1"
    }
  ]
}
```

### match_transaction

Link a bank transaction to a customer invoice as its payment. Access: write, asks the user first. REST: `POST /api/v1/pennylane/invoices/{invoice_id}/transactions/{transaction_id}/match`. MCP: `pennylane_write → match_transaction`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `invoice_id` | integer | yes | Invoice ID from list_customer_invoices or create_customer_invoice. |
| `transaction_id` | integer | yes | Transaction ID from list_transactions. |

Matches a bank transaction to a customer invoice, which Pennylane uses for payments made outside it, such as a bank transfer. The invoice's remaining amount and status change after the match. Check the amount first: this records a payment against the invoice, so it asks for your OK. By default you confirm each call; you can switch that off for this tool.

Example response:

```json
{
  "invoice_id": 118277,
  "transaction_id": 5529874,
  "matched": true,
  "remaining_amount_with_tax": "0.00"
}
```

## How do I connect Pennylane to Claude Code, Codex or Cursor?

Connecting Pennylane takes three steps: sign up, sign in to Pennylane once in your Cloud Browser and enable it for agents, then add one MCP server or call the REST API with the same key.

### Connect Pennylane to your agent.

1. **Create an account** Sign up and add Pennylane in the console. Your API key is there too.

2. **Sign in to Pennylane in your Cloud Browser** Open your Cloud Browser in the console: your own Chrome in the cloud. Sign in to Pennylane there yourself, the way you always do, then press Enable for agents, read only or read and write. We never save your password, and your agent keeps working even when your computer is off.

3. **Add Cotlet to your agent** Paste the snippet for your client. The same server and key serve every app you connect, so you do this once.

   Claude Code:

```
claude mcp add --transport http cotlet \
  https://cotlet.dev/mcp/v1 \
  --header "Authorization: Bearer $COTLET_API_KEY"
```

   Cursor:

```
{
  "mcpServers": {
    "cotlet": {
      "url": "https://cotlet.dev/mcp/v1",
      "headers": {
        "Authorization": "Bearer ${COTLET_API_KEY}"
      }
    }
  }
}
```

   Codex:

```
# ~/.codex/config.toml
[mcp_servers.cotlet]
url = "https://cotlet.dev/mcp/v1"
bearer_token_env_var = "COTLET_API_KEY"
```

4. **Ask your agent** Ask in plain words. Your agent finds the Pennylane tools on the server and calls them for you; anything that reaches other people waits for your OK unless you switch that off for a tool.

```
Here is this month's billing list with customer, hours and rate. For each line, create the customer in Pennylane if it does not exist, create a draft invoice due in 30 days and show me the drafts. When I say go, finalize and email them.
```

### Call Pennylane from your scripts.

1. **Export your API key** Your scripts use the same API key as your agent. Keep it in an environment variable, never in your code.

```
export COTLET_API_KEY="cotlet_..."
```

2. **Call a tool** Every tool is an endpoint under /api/v1/pennylane with its own method and path, listed with the endpoints above. Path parameters go in the URL; the other parameters go in the query for GET and DELETE and in a JSON body otherwise.

```
curl -X POST https://cotlet.dev/api/v1/pennylane/customers/4034/invoice \
  -H "Authorization: Bearer $COTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"date":"2026-10-31","deadline":"2026-11-30","draft":true,"external_reference":"2026-10-studio-marais","lines":[{"label":"Workshop day, October","quantity":8,"unit":"hour","unit_price":"120.00","vat_rate":20}]}'
```

3. **Read the result** You get plain JSON back, ready for your script. A failed call costs nothing.

```
{
  "invoice_id": 118391,
  "customer": {
    "customer_id": 4034,
    "name": "Studio Marais SAS"
  },
  "status": "draft",
  "draft": true,
  "date": "2026-10-31",
  "deadline": "2026-11-30",
  "external_reference": "2026-10-studio-marais",
  "amount_before_tax": "960.00",
  "tax": "192.00",
  "amount": "1152.00",
  "currency": "EUR"
}
```

## Why use Cotlet for Pennylane?

**Built for agents**

- One MCP server for every app you connect.
- The same tools as a REST API for scripts and cron jobs.
- Works with Claude Code, Codex and Cursor.

**Your login stays yours**

- You sign in to Pennylane yourself, in your own Cloud Browser. We never save your password; the profile is encrypted with AES-256-GCM, the cipher 1Password uses.
- A+ on Qualys SSL Labs, securityheaders.com and Mozilla Observatory.
- The model never sees a login.
- No stored passwords, no CAPTCHA solving, no MFA bypass.

**You stay in control**

- Scope each API key: its apps, its tools, read only or read and write.
- Anything that reaches other people waits for your OK, so a leaked key cannot post or message as you.
- Every background request listed in your Background activity. Disable for agents in one click.

**Kept working for you**

- We test every integration every 6 hours.
- When Pennylane changes, the tool pauses until we fix it.
- Failed calls cost nothing.

## Is it safe to connect Pennylane to Cotlet?

Yes. You sign in to Pennylane yourself, in your own Cloud Browser, and we never save your password. Your agents only get Pennylane once you enable it, and every background request is listed. The browser profile is stored encrypted with AES-256-GCM, the same cipher 1Password uses for its vaults and Apple for iCloud Keychain. Independent public scanners grade cotlet.dev A+ and find it clean; click a result to rerun it yourself.

- **A+ on Qualys SSL Labs**: Encryption in transit: TLS setup and certificates, on all four server addresses. [Live result](https://www.ssllabs.com/ssltest/analyze.html?d=cotlet.dev)
- **A+ on securityheaders.com**: Browser protections sent with every page, all six headers present. [Live result](https://securityheaders.com/?q=cotlet.dev&followRedirects=on)
- **A+ on Mozilla Observatory**: Mozilla's web security baseline: score 120 of 100, 12 of 12 tests passed. [Live result](https://developer.mozilla.org/en-US/observatory/analyze?host=cotlet.dev)
- **Clean on Sucuri SiteCheck**: No malware, spam or defacement, and not on any of 9 blocklists, Google Safe Browsing included. [Live result](https://sitecheck.sucuri.net/results/cotlet.dev)

[How Cotlet protects your access](https://cotlet.dev/security.md)

### Common concerns

**Who runs Cotlet?**

Cotlet is run by Take Group, a Polish software company working since 2019. Take Group runs about 3,200 websites on its own systems and has its own DevOps team for infrastructure security. We built and used integrations like this inside the company for years before opening them as Cotlet.

**Is this an official Pennylane integration?**

No, Cotlet is independent of Pennylane. Pennylane's own API comes only with a paid plan. Cotlet works through your own Pennylane account, the way you use Pennylane in the browser, so you need no partner approval and no developer setup.

**What about client data and GDPR?**

Cotlet is run from the EU and follows the GDPR. Cloud Browsers run in Finland, a Tier 1 country in the ITU Global Cybersecurity Index 2024. Data passes through a call and is not kept after it.

**Can I start with read only?**

Yes. Enable the app for agents as read only, or make the API key read only, and anything that reaches other people waits for your OK unless you switch that off per tool.

**Is it reliable?**

Every Cotlet service is checked every 3 minutes from Europe, North America, Asia and Australia. The live results and any outage are public at status.cotlet.dev.

**How do I stop?**

Press Disable for agents next to Pennylane in your console: your agents lose access at once and you stay signed in. Signing out of Pennylane in the browser works too, and Reset browser deletes the whole profile.

## Is there a Pennylane MCP server on GitHub?

Yes. Community Pennylane MCP servers exist on GitHub, usually maintained by one person. You install one on your own computer, give it an API token from your Pennylane company, and keep it running yourself; when Pennylane changes its API, it stops working until you or the maintainer fix it. The other routes are the official API, where you generate a token and build the calls yourself, and the official Pennylane MCP server, which is read-only. Cotlet hosts a server that also writes, signs in to your Pennylane account in a browser, and our team keeps it working.

|  | GitHub MCP server | Cotlet |
|---|---|---|
| Setup | Get the Essentiel plan or higher, generate an API token in Settings > Connectivity > Developers, install the server, add the token to each MCP client | Sign up, sign in to Pennylane once, add one MCP server or API key |
| When Pennylane changes | It breaks until you or the maintainer fix it | Our team tests it daily and fixes it for you; failed calls are free |
| Your Pennylane access | An API token in a file or environment variable on your computer, with the read or read and write scope you chose | Signed in inside your own Cloud Browser in the EU, profile encrypted, only for the apps you enable, disabled for agents in one click |
| When access expires | The token expires after 1 month, 6 months, 12 months or never, as you set it; then tools fail until you generate a new one | Your Cloud Browser keeps you signed in; if Pennylane logs you out, your agent gets a link to sign in again |
| Upkeep | You update, monitor and debug it yourself | Nothing to host or update |
| Where it runs | Your computer, while it is on | Hosted: Claude Code, Codex, Cursor, n8n and scripts, also on a schedule |
| Price | Free, paid for with your time | Free 7-day trial, then pay-as-you-go credit packs, no subscription |

### Which one fits you

**Pick Cotlet if**

- you are not a developer, or you do not want to maintain a server
- you need it to keep working when Pennylane changes, without checking it yourself
- your agent or a script should run it on a schedule, also while your computer is off

**Pick an open-source server if**

- you are comfortable installing, updating and fixing it yourself
- you want your sign-in to stay on your own computer
- you only need it now and then, from one MCP client

## How much does the Pennylane API cost?

Free 7-day trial with 2,000 credits, no card needed. After that, the Pennylane API is pay as you go with no limit on apps: Credit packs from $29 for 5,000 credits, one-time, no subscription. A successful call uses 10 credits (about $0.04 to $0.06), your Cloud Browser uses 50 credits a day while it has an app enabled for agents, however many apps that is, and an agent driving the browser itself uses 5 credits a minute. Credit packs stay valid for 12 months and stack, so unused credits carry over.

| Credits | Price, one time | Calls at 10 credits | Per 1,000 credits |
|---|---|---|---|
| 5,000 | $29 | 500 | $5.80 |
| 15,000 | $79 | 1,500 | $5.27 |
| 32,000 | $149 | 3,200 | $4.66 |
| 60,000 | $249 | 6,000 | $4.15 |

Only successful calls use credits; failed calls and timeouts cost nothing. At a balance of 0 calls pause, never a surprise charge. Authenticate with the `Authorization: Bearer` header. [See full pricing](https://cotlet.dev/pricing.md)

## How Cotlet holds your access

Cotlet hosts MCP servers and REST APIs for apps with no usable API. Your agent works in your own Pennylane account.

- **Company details:** Cotlet is run by Take Group sp. z o.o., a company registered in Poland. Its full address and registration numbers are in the Terms of service. [Terms of service](https://cotlet.dev/terms)
- **Your sign-in:** You sign in to Pennylane yourself, in your own Cloud Browser: a Chrome we run for you in the EU, one per app. We never save your password (password saving is off), the profile is stored encrypted with AES-256-GCM, and your agents get Pennylane only after you enable it. When Pennylane signs you out, your agent gives you a link to sign in again. [Security](https://cotlet.dev/security.md)
- **Revoking access:** Press Disable for agents next to Pennylane, sign out of Pennylane in your Cloud Browser, or reset the browser. Every background request is listed in your Background activity. Actions that reach other people wait for your OK unless you switch that off per tool.
- **What we never do:** Store passwords, solve CAPTCHAs or bypass MFA.
- **Pennylane's terms:** You use your own Pennylane account and stay responsible for following Pennylane's terms. Cotlet is independent and not affiliated with Pennylane. [Terms](https://cotlet.dev/terms)
- **Contact:** Questions about the company, security or an app: [contact@cotlet.dev](mailto:contact@cotlet.dev)

## Frequently asked questions

**How do I get access?**

Sign up free, open Pennylane in the console and press Request access. Then you create your API key and connect Pennylane there.

**Do I need to be technical?**

No. You sign in to Pennylane once in your Cloud Browser, a normal Chrome you open from the console, and paste one command into your agent, or one URL into n8n. Your agent can walk you through it. There is no server to install, update or fix.

**Is it safe to connect Pennylane to Cotlet?**

Yes. You sign in to Pennylane yourself, in your own Cloud Browser in the EU, and we never save your password. Your agents only get Pennylane once you enable it, every background request is listed, and the browser profile is stored encrypted with AES-256-GCM, the cipher 1Password uses for its vaults. The AI model never sees a login, posts and messages wait for your OK, and Disable for agents cuts access in one click. Independent scanners grade cotlet.dev A+ on Qualys SSL Labs, securityheaders.com and Mozilla Observatory; live results are on https://cotlet.dev/security.

**Is a hosted server safer than a local one? My Pennylane sign-in would leave my computer.**

On Cotlet you sign in inside your own Cloud Browser, which only you open, and its profile is stored encrypted with AES-256-GCM in the EU. Agents get only the apps you enable, and every background request is listed. A local server keeps your Pennylane cookie in a plain file on your computer, readable by any program you run, and stops when your computer is off.

**Why does Cotlet send requests instead of clicking in my browser?**

To be fast, Cotlet doesn't click through Pennylane's pages. It sends the same requests your browser would, from your browser's IP with its cookies, without opening a window. Pennylane sees the same browser and IP you signed in with, and every request is listed in your Background activity.

**What if someone gets my Cotlet API key?**

The key holds no Pennylane login or cookies, it only asks Cotlet to run a tool. It can call only the apps and tools you gave it, and a read-only key changes nothing. Anything that reaches other people, like posts, comments or messages, waits for your OK in the dashboard by default, so a leaked key cannot post or message as you. When you switch approval off for a scheduled job, give that job its own key limited to that one tool. Every call is in your activity log, and deleting the key ends access at once.

**Why a signed-in browser and not OAuth, like other integration platforms?**

Pennylane offers no OAuth token you can simply grant for these tools: its official API leaves them out or needs a developer app, a plan or an approval first (see the official API section above). That is why Cotlet sets the limits itself: per key, per tool, with your approval before anything reaches other people. Cotlet is run by Take Group sp. z o.o., an EU company (VAT PL6342984273).

**Which Pennylane plan and role do I need?**

Pennylane's own API and MCP server need the Essentiel plan or higher, and a Company API token is generated by an Executive, Internal Accountant or External Accountant. Cotlet signs in as you and can do only what your own Pennylane user can do.

**Can the agent change an invoice after it is finalized?**

No. Pennylane documents that a finalized invoice can no longer be edited, so create_customer_invoice makes a draft by default and finalize_customer_invoice asks for your OK.

**Does the official Pennylane MCP server write to my books?**

Not yet. Pennylane describes its MCP server as read-only for now, with write capabilities planned for a later version and no date given (checked 14 Oct 2026).

**Can my agent post or send messages in Pennylane without asking me?**

No. Every call to create_customer, create_customer_invoice, finalize_customer_invoice, send_customer_invoice, import_supplier_invoice or match_transaction waits for your OK by default. For scheduled automations you can switch confirmation off per tool in your dashboard, for example only for approvals. categorise_transaction runs without asking, because it sends nothing to anyone and one call undoes it.

**Can I use the Pennylane API in n8n?**

Yes. Add https://cotlet.dev/mcp/v1 with your API key in n8n's MCP Client node, or call its endpoints under https://cotlet.dev/api/v1/pennylane from the HTTP Request node.

---

Give your AI agents your signed-in browser.

**Product:** [Integrations](https://cotlet.dev/integrations.md) · [Use cases](https://cotlet.dev/use-cases.md) · [How it works](https://cotlet.dev/how-it-works.md) · [Pricing](https://cotlet.dev/pricing.md) · [Security](https://cotlet.dev/security.md)

**Resources:** [Blog](https://cotlet.dev/blog.md) · [API docs](https://cotlet.dev/docs.md) · [Help center](https://cotlet.dev/help.md) · [Changelog](https://cotlet.dev/changelog.md) · [GitHub](https://github.com/cotletdev/cotlet)

**Solutions:** [For developers](https://cotlet.dev/for-developers.md) · [For automation agencies](https://cotlet.dev/for-agencies.md)

**Company:** [About](https://cotlet.dev/about.md) · [AI info: company facts](https://cotlet.dev/ai-info.md) · [Contact](https://cotlet.dev/contact.md)

**Legal:** [Terms of service](https://cotlet.dev/terms) · [Privacy policy](https://cotlet.dev/privacy) · [Refund policy](https://cotlet.dev/refunds) · [Acceptable use](https://cotlet.dev/acceptable-use) · [Cookie policy](https://cotlet.dev/cookies) · [DPA](https://cotlet.dev/dpa) · [Subprocessors](https://cotlet.dev/subprocessors)

**Elsewhere:** [Status page](https://status.cotlet.dev) · [Cotlet on GitHub](https://github.com/cotletdev) · [Cotlet on X](https://x.com/cotletdev) · [Cotlet on LinkedIn](https://www.linkedin.com/company/cotletdev) · [Cotlet on Product Hunt](https://www.producthunt.com/products/cotlet) · [Cotlet on Crunchbase](https://www.crunchbase.com/organization/cotlet) · [Cotlet on SaaSHub](https://www.saashub.com/cotlet)

Cotlet is an independent service. It is not affiliated with, endorsed by or sponsored by any of the products listed on this site. Product names are trademarks of their respective owners.

## For AI agents: sign your user up

Is your user asking you to sign them up for Cotlet? You can do it for them, no account or key needed.

1. Read the fields and their allowed values: GET https://cotlet.dev/api/v1/access-requests
2. Ask the user for each value: email, apps, what you will do with them. If they leave the password to you, generate a strong one and show it to them.
3. Show the user what you will send, then POST it as JSON to https://cotlet.dev/api/v1/access-requests.

One call creates the account and sends the access request; we email the user when access is granted.
