MCP Server
Billabex exposes a Model Context Protocol (MCP) server so AI tools can access Billabex data using OAuth 2.1. This guide explains discovery, authorization, available tools and resources, and how to connect.
MCP Endpoint
The MCP server uses Streamable HTTP (stateless):
POST [baseURL]/mcp
OAuth Discovery Flow
MCP clients follow RFC 9728 (Protected Resource Metadata) and RFC 8414 (OAuth Authorization Server Metadata):
- Call
POST /mcpwithout a token. - The server responds with
WWW-Authenticateincludingresource_metadataandscope. - Fetch the resource metadata URL to discover
authorization_serversandscopes_supportedfor/mcp. - Fetch the authorization server metadata to locate the authorization and registration endpoints.
Example 401 Response
The MCP server returns:
WWW-Authenticate: Bearer resource_metadata="[baseURL]/.well-known/oauth-protected-resource", scope="mcp:read mcp:write"
Protected Resource Metadata (Path-Aware)
For /mcp, request:
[baseURL]/.well-known/oauth-protected-resource/mcp
The response includes:
{
"resource": "[baseURL]",
"authorization_servers": ["[baseURL]"],
"scopes_supported": ["mcp:read", "mcp:write"],
"bearer_methods_supported": ["header"]
}
Authorization Server Metadata
GET [baseURL]/.well-known/oauth-authorization-server
This returns the authorization_endpoint, token_endpoint, and
registration_endpoint needed for OAuth 2.1 + PKCE.
Scopes
MCP uses two scopes:
| Scope | Access | Description |
|---|---|---|
mcp:read |
Read | List and get invoices, accounts, communications. |
mcp:write |
Write | Create, update, delete operations. |
Consent Behavior
If an authorization request omits the scope parameter, Billabex defaults to
mcp:read mcp:write for MCP clients.
Protocol Details
- Transport: Streamable HTTP (stateless mode).
- Authentication:
Authorization: Bearer <token>header (required). - Rate limiting: 300 requests/minute per token (sliding window).
- CORS header: Clients may send
Mcp-Protocol-Versionheader.
Resources
MCP resources provide metadata that AI tools can read to understand Billabex capabilities.
| URI | Description |
|---|---|
billabex://about |
Platform overview, value proposition, key features |
billabex://concepts |
Key entities, statuses, relationships, and business rules |
billabex://workflow |
Step-by-step dunning workflow, task handling, and processes |
These resources help AI agents understand:
- About: What Billabex is, how it helps businesses manage invoice collection
- Concepts: Domain model including invoices, accounts, customer balances, communications, and tasks
- Workflow: Standard dunning workflow from invoice overdue to payment recovery
Prompts
MCP prompts provide business-oriented templates for AI agents to perform common tasks. Unlike tools, prompts use human-readable parameters (account names, organization names) instead of technical IDs.
| Prompt | Parameters | Use Case |
|---|---|---|
financial-snapshot |
organizationName?, period? (7/30/all days) |
Overview of outstanding balances, top accounts, cash flow |
account-360 |
accountName, organizationName? |
Complete account summary with invoices, communications, tasks |
daily-plan |
organizationName? |
Daily action plan: tasks, reminders, follow-ups |
daily-digest |
organizationName? |
Summary of today’s activities and updates |
communication-intelligence |
accountName?, organizationName? |
Recent replies, payment promises, accounts that replied but didn’t pay |
risk-report |
organizationName? |
Accounts at risk, silent accounts, accounts without contacts |
silent-accounts |
organizationName?, period? (30/90/all days) |
Accounts that never replied to a follow-up email |
blocked-accounts |
organizationName? |
Accounts whose emails bounced or were marked as spam |
inbound-email-digest |
organizationName?, period? (7/30/90 days) |
Synthesis of the incoming emails over a recent period |
How Prompts Work
- User provides names: You call a prompt with an account name like “Acme Corp” instead of a UUID
- Agent resolves IDs: The prompt instructs the AI agent to use tools like
list-accountsandlist-organizationsto find the matching IDs - Agent gathers data: The agent calls multiple tools to gather relevant data
- Agent presents results: Results are formatted naturally without exposing technical IDs
Example: Financial Snapshot
{
"name": "financial-snapshot",
"arguments": {
"organizationName": "My Company",
"period": "30days"
}
}
The agent will:
- Use
list-organizationsto find “My Company” - Use
list-customer-balancesto get outstanding balances - Use
list-invoicesto find upcoming due invoices - Present a formatted overview of your financial position
Tools Reference
Tools are grouped by domain. Read tools require mcp:read; Write tools require mcp:write.
Tool outputs include human-friendly fields such as displayName and summary where available. Prefer those fields for user-facing responses and keep IDs for follow-up tool calls.
All tools include annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) to help AI agents understand their behavior.
Invoicing
| Tool | Access | Description |
|---|---|---|
list-invoices |
Read | List invoices with the account payment method (paginated). |
get-invoice |
Read | Get invoice by ID, or by number within an account or an org. |
create-invoice |
Write | Create invoice with base64 file upload and optional sourceId. |
update-invoice-paid-amount |
Write | Update paid amount on an invoice. |
update-invoice-payment-schedule |
Write | Replace or remove an invoice payment schedule. |
delete-invoice |
Write | Delete an invoice. |
list-accounts |
Read | List customer accounts with their type and payment method. |
get-account |
Read | Get account type, details and contacts. |
get-account-financial-overview |
Read | Explain the account balance and list every invoice installment. |
create-account |
Write | Create a customer account with an optional account type. |
update-account |
Write | Update account type, name, currency, address or payment method. |
delete-account |
Write | Delete a customer account. |
list-contacts |
Read | List contacts on an account. |
get-contact |
Read | Get a specific contact. |
create-contact |
Write | Add a contact to an account. |
update-contact |
Write | Update a contact. |
delete-contact |
Write | Remove a contact. |
upsert-contact |
Write | Create or update contact by email/name match. |
set-contact-enabled |
Write | Enable or disable a contact on an account. |
list-credit-notes |
Read | List credit notes with the account payment method (paginated). |
get-credit-note |
Read | Get credit note details, by ID or by number. |
create-credit-note |
Write | Create a credit note, with an optional sourceId. |
update-credit-note-refunded-amount |
Write | Record a partial or full cash refund. |
delete-credit-note |
Write | Delete a credit note. |
apply-credit-allocation |
Write | Apply credit from a credit note to an invoice. |
remove-credit-allocation |
Write | Remove a credit allocation. |
add-account-tags |
Write | Attach existing labels to an account. |
remove-account-tag |
Write | Detach a label from an account. |
link-account-source |
Write | Link an external source to an account. |
update-account-source |
Write | Update account source last-sync timestamp. |
unlink-account-source |
Write | Unlink an external source. |
update-invoice-payment-schedule receives an invoice UUID, the complete installments array and
an optional skipConfirmation. Each installment contains a calendar date and a positive amount.
Send an empty array to remove the schedule. Confirmation is requested by default; automated clients
can set skipConfirmation: true.
A disabled contact is never contacted proactively by the agent. Re-enabling one is a human decision, so set-contact-enabled should not be used to undo a disable the client asked for.
create-contact, update-contact and upsert-contact take a phones list of { number, countryCode? }. A number starting with + or 00 carries its own country code; any other number is national and requires countryCode (ISO 3166-1 alpha-2), because Billabex never guesses a country. Contacts are returned with phones: [{ number, type, country, origin }], where type (Mobile / Landline / Unknown) and country are derived from the number itself and read-only. Writing phones replaces only what a human entered, never the numbers a connector brought (origin: "Connector"); omit the field to leave everything alone, send [] to clear the manual ones. Only a French mobile can receive an SMS. See Contacts for the full model.
Documents created by create-invoice and create-credit-note carry the Mcp channel, distinct from the PublicApi one the REST API stamps. They can live on an organization that is also synced with one or more connectors: see Sources for why an account’s source never tells you where one of its documents came from.
Both create tools also take an optional sourceId, your own identifier for the document. It is frozen at creation, returned on invoices and credit notes alongside connectionId, unique within the organization (a re-create with the same value is refused rather than duplicated), and filterable on list-invoices and list-credit-notes with is, isAnyOf, isEmpty and isNotEmpty only, because it is case-sensitive. Document numbers, on the other hand, are unique per account: get-invoice and get-credit-note answer exactly when given accountId plus number, while a lookup by organizationId plus number may answer any one of several namesakes.
create-contact, update-contact and upsert-contact may return a warnings array next to the contact when part of the write was not kept: a fullName or language a human owns that an automated sync tried to overwrite (MANUALLY_SET), or a phone number a sync sent that could not be normalized (UNPARSEABLE). The rest of the write still landed. Read the array instead of trusting a bare success.
list-invoices and list-credit-notes return accountPaymentMethod as
DIRECT_DEBIT, BANK_TRANSFER, or null. The value is read from the account
when the list is requested, so changing the account does not require updating
each document.
list-accounts, list-credit-notes, get-account, get-invoice and
get-credit-note return readOnlyReason: null when the record accepts writes,
otherwise the connector that would overwrite them. Read it instead of deriving a
verdict from channel or source. list-invoices does not carry it, because its
projection cannot tell a legacy invoice apart; call get-invoice when you need the
verdict for one of those rows.
Account tools expose accountType as Professional, Consumer, or Unknown.
create-account and update-account accept the same field. Omit it at creation to infer
Professional from a legal identifier, otherwise the account remains Unknown. A standard
connector controls the value and cannot be overridden through MCP; a Custom source remains
editable. Consumer accounts are not checked against BODACC, while their SIREN and VAT number,
if present, remain available through the API.
Dunning
| Tool | Access | Description |
|---|---|---|
list-customer-balances |
Read | List customer outstanding balances (paginated). |
get-customer-balance |
Read | Get balance details with invoices/credit notes. |
list-communications |
Read | List incoming and outgoing emails at once, filterable server-side. |
list-silent-accounts |
Read | List accounts emailed at least once that have never replied. |
list-outgoing-communications |
Read | List outgoing dunning emails (paginated). |
get-outgoing-communication |
Read | Get outgoing communication details. |
list-outgoing-communications-by-account |
Read | List outgoing communications for an account. |
update-outgoing-communication |
Write | Update a scheduled outgoing email. |
send-outgoing-communication |
Write | Send an email immediately on behalf of the agent, to free-form recipients. |
preview-outgoing-message |
Read | Preview and price an SMS or postal reminder without returning a PDF. |
send-outgoing-message |
Write | Send a paid message with a required idempotency key. |
get-outgoing-message |
Read | Get a sent SMS or postal reminder and its delivery state. |
preview-outgoing-letter |
Read | Deprecated alias for postal previews. |
send-outgoing-letter |
Write | Deprecated alias for postal sends. |
list-incoming-communications |
Read | List incoming (reply) communications. |
get-incoming-communication |
Read | Get incoming communication details. |
list-incoming-communications-by-account |
Read | List incoming communications for an account. |
get-account-dunning-status |
Read | Read whether an account is being chased, and the one reason it is not. |
pause-account-dunning |
Write | Suspend reminders with a fact reason, or until a date for a dated wait. |
resume-account-dunning |
Write | Lift human-resumable pauses and return any protections that remain. |
get-account-dunning-status answers “why is nothing going out on this account”. Its
silenceReason names the single cause, ranked once on the server, so the answer matches the one
the web application shows and the one GET /accounts/{accountId}/dunning returns on the REST API:
suspended, then blocking-task, then below-minimum-amount, then connector-sync-stale, then
first-reminder-delay, and null when nothing holds the account back. Read it rather than ranking
the individual flags yourself.
list-communications reads a pre-joined projection and filters, sorts and paginates on the server.
Prefer it over paging the incoming and outgoing lists separately. Beyond its own accountId, type
and date arguments, it takes the filters conditions described in
Filtering Lists: direction, status, deliveryStatus, accountName,
contactName, subject and date.
A communication outlives the account it belonged to. When that account has been deleted, the item
still carries its accountId and accountFullName, and accountDeletedAt holds the instant of the
deletion. Treat a non-null accountDeletedAt as “do not try to read this account”: the account
tools answer as if it never existed. It is null on every account still there.
| Tool | Access | Description |
|---|---|---|
list-emails |
Read | List all emails (incoming + outgoing). |
list-incoming-emails |
Read | List incoming emails (paginated). |
get-incoming-email |
Read | Get incoming email with simplified body. |
list-outgoing-emails |
Read | List outgoing emails (paginated). |
get-outgoing-email |
Read | Get outgoing email with simplified body. |
Platform
| Tool | Access | Description |
|---|---|---|
list-organizations |
Read | List organizations for the user, with their credits. |
get-organization |
Read | Get organization details, including the credit balance. |
update-organization |
Write | Update organization name or email domain. |
update-organization-settings |
Write | Update reminder delays and the reminder threshold. |
set-organization-logo |
Write | Set or remove the organization logo (base64 image). |
list-organization-members |
Read | List members and their profile picture metadata. |
remove-organization-member |
Write | Remove a member from an organization. |
set-my-avatar |
Write | Set or replace the authenticated user’s profile picture. |
remove-my-avatar |
Write | Remove the authenticated user’s profile picture. |
create-organization-invitation |
Write | Invite a user to an organization. |
cancel-organization-invitation |
Write | Cancel a pending invitation. |
list-tags |
Read | List the organization’s labels plus the standard ones. |
create-tag |
Write | Create a label owned by the organization. |
Both organization tools return credits, the prepaid balance that pays for the SMS and postal
letter reminders Billabex sends. See Credits below.
They also return the two effective first-reminder rules,
firstReminderDelayForDirectDebit and firstReminderDelayForBankTransfer, each
{ days, direction } with direction being BEFORE or AFTER the due date, or null when
neither an override nor firstReminderDelayDays is set. update-organization-settings writes them
under the same names: an absent key leaves that mode alone, an explicit null records that the
mode has no rule of its own and falls back on firstReminderDelayDays. An account whose payment
method is unknown follows firstReminderDelayDays alone, never either of these two.
They also return the reminder threshold as a pair, minimumDunningAmount and
minimumDunningAmountCurrency: an account is still reminded when its balance equals the amount,
and no longer when it is strictly below, the balance being converted to the threshold currency
first. New organizations start at 2 EUR. A null currency next to a non-null amount is a legacy
setting written before currencies existed: the amount is then compared in each account’s own
currency.
update-organization-settings writes the pair:
{ "minimumDunningAmount": 5, "minimumDunningAmountCurrency": "USD" }
- both keys together set the pair;
- the amount alone keeps the currency already set, or keeps the legacy comparison when there is none;
{ "minimumDunningAmount": null, "minimumDunningAmountCurrency": null }removes the threshold;- a currency without its amount is rejected, and so is a non-null amount with an explicit
nullcurrency.
They also return logo, the image uploaded for the organization, or null. null does not mean
no logo is shown: with none uploaded, one is derived from the organization’s email domain at
display time and nothing is stored. set-organization-logo takes a base64 PNG, JPEG or WebP up to
2 MB, or file: null to remove it. The real file signature is checked, not the declared type, so
an SVG is refused.
Tasking
| Tool | Access | Description |
|---|---|---|
list-account-tasks |
Read | Search the organization’s tasks (paginated). |
get-account-task |
Read | Get task details and required input format. |
cancel-account-task |
Write | Cancel an account task, with an optional reason. |
add-account-task-text-interaction |
Write | Add a text response to a task. |
add-account-task-structured-interaction |
Write | Add a structured response to a task. |
list-account-tasks changed contract. It used to take a required accountId, return every
active task of that account at once and never paginate. It now takes organizationId, accountId
being optional, returns tasks of any status by default, and pages. Its former type argument became
a type filter condition. Items keep their full shape, interaction thread included: the
projection selects the page, then every row is rehydrated from its aggregate.
Filtering Lists
list-accounts, list-invoices, list-credit-notes, list-communications and
list-account-tasks take a filters argument: an array of conditions, each
{ field, operator, value } or { field, operator, values }.
{
"organizationId": "123e4567-e89b-12d3-a456-426614174000",
"filters": [
{
"field": "status",
"operator": "isAnyOf",
"values": ["Overdue", "Issued"]
},
{ "field": "dueDate", "operator": "before", "value": "2026-01-01" },
{ "field": "remainingBalance", "operator": "greaterThan", "value": "1000" }
],
"first": 50
}
- Conditions combine with
AND; the values of one condition combine withOR. - A field carries at most one condition, and at most 20 conditions per call.
- Fields and operators come from a per-tool allowlist, listed in the description of each tool. An unknown field, or an operator the field does not accept, is refused, never ignored.
- Dates are written
YYYY-MM-DDand compared by whole day. Text comparison ignores case and accents.
| Tool | Fields |
|---|---|
list-accounts |
accountName, tags, dunningPaused, daysOverdue, overdueInvoiceCount, overdueBalance, remainingBalance, hasPaymentSchedule, paymentMethod, collectiveProceeding, source |
list-invoices |
number, accountName, tags, status, dueDate, paymentSchedule, paymentMethod, totalAmount, remainingBalance, channel |
list-credit-notes |
number, accountName, tags, status, issuedDate, paymentMethod, totalAmount, remainingAmount, channel |
list-communications |
direction, status, deliveryStatus, accountName, contactName, subject, date |
list-account-tasks |
title, accountName, status, type, tags, balance, createdAt, updatedAt |
On list-invoices and list-credit-notes, tags matches the labels of the account the
document belongs to, not labels on the document itself, which carries none. It takes label
identifiers, the ones list-tags returns, never their display names. That is how a group of
companies is read in one call: label the accounts once, then
{ "field": "tags", "operator": "containsAny", "values": ["<label id>"] } returns every open
invoice of the whole group, instead of one call per account.
The same conditions are available on the public REST API, where they travel as one query string. See Pagination & Filtering for the operator table and the exact semantics.
Credits
get-organization and list-organizations return a credits field: the prepaid balance that
pays for the SMS and postal letter reminders Billabex sends on your behalf. Emails are free and
never consume it.
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"name": "Acme Corporation",
"credits": 250
}
At zero, SMS and letters stop going out; email reminders keep running. Nothing else in the API signals that, which is why the balance is worth reading if you build a dashboard or an alert on top of Billabex.
The field is read-only, but send-outgoing-message spends it after rendering the selected
invoices and credit notes. Call preview-outgoing-message first to obtain the exact segment or page
price without spending anything, then pass that value as maxCredits. Reuse the same
idempotencyKey for every retry. send-outgoing-communication still sends a free email.
Topping the balance up is not self-service; Billabex handles it manually.
Pagination
Tools returning lists use cursor-based pagination:
- Parameters:
first(items per page, default 20, max 100),after(cursor). - Response:
nodesarray +pageInfo.endCursor. - Sort:
list-accounts,list-invoices,list-credit-notes,list-communicationsandlist-account-taskstakesortByandsortOrder(asc/desc).sortByaccepts the same values as the matching REST list, enumerated in each tool description.
On a filtered list, an endCursor belongs to the exact set of filters and the sort it was issued
for. Pass it back unchanged to get the next page; whenever a condition or the sort changes, drop it
and read the list again from its first page, otherwise the call is refused.
Notes
- OAuth is required for all MCP access.
- Tokens must be sent in the
Authorization: Bearer <token>header. - Rate limiting applies to MCP requests (300/min per token).
update-outgoing-communicationacceptsmessagein markdown. Do not include a signature; the platform renders HTML/text and appends the agent signature automatically.