Idempotency
The tools that create a resource or move money accept an optional idempotency_key argument. It is sent to the endpoint as the X-Idempotency-Key header the REST API already honours.
{ "name": "billing_invoices_create",
"arguments": {
"billing_account_id": "…",
"total": 1250.00,
"idempotency_key": "7c4a8d09-ca37-4f21-b1f0-3d2f2b8c9a11"
} }
| Format | Opaque string, 1 to 255 characters. A UUID is the usual choice. |
| Replay, same body | Returns the original response, without running the operation again. |
| Replay, different body | Conflict. |
Why an agent needs this more than your backend does
An MCP client that times out and tries again, or a model that decides "I am not sure that worked, let me redo it", is exactly the pattern that creates the second invoice, the second charge and the second customer. Your backend has a retry loop you wrote; the agent has a judgement call.
Send a key whenever the operation charges, issues or creates. Reads and updates are naturally idempotent and take no key.
Tools that accept it
| Family | Tools |
|---|---|
customers | billing_customers_create, billing_customers_upsert, billing_billing_accounts_create, billing_billing_accounts_upsert |
portal-users | billing_customer_users_create, billing_customer_users_invite |
users, roles | billing_dashboard_users_create, billing_dashboard_users_upsert, billing_team_roles_create, billing_team_roles_upsert |
products, plans, services | billing_products_create, billing_products_upsert, billing_product_groups_create, billing_product_groups_upsert, billing_plans_create, billing_plans_upsert, billing_plan_groups_create, billing_plan_groups_upsert, billing_service_items_create, billing_service_items_upsert |
subscriptions, plan-changes | billing_subscriptions_create, billing_subscriptions_upsert, billing_subscriptions_items_change, billing_plan_changes_execute, billing_plan_changes_rules_create |
invoices | billing_invoices_create, billing_invoices_upsert, billing_invoices_finalize, billing_invoices_pay, billing_invoices_installments_pay, billing_subscriptions_create_invoice |
payments | billing_payments_create, billing_payments_retry, billing_payments_refund, billing_payment_methods_create, billing_payment_methods_upsert |
credits, coupons | billing_credits_create, billing_credits_upsert, billing_coupons_create, billing_coupons_upsert |
nfes, companies | billing_nfes_create, billing_nfes_upsert, billing_nfes_issue, billing_companies_create, billing_companies_upsert |
tax-rules, tax-periods, withholdings | billing_tax_rules_create, billing_tax_rules_upsert, billing_tax_periods_process_month, billing_withholdings_create, billing_withholdings_upsert, billing_withholdings_convert_to_credit, billing_withholdings_convert_to_credits |
proposals, templates | billing_proposals_create, billing_proposals_upsert, billing_proposals_send, billing_proposals_accept, billing_proposal_templates_create, billing_proposal_templates_upsert |
usage | billing_usage_report, billing_usage_snapshots_daily_create, billing_usage_snapshots_monthly_create |
:::caution The ones that charge
billing_invoices_pay, billing_payments_create, billing_payments_retry and billing_nfes_issue produce an effect in the world — a charge at the gateway, a tax invoice at the tax authority — that no _delete undoes. Send an idempotency_key on those even when you think you will not repeat.
:::
Upsert is not idempotency
*_upsert matches an existing record by id, then external_id, then the resource's business key, and creates only if it finds none. That solves "I do not know whether it already exists", which is a different problem from "I do not know whether my call arrived". The two combine well: an upsert with an idempotency_key is safe on both dimensions.