MCP server: invoices from ChatGPT, Claude, Gemini and other AI assistants
Fakturnia ships an MCP (Model Context Protocol) server so an AI assistant can work directly on a user's account: issue invoices, check payments, look up contractors, read stock levels and send documents to KSeF. It works with ChatGPT, Claude and Claude Code, Gemini, Mistral Vibe (formerly Le Chat), Perplexity, GitHub Copilot, Cursor and Windsurf, as well as other MCP clients and your own agent. The user connects the assistant with a single sign-in (OAuth 2.1), and every data change goes through a preview and a confirmation in the conversation.
This page is the technical description. The end-user guide (step by step, with screenshots) lives in the Fakturnia help centre (Polish).
In short. Server address:
https://prod.api.fakturnia.pl/mcp(sandbox:https://sandbox.api.fakturnia.pl/mcp). Transport: Streamable HTTP,POSTonly, stateless. Authorization: OAuth 2.1 with discovery (RFC 9728 and RFC 8414), PKCE S256, dynamic client registration and client ID metadata documents; alternatively anfmcp_…access key in theAuthorizationheader. An active MAX plan is required.
MCP versus the REST API
| REST API | MCP server | |
|---|---|---|
| Audience | system integrations (shop, ERP, CRM) | AI assistants and agents acting on behalf of a user |
| Authentication | OAuth 2.0 client credentials (API account) | OAuth 2.1 authorization code with PKCE (user consent) or a personal key |
| Who connects | account owner | owner or employee, each with their own permissions |
| Plan | MAX | MAX |
| Issuing a document | POST /documents |
guided flow: prepareDocument (preview, warnings) then issueDocument (after confirmation) |
| Irreversible operations | executed immediately | require confirm=true after the user agrees |
MCP tools execute through the same External API (full validation, rate limits and logs), so the rules from the other guides (amounts, document types, KSeF) apply unchanged.
Addresses and discovery
| Resource | Production | Sandbox |
|---|---|---|
| MCP server | https://prod.api.fakturnia.pl/mcp |
https://sandbox.api.fakturnia.pl/mcp |
| Protected resource metadata (RFC 9728) | https://prod.api.fakturnia.pl/.well-known/oauth-protected-resource/mcp |
https://sandbox.api.fakturnia.pl/.well-known/oauth-protected-resource/mcp |
| Authorization server metadata (RFC 8414) | https://prod.api.fakturnia.pl/.well-known/oauth-authorization-server |
https://sandbox.api.fakturnia.pl/.well-known/oauth-authorization-server |
| Client registration (RFC 7591) | POST https://prod.api.fakturnia.pl/mcp/oauth/register |
POST https://sandbox.api.fakturnia.pl/mcp/oauth/register |
| Authorization | GET https://prod.api.fakturnia.pl/mcp/oauth/authorize |
GET https://sandbox.api.fakturnia.pl/mcp/oauth/authorize |
| Token | POST https://prod.api.fakturnia.pl/mcp/oauth/token |
POST https://sandbox.api.fakturnia.pl/mcp/oauth/token |
A request without a token gets 401 with WWW-Authenticate: Bearer realm="mcp", resource_metadata="…/.well-known/oauth-protected-resource/mcp", which is where MCP clients start discovery. GET and DELETE on /mcp return 405: the transport is stateless and Mcp-Session-Id is ignored. The server speaks both protocol eras: requests carrying metadata in params._meta (revision 2026-07-28) and older clients that begin with initialize.
Connecting clients
Every client only needs the server address https://prod.api.fakturnia.pl/mcp. A client with OAuth support registers itself (dynamic registration) and opens the Fakturnia consent screen; a client without OAuth sends an fmcp_… access key (see Authentication).
ChatGPT
Settings, Connectors, Create: name Fakturnia, the MCP server address, OAuth authentication. ChatGPT registers dynamically, opens the Fakturnia consent screen and receives tokens once the user clicks "Allow". MCP connectors are available on paid ChatGPT plans; some accounts need developer mode enabled first.
Claude (claude.ai, Claude Desktop)
Settings, Connectors, "Add custom connector": name Fakturnia, the server address. Leave Client ID and Client secret empty (dynamic registration or a client ID metadata document). "Connect" opens the consent screen.
Claude Code
claude mcp add --transport http fakturnia https://prod.api.fakturnia.pl/mcp
Then /mcp in Claude Code, fakturnia, Authenticate: the browser opens the consent screen. Claude Code registers its redirect_uri on http://localhost:<port>/callback (both localhost and 127.0.0.1 are accepted).
Gemini
Gemini app (gemini.google.com). In settings open Connected Apps, then Custom apps, and choose "Add a custom app". Paste the MCP server address, click "Next" and approve access on the Fakturnia consent screen. Gemini registers dynamically, so you do not enter a Client ID or secret. Once connected on the web, Fakturnia also works in the Gemini mobile app; pick it in a prompt with @.
Google offers custom apps in Gemini only in the US for now: for adults, in English, on a personal Google account (not Workspace) and with Keep Activity turned on. It is not available in Poland yet; once it is, you connect Fakturnia with the same server address.
Gemini Enterprise. In the Business edition a team administrator can add a connection to a custom MCP server. Google requires the Streamable HTTP transport and a public HTTPS address with a trusted certificate, and the Fakturnia server meets both. The authorization and token URLs are in the "Addresses and discovery" table above, and the scope is mcp. If the form asks for a fixed Client ID and secret, use a pre-registered client issued by Fakturnia (see Authentication).
Gemini CLI. Add the server to ~/.gemini/settings.json:
{
"mcpServers": {
"fakturnia": {
"httpUrl": "https://prod.api.fakturnia.pl/mcp"
}
}
}
Gemini CLI discovers OAuth from the server's 401 response and opens the consent screen in the browser.
Mistral Vibe (formerly Le Chat)
Le Chat is now called Mistral Vibe, and accounts, plans and settings carried over unchanged. On chat.mistral.ai open Connectors, "Add connector" and the "Custom MCP Connector" tab. Enter the name Fakturnia and the server address, then click "Connect". Vibe detects OAuth 2.1 with dynamic client registration on its own and opens the Fakturnia consent screen. If you prefer a key, choose "HTTP Bearer Token" authentication and paste an fmcp_… key. Custom connectors are added by the organization administrator; on the Free, Pro and Student plans the account owner is the administrator.
Perplexity
Custom remote connectors are available on the Pro, Max and Enterprise plans, not on Free. In the connector settings add a custom remote connector, paste the server address and choose OAuth: Perplexity registers dynamically and opens the Fakturnia consent screen. If you pick API key authentication instead of OAuth, use an fmcp_… key, which the server accepts in the Authorization: Bearer header.
GitHub Copilot (VS Code, JetBrains, Eclipse, Xcode)
GitHub Copilot agent mode connects to remote MCP servers and signs in with OAuth through dynamic client registration. In VS Code (version 1.101 or later) add a .vscode/mcp.json file to the project:
{
"servers": {
"fakturnia": {
"type": "http",
"url": "https://prod.api.fakturnia.pl/mcp"
}
}
}
Then start the server ("Start" above its entry in the file). VS Code opens the Fakturnia consent screen in the browser, and the tools show up in Copilot Chat in agent mode. In JetBrains, Eclipse and Xcode add the same address to the MCP configuration of the GitHub Copilot plugin; sign-in works the same way, through dynamic registration.
Cursor, Windsurf and other MCP clients
Add an HTTP server with the address above. A client with OAuth support signs in through the browser; a client without OAuth uses an access key:
{
"mcpServers": {
"fakturnia": {
"url": "https://prod.api.fakturnia.pl/mcp",
"headers": { "Authorization": "Bearer fmcp_..." }
}
}
}
Your own agent (Claude API, OpenAI Responses API)
The server works as a remote MCP server in the Messages API (mcp_servers with url and an authorization_token equal to an fmcp_… key or an OAuth access token). The same pattern works with the OpenAI Responses API (tools: [{ type: "mcp", server_url, authorization }]).
Authentication
OAuth 2.1 (recommended)
Authorization code flow with PKCE S256, following the authorization section of the MCP specification:
- The client reads the protected resource metadata, then the authorization server metadata (
code_challenge_methods_supported: ["S256"],client_id_metadata_document_supported: true,authorization_response_iss_parameter_supported: true). - The client identifies itself in one of three ways: dynamic registration (
POST /mcp/oauth/register, public clients,redirect_urisrestricted tohttps://orhttp://localhost), a client ID metadata document (client_idis the HTTPS URL of a JSON document withredirect_uris; the server fetches it with SSRF protection and caches it) or a pre-registered client (issued by Fakturnia for connectors that need a fixedclient_idand secret). GET /mcp/oauth/authorizewithresponse_type=code,code_challenge,code_challenge_method=S256,state,resource=https://prod.api.fakturnia.pl/mcp(RFC 8707; a missingresourcemeans the/mcpresource). The server redirects to the Fakturnia app consent screen. An unknownclient_idor aredirect_urioutside the registration ends on an error page, never in a redirect.- The signed-in user (owner or employee) sees the client name, the return host, the access scope and the plan, then allows or denies. The authorization response carries
code,stateandiss. POST /mcp/oauth/token(grant_type=authorization_code,code_verifier,redirect_uri,resource) returns anaccess_token(JWT, 1 hour, audience equal to the server address) and arefresh_token(30 days, rotated on every refresh; reusing a consumed token revokes the whole connection).
Account state is checked on every request rather than read from the token: disconnecting in the app, deactivating an employee, changing permissions or an expired plan take effect immediately.
Access key fmcp_…
For clients without OAuth. The user creates a key in the app (Integrations, AI assistant, Access keys): a label, validity up to 90 days, the secret shown once. The key carries exactly the permissions of the person who created it and is sent as Authorization: Bearer fmcp_…. Revocation is immediate; the account owner sees and revokes employees' keys.
Permissions and plan
- The account owner sees every tool except integration configuration (webhooks, employee accounts) and what never reaches MCP: KSeF configuration (the KSeF token would travel in arguments), PDF download (binary), mailbox data beyond read-only.
- An employee sees only the tools matching their panel permissions (for example "View documents" yields
browseDocumentsandgetDocument, "Issue and edit documents" addsprepareDocument,issueDocument,patchDocument). A tool missing from the list is also unavailable on call; the response does not distinguish "does not exist" from "no permission". - Plan. The AI assistant is a MAX plan feature: connecting an account (the consent screen) and creating an
fmcp_…key need an active MAX plan, otherwise the response is403 SUBSCRIPTION_REQUIRED. When the plan expires or drops below MAX, onlygetAccountOverview,getDictionariesandlookupCompanyByNipwork. Every other tool stays listed with the prefix[WYMAGA PLANU MAX …]and returnsisErrorwith an explanation, so the model can say what is missing instead of pretending the feature does not exist.
Tools
The list depends on permissions and plan; the full set is below. Tools marked as generated map 1:1 to REST API operations (same operationId, same fields), so their schemas are in the OpenAPI specification.
| Group | Tools | Notes |
|---|---|---|
| Account | getAccountOverview |
seller, plan, numbering, default bank account, KSeF state without secrets; always available |
| Bank accounts | listBankAccounts, addBankAccount, setDefaultBankAccount |
the number is checked with IBAN/NRB check digits; the default account is printed on transfer invoices; the list needs settings:read, adding and changing the default is owner-only |
| Issuing (guided) | prepareDocument, issueDocument |
replace createDocument and validateDocument; described below |
| Documents | browseDocuments, getDocument, getDocumentHistory, patchDocument, updateDocumentPaymentStatus, updateDocumentExternalReference, deleteDocumentExternalReference (confirm), deleteDocument (confirm), sendDocumentToKsef (confirm), getDocumentKsefStatus |
generated |
| Contractors | browseClients, getClient, createClient, updateClient, deleteClient (confirm) |
generated |
| Products | browseProducts, getProduct, createProduct, updateProduct, deleteProduct (confirm) |
generated |
| Dictionaries and GUS | getDictionaries, lookupCompanyByNip |
visible to every token |
| KSeF | getKsefConnection, testKsefConnection |
connection setup only in the app |
| Settings | getSettings, updateGeneralSettings, updateNumberingSettings |
generated |
| Warehouse | prepareWarehouseDocument (WZ, PZ, PW, RW, MM; issued with issueDocument), browseWarehouses, getWarehouse, createWarehouse, updateWarehouse, deleteWarehouse (confirm), getStockReport, getWarehouseMovements, createStockAdjustment, createWzFromInvoice |
guided and generated |
| Recurring documents | browseRecurringRules, getRecurringRule, getRecurringRuleHistory, toggleRecurringRule |
generated |
| Email accounts | browseEmailAccounts, getEmailAccount |
read-only, no secrets |
| Activity | getAccountActivity |
account journal; account owner only |
Guided document issuing
Raw createDocument is not exposed as a tool. Instead:
prepareDocumenttakes the full document body (same schema asPOST /documents:documentType,issueDate,buyerIdorbuyer,products, payment, annotations), runs the full validation without saving and returns:
{
"previewId": "b7Jk…",
"expiresAt": "2026-09-26T12:15:00.000Z",
"preview": {
"documentType": "FakturaVAT",
"documentNumber": "FV/12/09/2026",
"seller": { "name": "Firma Testowa", "nip": "5261040828" },
"buyer": { "name": "Beta Sp. z o.o.", "nip": "5252248481", "street": "Prosta 1", "postalCode": "00-001", "city": "Warszawa" },
"products": [{ "name": "Usługa doradcza", "quantity": 2, "unitOfMeasure": "szt.", "netPrice": 500, "vatRate": 23 }],
"totals": { "netSum": 1000, "vatSum": 230, "grossSum": 1230, "currency": "PLN" },
"payment": { "method": "przelew", "statusId": 1, "paymentDeadlineDate": "2026-10-10" }
},
"warnings": [
{ "code": "NEW_CONTRACTOR", "message": "Nabywca „Beta Sp. z o.o." nie występował wcześniej w kontrahentach…" }
],
"summaryText": "Faktura VAT nr FV/12/09/2026 (data wystawienia 2026-09-26)\nSprzedawca: …\nRazem: netto 1 000,00 zł, VAT 230,00 zł, brutto 1 230,00 zł\n…",
"nextStep": "Pokaż użytkownikowi summaryText razem z ostrzeżeniami i zapytaj, czy wystawić dokument…"
}
Warnings: POSSIBLE_DUPLICATE (same buyer, amount and type within the last 30 days), NEW_CONTRACTOR, ISSUE_DATE_IN_FUTURE, ISSUE_DATE_OLD, PAYMENT_DEADLINE_BEFORE_ISSUE, SALE_DATE_AFTER_ISSUE, NO_ITEMS, ZERO_QUANTITY, ZERO_PRICE, FOREIGN_CURRENCY_NO_RATE, HIGH_AMOUNT, KSEF_BUYER_ADDRESS_MISSING, BANK_ACCOUNT_INVALID (the account number printed on the document fails the check digits). The preview also carries payment.bankAccount: the seller account that will be printed (the account's default for bank transfers). A validation failure comes back as isError with the code and field list from the External API (422 VALIDATION_ERROR, PAYMENT_DEADLINE_REQUIRED and so on). The summary text and warning messages are in Polish, the language of the account holders.
Missing fields are filled from the account settings like in the app (comments, signatures, place of issue, automatic annotations, VAT exemption basis, payment, VAT rate): the preview lists them in preview.appliedDefaults and in summaryText, so the model tells the user. Details in Creating documents. Annotation warnings: VAT_EXEMPTION_BASIS_MISSING (zw rate without an exemption basis), REVERSE_CHARGE_ANNOTATION_MISSING and REVERSE_CHARGE_WITHOUT_OO_RATE (reverse charge inconsistent with the oo rate), WDT_BUYER_VAT_UE_MISSING (0% WDT without the buyer's EU VAT number), INVOICE_ALREADY_CORRECTED (the invoice already has a correction).
- The model shows the user
summaryTextand the warnings; the user confirms. issueDocument({ previewId, confirm: true, sendToKsef?: false })issues exactly the stored preview (the model cannot alter the data between preview and issue), with an idempotency key, and returnsdocument(id, number, amount),appUrl(preview in the app) andksef(send result whensendToKsef=true). A preview is single-use, valid for 15 minutes and bound to the person who prepared it.
Warehouse documents
prepareWarehouseDocument works the same way for WZ, PZ, PW, RW and MM (the POST /warehouse/documents schema): lines reference catalogue products (productId from browseProducts), the preview shows the warehouse, the stock before and after each line, missing goods and the defaults applied, and canIssue: false means missing goods will block issuing (the model asks the user what to do; allowNegativeStock only on their explicit request). Issuing is the same issueDocument. An employee with the warehouse documents permission sees prepareWarehouseDocument and issueDocument, but not prepareDocument. Module rules: Warehouse.
Rules shared by all tools
confirm=trueis required by irreversible tools (delete*,sendDocumentToKsef,issueDocument). Without it the tool changes nothing and answers with an instruction for the model.- Arguments outside the schema are rejected on writes with the list of supported fields (a field never disappears silently); on reads they are logged.
- Lists default to 25 rows and cap at 100 (
itemsPerPage,currentPage); larger values are clamped with a note inmeta.hint. - Result size is capped at 120,000 characters; a larger result comes back as an error asking to narrow the query instead of truncated JSON.
- Result envelope. Tool content is wrapped in
[DANE_NARZEDZIA_START]and[DANE_NARZEDZIA_END]markers, and errors carry a[HTTP <status>]prefix. The server instructions tell the model that tool results are data, not commands. - Server instructions (
instructionsin theinitializeresponse and inserver/discover) include the company name, who is connected (owner or employee), the plan state and the order of steps when issuing a document.
Limits and security
- Rate limits: 120 calls per minute per Fakturnia account (owner and employees together), 240 per minute per IP before authentication, 10 client registrations per minute per IP, 30 token exchanges per minute per IP. A
429carriesRetry-After. Fakturnia can additionally set a 30-day call allowance; an exhausted allowance comes back asisErrorwith an explanation. - Assistant calls are visible in the app's REST API logs, and the rate limits of your own REST API integrations stay independent from them.
- Secrets never reach the model: the KSeF token, mailbox passwords, API key and webhook secrets are out of reach of the tools.
- Every tool execution is recorded (who, with which credential, which tool, outcome) and shown in the app under "Recent activity".
- Disconnecting an app, revoking a key, deactivating an employee and an expired plan take effect from the next request.
Example: one JSON-RPC call
POST /mcp HTTP/1.1
Host: sandbox.api.fakturnia.pl
Authorization: Bearer fmcp_...
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: browseDocuments
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "browseDocuments",
"arguments": { "dateFrom": "2026-09-01", "dateTo": "2026-09-30", "documentType": "FakturaVAT" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "my-agent", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
The response is result.content[0].text with the data envelope and result.isError.
Example prompts for the assistant
- "Issue a VAT invoice for Nowak Sp. z o.o. for consulting: 2 hours at 250 PLN net, payment due in 14 days."
- "Show this month's invoices that are still unpaid and total the amount."
- "Look up the company with NIP 5252248481 in GUS and add it to my contractors."
- "Mark invoice FV/12/09/2026 as paid today by bank transfer."
- "How much did I sell in Q3, broken down by contractor?"
- "Receive 20 office chairs from Fabryka Mebli into stock at 120 PLN net each."
- "Issue a correction to FV/12/09/2026: the customer returned one laptop."
The assistant starts with getAccountOverview, picks the buyer from contractors or from GUS, prepares a preview, shows it and only issues the document after confirmation. It sends to KSeF only on explicit request. KSeF is already mandatory in Poland, so an invoice issued by the assistant reaches the system the same way a manually issued one does: after your decision.
See also
- For agents & bots: machine-readable documentation resources.
- Authentication and Creating documents: the rules MCP tools inherit from the REST API.
- Sending to KSeF: the asynchronous flow, statuses and UPO.