Draft an invoice from Codex
Codex reads MCP servers from ~/.codex/config.toml or a project-scoped .codex/config.toml. Add a [mcp_servers.billr] table with the Billr URL and bearer_token_env_var, restart Codex, and the session can draft invoices as drafts.
Written and reviewed by the Billr team. Published , last reviewed .
- Who this is for
- People running Codex in a terminal, the IDE extension, or the desktop app who want their own config, not a one-line command.
- Billr capabilities used
list_unbilledcreate_invoice
What you need first
- A Billr account with at least one client, and the work you want to bill already logged. See Find unbilled work if the entries are not in Billr yet.
- Codex installed: the CLI, the IDE extension, or the desktop app. Codex's MCP documentation states that all of these read the same configuration file.
- An API key with the Read and draft profile, and a way to keep it in an environment variable instead of pasting it into a file.
Set it up
01Create a scoped key in Billr
In Billr, open Agents → API Keys and create a key with the Read and draft profile. Keys expire after 90 days and can be revoked at any time. Read API keys and scopes before you decide which profile to grant.
02Put the key in an environment variable
Codex reads the token from the environment variable you name in the config, so the key itself never has to sit in a dotfile:
export BILLR_API_KEY="billr_sk_your_key"Export it from the shell or profile that launches Codex, or set it in the app's own environment settings. The variable has to be visible to the process that starts Codex.
03Add the server to config.toml
Codex stores MCP servers as a
[mcp_servers.<name>]table, by default in~/.codex/config.toml. Add:[mcp_servers.billr] url = "https://mcp.billr.cc" bearer_token_env_var = "BILLR_API_KEY"urlis the MCP endpoint, andbearer_token_env_varis the name of the variable holding the token, not the token itself. That indirection is why this file can be checked in safely; never put the key itself in a config file.04Or scope it to one trusted project
If you would rather not enable Billr everywhere, put the same table in
.codex/config.tomlat the root of a trusted project. The desktop app and the IDE extension can also add the server from their settings screens using the same URL and a Streamable HTTP entry.05Restart Codex and check the server
codex mcp listIn the Codex TUI,
/mcplists the active servers. The desktop app and the IDE extension show the same list under their MCP server settings. Codex reads the config at startup, so a running session will not pick up the new table until it is restarted.06Draft from unbilled work
With the work already logged, one prompt is enough. Naming the source makes the linkage explicit:
Draft the month-end invoice for Harbor & Lane from their unbilled time and expenses. Leave it as a draft.
Worked example
Fictional account, field names as returned by the server
You say
Draft the month-end invoice for Harbor & Lane from their unbilled time and expenses. Leave it as a draft.
Your agent calls
list_unbilled { "client_id": "9f2a4d18-3b7c-4a6e-8f0d-1c5b2e9a7d44" }create_invoice { "request_id": "1f5a7c93-2b64-4d08-9e31-6c0a4b7d2f85", "client_name": "Harbor & Lane", "issue_date": "2026-09-30", "time_entry_ids": [ "3f6c1a44-5b2d-4e71-9c08-7a1d3f5b9e22", "c2b7e4d9-8f30-4a51-9e6d-3b5a1c7f0d92", "9a2d0b71-6c34-4f52-8e19-2b7c4d0a6f31" ], "expense_ids": [ "d41b8e93-2a70-4c15-9b6f-5e8a1c3d7f08" ] }
Expected response, abridged (source lines keep the shape the server writes)
{
"summary": "Drafted INV-1148 for Harbor & Lane: $1,060.00 Source work was priced and linked atomically.",
"invoice": {
"id": "b7c1e5a0-51de-4b2c-9f0a-8f2c4d1e6a33",
"invoice_number": "INV-1148",
"status": "draft",
"issue_date": "2026-09-30",
"due_date": "2026-10-14",
"currency": "USD",
"total": 1060,
"balance_due": 1060,
"line_items": [
{
"description": "Schema markup fixes (5400 seconds)",
"quantity": 1,
"rate": 225,
"amount": 225
},
{
"description": "Content refresh (16200 seconds)",
"quantity": 1,
"rate": 675,
"amount": 675
},
{
"description": "Analytics audit (2700 seconds)",
"quantity": 1,
"rate": 112.5,
"amount": 112.5
},
{
"description": "Client lunch",
"quantity": 1,
"rate": 47.5,
"amount": 47.5
}
]
},
"linked_time_entry_ids": [
"3f6c1a44-5b2d-4e71-9c08-7a1d3f5b9e22",
"c2b7e4d9-8f30-4a51-9e6d-3b5a1c7f0d92",
"9a2d0b71-6c34-4f52-8e19-2b7c4d0a6f31"
],
"linked_expense_ids": [
"d41b8e93-2a70-4c15-9b6f-5e8a1c3d7f08"
],
"public_url": "https://billr.cc/i/8pLw2Xk7Qa",
"pdf_url": "https://billr.cc/api/public/invoices/8pLw2Xk7Qa/pdf",
"app_url": "https://billr.cc/invoices/b7c1e5a0-51de-4b2c-9f0a-8f2c4d1e6a33"
}What this flow does not do
- The invoice is a draft. Sending it means calling
send_invoicewith a key that has the billing or full scope. The key's scope is the guardrail; human approval is required for task pricing and task invoice approval, not for an ordinary invoice send. - Billr does not use OAuth. There is no
codex mcp login billrstep; the bearer token is the whole authentication story. - If no credential resolves in Codex's environment, Codex can still start the server, and Billr then treats calls as unauthenticated and rejects them with a 401. That is a rejected call, not a silent empty account.
- The billed amounts come from the logged work. Entries logged without a rate cannot be attached to an invoice at all; the server rejects them with
Time must be unbilled, billable, priced and assigned to this client.
Troubleshooting
- Tools appear, but every call is rejected
- Confirm the variable is visible to the process that launches Codex without printing the key: test -n "$BILLR_API_KEY" && echo "BILLR_API_KEY is set" || echo "BILLR_API_KEY is unset" executed in the same shell. GUI apps and IDE extensions may not inherit shell exports, so set the variable where the app is launched or use the app's own environment settings. The hosted endpoint is https://mcp.billr.cc.
- The billr server does not appear in codex mcp list
- Check the table name and the file you edited. Codex reads ~/.codex/config.toml, or .codex/config.toml in a trusted project. TOML is case-sensitive, and the header must be [mcp_servers.billr].
- The server was working and stopped after a restart
- The environment variable may have been set for one shell session only. Move the export into the profile your terminal loads, or use the app-level environment setting instead.
- A call is refused with a permission message
- Billr checks the key profile per tool. Read and draft can read and write; sending needs the billing profile, and deleting unbilled work needs the full profile.
- The agent created two drafts for one request
- Retries must reuse the same request_id and the same input. create_invoice returns the original draft when the id matches, and creates a new invoice when it does not.
Sources
Reviewed 2026-09-20 against Billr's own tool implementations and the client documentation linked above.
Read next
A hosted account gives you the MCP endpoint this guide points at; a self-hosted install serves the same tools from its own origin.
Create an account