---
title: "Billabex MCP Server"
description: "Connect AI tools to Billabex using the Model Context Protocol (MCP) with OAuth 2.1."
canonical: https://developer.billabex.com/en/guides/mcp/
lang: en
alternate: https://developer.billabex.com/fr/guides/mcp/
last-updated: 2026-10-26
---

# Billabex MCP Server

> Connect AI tools to Billabex using the Model Context Protocol (MCP) with OAuth 2.1.

Source: https://developer.billabex.com/en/guides/mcp/
Language: English (en)
French version: https://developer.billabex.com/fr/guides/mcp/

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 https://next.billabex.com/mcp`

## The Public Tool Catalogue

`GET https://next.billabex.com/mcp/tools` returns every tool, prompt and resource of the server, each with its
description, its JSON Schema and the scope it needs. It takes **no credential** and is rate limited
per IP address under the `public-api-anonymous` policy.

```bash
curl -s https://next.billabex.com/mcp/tools | jq '.tools[] | {name, scopes}'
```

Use it to decide whether to connect at all, to generate typed bindings, or to show a user what
authorizing Billabex would grant, before any OAuth flow starts.

| View                  | What it contains                                                                                                               |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| default (`view=full`) | Everything, input and output JSON Schemas included. The faithful preview of what `tools/list` answers, and around 320 KB.      |
| `?view=summary`       | Names, descriptions, scopes and annotations, without the schemas. Around 40 KB, enough to decide whether Billabex is relevant. |

A `view` the server does not have is refused with a `400` and the usual JSON error body, rather
than quietly answered with another one.

The MCP endpoint itself stays protected, and that is deliberate rather than an oversight. An MCP
client starts the OAuth flow because it receives a `401` with a `WWW-Authenticate` challenge; a
server that answered `initialize` without a token would leave the client believing it is connected,
and every tool call would then fail. The catalogue gives an agent what it legitimately needs before
authorizing, without taking that challenge away.

Nothing in the catalogue is organization data: names, descriptions and schemas are the same static
contract this guide documents.

## OAuth Discovery Flow

MCP clients follow RFC 9728 (Protected Resource Metadata) and RFC 8414 (OAuth
Authorization Server Metadata):

1. Call `POST /mcp` without a token.
2. The server responds with `WWW-Authenticate` including `resource_metadata` and `scope`.
3. Fetch the resource metadata URL to discover `authorization_servers` and
   `scopes_supported` for `/mcp`.
4. Fetch the authorization server metadata to locate the authorization and
   registration endpoints.

### Example 401 Response

The MCP server returns:

```
WWW-Authenticate: Bearer resource_metadata="https://next.billabex.com/.well-known/oauth-protected-resource", scope="mcp:read mcp:write"
```

### Protected Resource Metadata (Path-Aware)

For `/mcp`, request:

`https://next.billabex.com/.well-known/oauth-protected-resource/mcp`

The response includes:

```json
{
  "resource": "https://next.billabex.com",
  "authorization_servers": ["https://next.billabex.com"],
  "scopes_supported": ["mcp:read", "mcp:write"],
  "bearer_methods_supported": ["header"]
}
```

### Authorization Server Metadata

`GET https://next.billabex.com/.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-Version` header.

## 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 payment follow-up, 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**: The standard path from an overdue invoice to payment

## 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

1. **User provides names**: You call a prompt with an account name like "Acme Corp" instead of a UUID
2. **Agent resolves IDs**: The prompt instructs the AI agent to use tools like `list-accounts` and `list-organizations` to find the matching IDs
3. **Agent gathers data**: The agent calls multiple tools to gather relevant data
4. **Agent presents results**: Results are formatted naturally without exposing technical IDs

### Example: Financial Snapshot

```json
{
  "name": "financial-snapshot",
  "arguments": {
    "organizationName": "My Company",
    "period": "30days"
  }
}
```

The agent will:

1. Use `list-organizations` to find "My Company"
2. Use `list-customer-balances` to get outstanding balances
3. Use `list-invoices` to find upcoming due invoices
4. 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, and a human-readable `title` that clients display instead of the tool name.

### Invoicing

| Tool                                 | Access | Description                                                                                         |
| ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- |
| `list-invoices`                      | Read   | List invoices with the account payment method (paginated).                                          |
| `export-invoices`                    | Read   | Export every filtered invoice, or credit note with `documentType: "creditNotes"`, to `.xlsx`: returns a download link valid for 15 minutes. |
| `get-aging-balance`                  | Read   | Net aging balance totals per currency.                                                              |
| `list-aging-balance-by-account`      | Read   | Net breakdown per account and per currency, 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`                     | Write  | Correct one aspect at a time: the total paid (`paidAmount`), the due date, or the payment schedule. |
| `set-invoice-dunning-block`          | Write  | Exclude one invoice from reminders, or put it back; it stays due, the account keeps going.          |
| `delete-invoice`                     | Write  | Delete an invoice.                                                                                  |
| `list-accounts`                      | Read   | List accounts with type, payment method and external source ID.                                     |
| `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 an account with an optional type and payment method.                                         |
| `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.                                                                        |
| `delete-contact`                     | Write  | Remove a contact.                                                                                   |
| `upsert-contact`                     | Write  | Create or update by email/name match, update one by `contactId`, or enable/disable it.              |
| `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.                                                                          |
| `check-account-bodacc`               | Write  | Check French collective proceedings and propose SIREN candidates.                                   |
| `get-account-portal-link`            | Read   | Read the customer's document page link of an account.                                               |
| `ensure-account-portal-link`         | Write  | Get or create that link; never replaces an existing one.                                            |
| `regenerate-account-portal-link`     | Write  | Replace that link; every previous copy stops working.                                               |

`update-invoice` receives an invoice UUID, exactly one of `paidAmount`, `dueDate` or
`installments`, and an optional `skipConfirmation`. Each is a separate correction: a call carrying
none of them, or several, is refused without writing anything, so a call never half applies. The
answer is the invoice acknowledgement, `paymentSchedule` included. The tool is annotated as
destructive, so the client asks the user to approve the call. `skipConfirmation` is still accepted
and changes nothing.

- `paidAmount` is the new total paid on the invoice, never an amount to add. Pass it with
  `expectedPaidAmount`, the paid amount you read (accepted with `paidAmount` only): if another
  update landed meanwhile, the call is refused with `app-invoicing.invoice.paid-amount-conflict`
  carrying the current figure.
- `dueDate` is a calendar date that cannot be earlier than the invoice issue date. Rewriting the
  same date changes nothing, so a post-import correction script can be replayed safely. Moving the
  due date recalculates how overdue the invoice is and reschedules upcoming reminders; a reminder
  already edited by hand is left untouched. An invoice paid in installments refuses it: correct its
  `installments` instead.
- `installments` is the complete schedule, each installment a calendar date and a positive amount.
  Send an empty array to remove the schedule.

`get-account-financial-overview` returns `referenceDate` and the `dueDate` of every installment in
the same shape as the public API: an object `{ "year": 2026, "month": 10, "day": 15 }`, a calendar
date with no time and no time zone. Read each component by its name, since the order of the
properties is not guaranteed, and display the date from its components, without converting it to a
UTC instant. Instants, such as `sentAt`, stay ISO 8601 strings. See
[Dates and Instants](/en/guides/getting-started/#dates-and-instants).

`upsert-contact` covers every write on an existing contact. Without `contactId`, it matches on email or name, then updates the contact found or creates one. With `contactId`, it updates that contact, and `null` clears `fullName` (the contact must keep an email address), `email` or `jobTitle`. With `contactId` and `enabled` and no other field, it enables or disables the contact; `enabled` is refused in any other combination, so change a contact and its status in two calls. A disabled contact is never contacted proactively by the agent. Re-enabling one is a human decision, so `enabled: true` should not be used to undo a disable the client asked for.

`create-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 mobile from France (`FR`) or French Guiana (`GF`) can receive an SMS. See [Contacts](/en/guides/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](/en/guides/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.

To pull a known set of documents in one call rather than one call per document, filter `number` with `isAnyOf` on `list-invoices` or `list-credit-notes`: `{"field": "number", "operator": "isAnyOf", "values": ["FA-2026/1", "FA-2026/2"]}`, up to 50 numbers per condition. Numbers match on their canonical form, so punctuation, spacing and case do not have to be right; a value left with nothing after that is skipped rather than emptying the whole condition. Since a number is unique per account and not per organization, add an `accountId` when namesakes would be ambiguous.

`create-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.

For connector-linked accounts, `list-accounts` returns the connector's customer
reference in `source.sourceId`. The same field is also available on
`lastUnlinkedSource` after a connector is detached. No `get-account` call is
needed to match a listed account with the external customer.

`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.

Both tools also accept `paymentMethod` as `DIRECT_DEBIT`, `BANK_TRANSFER`, or `null`. Omit it or
send `null` at creation to leave the payment method unknown.

An account under collective proceedings also carries what a creditor needs to act on it:
`bodaccNoticeDate` (when the notice was published, which the two-month window to file a claim
runs from), `bodaccJudgmentDate` (the ruling itself, usually a few days earlier),
`bodaccPractitioner` and `bodaccNoticeUrl`. Read `bodaccNoticeDate` for any deadline: the
publication date is the one that counts, not the ruling. Unlike the other calendar dates, both are
days sent as instants at midnight UTC (`"2026-08-23T00:00:00.000Z"`): read their date part in UTC,
never convert them to local time.

`bodaccPractitioner` is the insolvency practitioner to file the claim with, as one line of free
text quoted from the notice: a firm, often a person, and a postal address. The BODACC carries no
structured field for it and never publishes an email or a phone number, so nothing more precise
can be returned, and it is null when the notice words the appointment in a way that cannot be
parsed. `bodaccNoticeUrl` links the notice itself and is the fallback in that case. Both are
null while no proceeding is open, and for a company struck off after a liquidation closed for
insufficient assets, where no claim can be filed any more.

`check-account-bodacc` requires `mcp:write` and takes an `accountId` plus an optional `siren`
(a 9-digit SIREN or a 14-digit SIRET). Without a confirmed identifier it returns the proposed
candidates without adopting any of them: a broader search never establishes identity on its own.
Confirm the identifier on a document that names the debtor, or on their customer record, before
sending it.

The REST equivalent is `POST /api/public/v1/accounts/{accountId}/bodacc-check`, with the
`accounts:all` scope and a body of `{}` or `{"siren":"482309382"}`. The check is also available
for connector-managed accounts.

The response carries `siren`, `status`, `checkedAt`, `companyName`, `noticeUrl` and `candidates`,
a list of `{siren,name,address}`. The status is null when the account falls outside the scope of
French companies. The call can record a check, pause reminders and open a task: it is not a read.
A legal or manual pause stays in place, even when the identification changes.

When a proceeding has been reported but the SIREN is still unknown, a task asks for it. It is read
and answered through the usual task tools. Closing that task does not fill in the SIREN and does
not lift the legal pause.

### 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 every channel of an organization or one account, filterable.          |
| `get-communication`                     | Read   | Read one communication by the id and type of a list row.                   |
| `list-silent-accounts`                  | Read   | List accounts emailed at least once that have never replied.               |
| `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 letter; `includePdf` adds the rendered PDF.    |
| `send-outgoing-message`                 | Write  | Send a paid SMS or letter with a required idempotency key.                 |
| `refresh-outgoing-message-tracking`     | Write  | Refresh a letter's postal history and receipt, without sending or credits. |
| `get-account-dunning-status`            | Read   | Read whether an account is chased, why not, and its payment promise.       |
| `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.        |
| `withdraw-account-payment-date-request` | Write  | Take back the instruction to ask the debtor for a payment date.            |

`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 `payment-decision-overdue`, then `payment-declaration`, then
`all-invoices-blocked`, 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. `payment-declaration` is a reported payment still within its grace
period; `payment-decision-overdue` is one past it that no open task asks about anymore, and it never
clears on its own: record a decision with `resolve-account-payment-declaration`.

`set-invoice-dunning-block` with `blocked: true` excludes a single invoice from the automatic
reminders while the other invoices of the account keep being chased: the invoice stays due and in
the balance, but no reminder claims it, names it or attaches it, and it no longer drives the cadence.
Use it for a disputed invoice, a partial delivery or an awaited credit note; pass an optional
internal `note` (500 characters at most), never sent to the debtor. This is neither
`pause-account-dunning`, which stops the whole account, nor `record-account-payment-declaration`,
which records a fact about money. When every open invoice of an account is excluded,
`get-account-dunning-status` answers `all-invoices-blocked`. `blocked: false` lifts the exclusion
and refuses a `note`; `get-invoice` and `list-invoices` return it as `dunningBlock` (`note`,
`blockedAt`, `blockedBy`), null when the invoice is chased like any other.

The same response returns `collectionStatus`: `Open`, `Settled`, `WrittenOff` or `NoInvoice`.
`resume-account-dunning` accepts an optional `reason`; pass `written-off` to reopen a written-off
account without lifting any other pause. `list-accounts` can filter on `collectionStatus`, although
the derived field is not added to each list item.

The status also carries `paymentDateRequest`, a person's standing instruction to ask the debtor for
a realistic payment date rather than for payment (`taskId`, `instructedAt`), or `null`. Reminders
ask for a date only while the account is due and no active payment promise, direct debit or payment
arrangement applies. `withdraw-account-payment-date-request` takes the instruction back, so the next
reminders ask for payment again; it is idempotent. The REST equivalent is
`POST /api/public/v1/accounts/{accountId}/dunning/payment-date-request/withdraw`.

`list-communications` reads a pre-joined projection and filters, sorts and paginates on the server.
It covers every channel (`IncomingEmail`, `OutgoingEmail`, `OutgoingSms`, `OutgoingTrackedLetter`,
`OutgoingRegisteredLetter`); pass `accountId` to restrict it to one account. Each row carries an `id`
and a `type`: pass both to `get-communication` for the content. Only the section matching `type`
(`incomingEmail`, `outgoingEmail` or `message`) is set; a letter's `message.pdfUrl` is a short-lived
signed URL, renewed by reading again. Beyond its own `accountId`, `type` and date arguments, it takes the `filters` conditions described in
[Filtering Lists](#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.

### Email

| Tool          | Access | Description                                                              |
| ------------- | ------ | ------------------------------------------------------------------------ |
| `list-emails` | Read   | List the mailbox (incoming and outgoing), or one direction with `type`.  |
| `get-email`   | Read   | Read one email with its simplified body and short-lived attachment URLs. |

### 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 the name and email domain, or the settings.      |
| `list-organization-members` | Read   | List members and their profile picture metadata.        |
| `list-tags`                 | Read   | List the organization's labels plus the standard ones.  |
| `create-tag`                | Write  | Create a label owned by the organization.               |

`update-organization` writes either the identity (`name`, `emailDomainName`) or the settings
described below, never both: they are two separate writes, so a call carrying both is refused. Only
the fields sent are applied. It answers with the organization id, identity and settings; read the
rest with `get-organization`.

`list-organizations` and `get-organization` return `credits`, the prepaid balance that pays for the
SMS and postal letter reminders Billabex sends. See [Credits](#credits) below.

They also return `firstReminderDelay`, the default first-reminder rule, `{ days, direction }` or
`null`. It applies to every mode without a rule of its own and to accounts whose payment method is
unknown, and it is the only way to set a default **before** the due date: `firstReminderDelayDays`
only says "N days after". `update-organization` writes it under the same name, with `days` of
at least 1, or `null` to fall back to the standard pace (a reminder 3 business days before the
due date). Do not send it together with `firstReminderDelayDays`.

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, or when the mode was disabled.
`firstReminderDelayForDirectDebitFollowsDefault` and `firstReminderDelayForBankTransferFollowsDefault`
say whether each mode still follows `firstReminderDelayDays` or carries a rule of its own.
`update-organization` writes the rules under the same names: an absent key leaves that mode
alone, and an explicit `null` disables the rule for that mode whatever `firstReminderDelayDays` says.
To make a mode follow `firstReminderDelayDays` again, send its `...FollowsDefault` field as `true`,
without a rule for the same mode. An account whose payment method is unknown follows
`firstReminderDelayDays` alone, never either of these two.

Generated emails group the per-invoice amounts that fall strictly below the threshold, converted
into the account's currency. The total and the supporting documents are unchanged. When the group's
total stays below the threshold, it does not harden the tone of the reminder. This rule reads the
same setting from the web app, the public API and MCP.

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` writes the pair:

```json
{ "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 `null`
  currency.

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. The logo is set from the organization settings of the webapp.

### 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`. Refused on a payment check.                                                |
| `close-account-task`                      | Write  | End a task dealt with elsewhere, without a model or an email. Only a `NeedUserInput` asking for information.                  |
| `set-pending-account-task`                | Write  | Set an account task aside, with an optional `pendingUntil` date it should come back. Without one it comes back after 30 days. |
| `add-account-task-text-interaction`       | Write  | Add a text response to a task.                                                                                                |
| `decide-account-task-eligibility`         | Write  | Decide contact eligibility.                                                                                                   |
| `decide-account-task-contact-change`      | Write  | Approve or refuse proposed contact changes.                                                                                   |
| `decide-account-task-first-outreach`      | Write  | Authorize or refuse first outreach.                                                                                           |
| `provide-account-task-contact`            | Write  | Provide the requested contact.                                                                                                |
| `decide-account-task-next-action`         | Write  | Choose the next action.                                                                                                       |
| `decide-account-task-payment-arrangement` | Write  | Grant, refuse or keep a payment arrangement.                                                                                  |
| `decide-account-task-email-draft`         | Write  | Send or discard the pending draft.                                                                                            |
| `confirm-account-task-recipients`         | Write  | Confirm reminder recipients.                                                                                                  |
| `confirm-account-task-resume`             | Write  | Confirm resume or keep reminders paused.                                                                                      |
| `acknowledge-unknown-account-task`        | Write  | Acknowledge an Unknown task.                                                                                                  |

A `NeedUserInput` task may expose `extraData.pendingCommunicationId`. Read that draft with
`get-communication` (`type: "OutgoingEmail"`), edit it with `update-outgoing-communication` if needed, then call
`decide-account-task-email-draft` with `accountTaskId` and
`value: { "communicationId": "…", "decision": "send" }`. Use `discard` to abandon it. A text response
never approves the draft.

**`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 }`.

```json
{
  "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 with `OR`.
- 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-DD` and compared by whole day. Text comparison ignores case and
  accents.

| Tool                            | Fields                                                                                                                                                                                                                            |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list-accounts`                 | `accountName`, `tags`, `dunningPaused`, `collectionStatus`, `followUpStatus`, `daysOverdue`, `overdueInvoiceCount`, `overdueBalance`, `remainingBalance`, `hasPaymentSchedule`, `paymentMethod`, `collectiveProceeding`, `source` |
| `list-aging-balance-by-account` | `accountName`, `tags`, `dunningPaused`, `paymentMethod`, `collectiveProceeding`, `source`, `notDue`, `d0_30`, `d31_60`, `d61_90`, `d90Plus`, `totalOutstanding`, `overdueOutstanding`, `overdueRate`                              |
| `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](/en/guides/pagination/#filtering-a-list) 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, and for contact
searches. Emails are free and never consume it.

```json
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "name": "Acme Corporation",
  "credits": 250,
  "creditLots": [
    {
      "grantedAt": "2026-01-12T09:30:00.000Z",
      "expiresAt": "2027-01-12T09:30:00.000Z",
      "remaining": 100
    },
    {
      "grantedAt": "2026-02-12T09:30:00.000Z",
      "expiresAt": "2027-02-12T09:30:00.000Z",
      "remaining": 150
    }
  ]
}
```

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.

**Credits expire twelve months after they are loaded, and each top-up runs on its own clock.** That
is what `creditLots` reports, nearest expiry first: the balance above says 250 credits, but 100 of
them are gone in January. A send always spends the credits closest to expiring first, so an alert
built on the balance alone will miss the drop. There is no expiry notification: read `creditLots`.

The fields are **read-only**, but `send-outgoing-message` spends the balance after rendering the
selected invoices and credit notes, and so do `search-contact-candidates` and `enrich-contact`. 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**: `nodes` array + `pageInfo.endCursor`.
- **Sort**: `list-accounts`, `list-aging-balance-by-account`, `list-invoices`, `list-credit-notes`,
  `list-communications` and `list-account-tasks` take `sortBy` and `sortOrder`
  (`asc`/`desc`). `sortBy` accepts 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).
- `send-outgoing-communication` and `update-outgoing-communication` verify that each attachment
  belongs to the account organization. An identifier present in a task or an older email is
  insufficient. If an older file cannot be verified, upload it again through `files`.
- `update-outgoing-communication` accepts `message` in markdown and new attachments in `files` as base64. Calls carrying files are not idempotent. Do not include a signature; the platform renders HTML/text and appends the agent signature automatically.

### Payment promise

Reads also expose `dateEvidence`, null when no source evidence was retained.
It contains `sourceEmailMessageId`, `authorEmail`, `receivedAt` and `quote`:
the source message, sender address, reception timestamp and verified quotation.
The server supplies this field; it is not a write parameter. The web app shows
the source on the account and in promise history. A quotation establishes its
source, never payment receipt or, on its own, the validity of a date calculation.
Older records receive no reconstructed quotation.

Promise reads expose `promisedDatePrecision`: `exact`, `conservative-window-end`
or `not-stated`. A window boundary or a calculated date without a stated day is
not an exact commitment. Historical records without precision are read as
`exact`. This field is independent of `amountIsEstimated`. The existing
`record-account-payment-promise` tool and
`POST /accounts/{accountId}/payment-promise` accept the same optional
`promisedDatePrecision` parameter. Omit it to preserve the active precision when
editing, or default to `exact` on creation. Use `exact` only for an explicit
commitment; `conservative-window-end` is an estimated window end and `not-stated`
a computed date without a stated day. Both estimates require `promisedDate` and
never replace an active exact commitment. Read the response: it returns the
object actually retained. The web app offers the same choices and prefills
precision when editing.

`get-account-dunning-status` returns the standing promise as `paymentPromise` (`null` when there is
none, including when the account owes nothing), while `record-account-payment-promise` and
`cancel-account-payment-promise` write it. Together they offer the same capabilities as the web app
and the public API (`GET`, `POST` and `DELETE` on `/accounts/{accountId}/payment-promise`). Reading
requires `mcp:read`, writing requires `mcp:write` plus the account's permissions.

A promise is a commitment to pay, read from what the customer said: it changes no accounting entry.
It is what the next reminder must not contradict, and what the cash forecast reads for exact commitments only. An estimate earns no promise
bonus and never becomes a broken commitment; its expiration requires a review. An account holds
one at a time, so recording the next one replaces the previous, except when an estimate meets
an active exact commitment: that is also how you edit it. A date
already in the past is refused, and a payment the customer says is already made is a payment
declaration.

`invoiceNumbers` carries the invoices the commitment names, never those guessed from the balance.
Without an amount, the promise is estimated at what those invoices still owe, and at the whole
balance when the commitment names none; `amountIsEstimated` reports it. The scope still defaults to
every invoice overdue on the day it is recorded: the numbers narrow it, they do not replace it.

### Announced payments and reconciliation

`list-account-payment-declarations`, `record-account-payment-declaration` and
`resolve-account-payment-declaration` offer the same capabilities as the web app and the API.
Reading requires `mcp:read`, writing requires `mcp:write` plus the account's permissions. An
announcement is deduplicated on `sourceRef`, and a report of a payment already held open (exactly
the same invoices, another `sourceRef`, no amount, currency or payment date that disagrees) is
folded into that declaration under `restatements` instead of creating a second one; resolving with
its `sourceRef` decides the declaration that absorbed it. `declaredStage` repeats what the customer said about
their payment without promoting it, among `authorized`, `processed`, `sent` and `paid`: only `paid`
asserts the payment itself, the other three announce a step that may still produce nothing, and the
reminder then asks where that step stands. `proofDocumentAttached` reports that the customer
attached supporting evidence for that payment: it does not prove receipt and does not change the
stage, it says the reconciliation starts from a reference. Resolving requires an explicit decision,
the note is optional; `PartiallyReceived` expects the total paid already set on the invoices; never infer `PaymentNotReceived` from an open invoice. `PaymentReceived` confirms the announced amount, including an installment: the review ends when recorded payments cover it in the account currency on every identified invoice, with no competing announcement. A remaining invoice balance alone no longer keeps the review open; uncertain reconciliation retains the hold. Closing the task alone
resolves nothing.

## Next Steps

- **[OAuth Authentication](/en/guides/oauth/)**
- **[OAuth Scopes](/en/guides/scopes/)**
- **[Pagination & Filtering](/en/guides/pagination/)**

### Incoming email account association

Call `get-email` with `type: "IncomingEmail"`, the `emailId` (from `list-emails`) and the
`organizationId`: alongside the content, `incoming.inboxEntry` then returns `communicationId`,
`organizationId`, `emailId`, `status`, `platformRejection`, `accountId`, `accountFullName`,
`accountDeletedAt`, and `taskId`. The account and task fields are nullable, and `inboxEntry` itself
is null when `organizationId` is omitted.

`Unclassified` can already have a resolved account without being a customer reply used in payment
follow-ups. `PendingReview` carries a candidate account and `Rejected` a rejected association.
When `accountDeletedAt` is set, the account page is no longer available. A missing entry in the
requested organization returns a not-found error; unauthorized access is rejected before reading.

The public API exposes the same read:
`GET /api/public/v1/incoming-emails/{emailId}/inbox-entry?organizationId={organizationId}`,
with the `communications:read` or `dunning:manage` scope. A missing entry returns 404. There the
account name is `accountName`, as in `GET /communications`, not `accountFullName`.

`DeliveryFailure` identifies an explicit delivery failure notice for at least one recipient.
It may carry a resolved account, but is not a contact reply and does not enter the payment
follow-up context. Both the public API and MCP inbox entry expose this status.

## Explicit BODACC decisions

`ignore-account-bodacc-alert` and `revoke-account-bodacc-alert-override` require `mcp:write` and an
explicit user decision. First read the detailed account and show SIREN, notice, procedure, dates
and consequences. Never infer the decision from acknowledgement or a general request to resume.
Send `accountId`, `siren`, `noticeId`, `status` and `confirmed: true`, plus a nonempty justification
to ignore. The tools use the same service as the webapp and public API. Read `applied`,
`retryRequired`, `dunningStatus` and `blockingTasks` before reporting the result. No immediate send
or legal validation is guaranteed. See [contracts and examples](/en/guides/account-tasks/).

To ignore, copy `bodaccSiren`, `bodaccNoticeId` and `bodaccStatus` from `get-account`, with
`canIgnoreBodaccAlert: true` for a new decision. `Clear` and `Unresolved` are not accepted.
If the scope changes, request new confirmation. The reason must contain 1 to 2000 characters
after trimming; request a shorter reason if it exceeds the limit. To revoke, use the scope
stored in `bodaccAlertOverride`, even if the current legal finding differs.

Explain the server-computed `summary`. If `retryRequired: true` or `dunningStatus: null`,
follow-up status remains unverified. Revocation cancels the exception and does not justify
claiming that the block was lifted. `applied` does not prove a new event, including an
idempotent revocation of an already invalidated decision.

Copy the technical account ID and reason exactly, preserving the reason's language. An explicit
confirmation already given after reviewing the scope remains sufficient if the fresh read shows
no change. This decision belongs to the Billabex user, never the debtor; do not ask the debtor
to confirm this exception.

### Confirm receipt for missing historical references

For `POST /api/public/v1/accounts/{accountId}/payment-declarations/{declarationId}/resolve`
or `resolve-account-payment-declaration`, use a user identity, `PaymentReceived`,
`historicalInvoicesOutsideBillabex: true`, and a required personal `note` (1 to 4000 characters,
not a default decision note). Explain how full receipt and the historical scope were verified,
including possible number variants. Service accounts cannot take this decision.

Responses expose `invoiceNumbersMissingFromAccount`, `historicalReceiptEligible`, and
`historicalReceiptAccountInvoices` (current invoice numbers, dates and amounts for review).
Submission rechecks eligibility: no invoices identified at recording, explicit references,
no matching invoice on the account or organization, successful reads, and no prepared email
awaiting approval on the review task. A previous different decision, including ordinary
`PaymentReceived`, returns the existing conflict.

The terminal result carries `historicalInvoicesOutsideBillabex: true`, the author, date and note.
It changes no invoice. Identical retries preserve the audit and retry review closure only.
Other open declarations continue holding reminders; otherwise ordinary follow-ups may resume
under their usual guards. This action sends no email. Missing references never prove receipt.

### Waiting after an invoice arrives

Automatic reminders wait at least 24 elapsed hours after each open, non-excluded invoice arrives.
A new invoice also postpones the consolidated reminder for older invoices. The planned
communication date remains available. While the first-reminder setting holds the account,
`firstReminderEligibleAt` includes this minimum invoice age.

## Import an attached invoice

Use `create-invoice-from-file` when the client can pass a native ChatGPT file reference. The
server lists it only to ChatGPT, recognized by its OAuth client: named ChatGPT or redirecting
to an OpenAI domain. Other clients, such as Claude or Microsoft 365 Copilot, do not see it.
Its `file` parameter requires `download_url` and `file_id`; `mime_type` and `file_name` are optional.
It does not require base64.

Otherwise, call `prepare-invoice-upload` with the invoice details and give the returned link to
the user. The link expires after 15 minutes. The user reviews the customer, dates and amounts,
selects a document and confirms the import. Preparing a link does not create an invoice.

Generate one UUID `importId` and reuse it with identical invoice details for retries and when
switching between these two methods. Completed retries return the same receipt. If the invoice
already exists, inspect it instead of changing its number. PDF, PNG, JPEG and WebP files are
limited to 10 MB. The existing `create-invoice` tool retains its base64 contract for programmatic clients.

Native downloads accept only `oaiusercontent.com` and its subdomains. Use the upload link for
other hosts or when ChatGPT/Work cannot provide a downloadable reference. Actual file-transfer
support must be verified with the client in use.

## Microsoft 365 Copilot

Billabex can provide its data and actions to **Microsoft 365 Copilot**, using a work or school
account. Each user connects their own Billabex account and retains their permissions for
organizations and customer accounts.

**Availability:** the Billabex agent package is prepared for a pilot. It has not been published
in the Agent Store. There is no public installation link at this stage.

### Use Billabex in Microsoft 365 Copilot

Once your Microsoft 365 administrator enables the agent:

1. Open Microsoft 365 Copilot and select the **Billabex** agent.
2. Select the Billabex sign-in prompt.
3. Sign in to your Billabex account and review the requested permissions.
4. Ask a question such as "Which customer invoices are overdue?" or
   "Prepare my daily brief with recent replies and tasks to address."

Analysis requests read data. Changes and outgoing emails require explicit instructions. Email
recipients and content are presented for approval. To import an attached invoice, the agent
provides a personal Billabex upload link.

Agent access depends on your organization's Microsoft 365 licenses or metered usage and its
administration policies. You also need an authorized Billabex account. Enabling the agent in an
organization does not grant additional Billabex permissions.

### Prepare a pilot with your administrator

The package uses Billabex's existing MCP server: **Streamable HTTP**, OAuth, and tool discovery
at runtime. It does not add a copy of the data to Microsoft Graph.

Your administrator or integration team can configure the agent using
[Microsoft 365 Agents Toolkit](https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/build-mcp-plugins):

1. Create a **Declarative Agent** and add an action using **Start with an MCP Server**.
2. Enter `https://next.billabex.com/mcp` for the relevant Billabex environment.
3. Select **OAuth (with dynamic registration)**. The server publishes OAuth metadata and its
   registration endpoint, so no OAuth client needs to be created manually.
4. Enable the agent in a test tenant, verify sign-in and tools, then make it available to
   authorized users according to the organization's policies.

The pilot checks initial sign-in, session refresh, permissions, organization selection,
pagination, and action confirmation. Any sending test must use an explicitly approved test recipient.

Microsoft's OAuth flow uses the callback
`https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect`. Use PKCE S256 for this flow.
Authorization metadata is available at `https://next.billabex.com/.well-known/oauth-authorization-server`.
Read tools require `mcp:read`, and write tools require `mcp:write`.

### Distribution to multiple companies

To provide a standard installation for customers across organizations, Billabex must submit its
agent through **Microsoft Partner Center**, in the Microsoft 365 and Copilot program, and
complete Microsoft's review. Deployment in a pilot tenant is not public publication. Customer
administrators retain control over available agents.

See [Microsoft's distribution process](https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/publish).

### Consumer Copilot

Copilot with a personal Microsoft account is a different experience. Microsoft documents
connectors for OneDrive, Outlook, and Google services, but no public workflow for freely adding a
third-party MCP server such as Billabex. Installing the Microsoft 365 agent does not add it to
consumer Copilot.

See [consumer Copilot connectors](https://support.microsoft.com/en-us/microsoft-copilot/connecting-microsoft-copilot-to-other-services).

### Further reading

- [OAuth authentication](/en/guides/oauth/)
- [OAuth scopes](/en/guides/scopes/)

### Invoice block owners

When present, `dunningBlock.holders` lists independent causes: `source` (`user` or
`task:<id>`), `conditionKind`, `note`, `blockedAt` and `blockedBy`. `blockedBy` is null
for a task hold. Legacy summary fields describe the human exclusion when present,
otherwise one task hold. An older response without `holders` represents one human exclusion.
Clearing the manual block (`set-invoice-dunning-block` with `blocked: false`, or
`DELETE /api/public/v1/invoices/:invoiceId/dunning-block`) removes only the human cause
and preserves independent task conditions.


When every open invoice is held only by task conditions, `silenceReason` is `invoice-decision-pending`. An explicit decision or verified resolution is required; a date passing does not release these holds. Manual exclusions remain distinct. The webapp, public API and `get-account-dunning-status` share this status.


### Request a credit top-up

After explicit user approval, call `update-organization` with only `organizationId` and
`creditTopUpRequest: { "credits": 200, "requestId": "UUID-v4" }`. Credits must be a multiple
of 100, from 100 to 2,147,483,600, at EUR 99 excluding VAT per pack of 100. Reuse the same
`requestId` when retrying the same request after a network error.

The REST equivalent is `POST /organizations/{organizationId}/credit-top-up-requests` under
`/api/public/v1`, with scope `organizations:all` and body `{ "credits": 200,
"requestId": "UUID-v4" }`. A `200` response contains `true` after the email is accepted.
The MCP returns the organization information and `creditTopUpRequest` containing `accepted: true`,
the quantity and reference. A receipt is sent to the authenticated user, with
`billing@billabex.com` copied. Billabex then processes the top-up manually: this request
neither charges a payment nor adds credits.

---

Billabex developer portal. OpenAPI specification: https://developer.billabex.com/openapi.json.
Agent instructions: https://developer.billabex.com/llms.txt. Complete documentation: https://developer.billabex.com/llms-full.txt.
