Developers · Agents guide · OpenAPI · llms.txt · Markdown

Connect a service to InvoiceCow: the guide for agents

This guide is for an agent that runs on the merchant's computer, with the merchant's application and its repository. Follow the steps in order. Each step names its request to the API; the MCP server has a tool for most of them. The API is at https://invoicecow.com/api/v1, and its OpenAPI 3.1 document has every field.

The person does three things, at most: create or confirm the account and accept the Terms, approve your token, and authorize the processor. Do everything else yourself, and ask the person only what this guide tells you to ask.

0. Find out how the application sells

Read the application's payment code before you start. Find:

  • The processor that takes the money: Stripe (Checkout, Payment Links, invoices, subscriptions) or another one.
  • The seller of record. If a platform sells for the merchant (a merchant of record such as Paddle or Lemon Squeezy, an app store, a marketplace), InvoiceCow does not invoice those sales. Tell the person, and stop.
  • Where the application sends its receipt or confirmation email, if it sends one.

1. Get a token

Ask for a device code. Name yourself and the machine, and ask for the four scopes the recipe uses:

curl -s https://invoicecow.com/api/v1/agent/device \
  -d client_id="Claude Code" -d device_name="$(hostname)" -d scope="read write connections downloads"

The answer holds device_code, user_code, verification_uri_complete, expires_in (600 seconds) and interval (5 seconds). Give the person the link verification_uri_complete and the user_code. The person signs in or signs up, accepts the Terms if needed, checks that the page shows the same code, and approves.

Poll every interval seconds, with the same client_id:

curl -s https://invoicecow.com/api/v1/agent/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code -d device_code="$DEVICE_CODE" -d client_id="Claude Code"

authorization_pending means wait; slow_down means add 5 seconds to the interval; access_denied and expired_token end the request. The answer with access_token comes once. Keep the token in an environment variable, such as INVOICECOW_TOKEN, or in the person's secret store. Never write it to a file of the repository.

Check the token: GET /api/v1/organization returns the organization, the token's scopes and the buyer page.

To use the MCP server instead of plain requests, add it with the token:

Keep the token in an environment variable, here INVOICECOW_TOKEN, and never in a file of a repository.

Claude Code:

claude mcp add --transport http invoicecow https://invoicecow.com/mcp --header "Authorization: Bearer $INVOICECOW_TOKEN"

Cursor, in .cursor/mcp.json:

{
  "mcpServers": {
    "invoicecow": {
      "url": "https://invoicecow.com/mcp",
      "headers": { "Authorization": "Bearer ${env:INVOICECOW_TOKEN}" }
    }
  }
}

Codex, in ~/.codex/config.toml:

[mcp_servers.invoicecow]
url = "https://invoicecow.com/mcp"
bearer_token_env_var = "INVOICECOW_TOKEN"

2. Add the company

The company that sells is the issuer: its name, address and tax ids go on every invoice. Find them in the application (its legal page, its footer, its configuration), then show them to the person and ask the person to confirm them.

curl -s https://invoicecow.com/api/v1/issuers -H "Authorization: Bearer $INVOICECOW_TOKEN" \
  -H "Idempotency-Key: issuer-1" -H "Content-Type: application/json" \
  -d '{"legal_name":"Your Company Ltd","address_line1":"1 Example Street","city":"Dublin","postal_code":"D02 XY45","country_code":"IE","tax_ids":["IE3141592TA"]}'

The tool is create_issuer. Every write sends an Idempotency-Key: the same key on a retry, a new key for a new write. Each tool that changes records takes the same key as its argument idempotency_key; send it with every such call, so a retried call creates nothing twice.

3. Set the numbering

Each company has two series, invoice and credit_note. The default invoice pattern is {YYYY}- with six digits: 2026-000001, restarting each year. Ask the person whether the company already issues invoices with another tool. If it does, give InvoiceCow's series a prefix of its own, so the two series never share a number:

curl -s -X PATCH https://invoicecow.com/api/v1/issuers/$ISSUER_ID/number_sequences/invoice \
  -H "Authorization: Bearer $INVOICECOW_TOKEN" -H "Idempotency-Key: numbering-1" -H "Content-Type: application/json" \
  -d '{"prefix":"IC-{YYYY}-","padding":6}'

The answer shows next_number. The tool is set_number_pattern.

4. Connect the processor

Stripe. The InvoiceCow Stripe App (Pending Stripe review) connects in one click: POST /api/v1/connections/stripe/authorizations with the issuer_id returns the install link, and the person installs the app on Stripe. The tool is start_stripe_authorization. While the app waits for Stripe's review, that request answers 503 with stripe_app_pending_review. Then ask the person to connect Stripe in the dashboard, at https://invoicecow.com/dashboard/connections/new?kind=stripe, with a restricted key that can only read (checkout_sessions, payment_intents, charges, disputes, invoices, invoice_payments, customers, account) and the signing secret of a Stripe webhook endpoint. Never ask for the key or the secret yourself. GET /api/v1/connections shows the connection when it is live.

Another checkout with server code. Open a webhook connection:

curl -s https://invoicecow.com/api/v1/connections -H "Authorization: Bearer $INVOICECOW_TOKEN" \
  -H "Idempotency-Key: webhook-1" -H "Content-Type: application/json" \
  -d '{"issuer_id":"'$ISSUER_ID'","kind":"webhook"}'

The answer holds webhook_url and secret, once. Put both where the application keeps its other secrets (for example INVOICECOW_WEBHOOK_URL and INVOICECOW_WEBHOOK_SECRET in its production environment), never in the repository. Then add the sale call of the signed webhook to the payment path, after the payment succeeds, and a test for it. The tool is create_webhook_connection.

Past sales, from any source, can come as a CSV file: open a connection with "kind": "csv" and post the file to /api/v1/connections/{id}/imports.

5. Put the buyer page in the receipt

Give the buyer page an address, if it has none, and read the texts that share it:

curl -s -X PATCH https://invoicecow.com/api/v1/portal -H "Authorization: Bearer $INVOICECOW_TOKEN" \
  -H "Idempotency-Key: portal-1" -H "Content-Type: application/json" -d '{"slug":"yourcompany"}'
curl -s "https://invoicecow.com/api/v1/portal?language=en" -H "Authorization: Bearer $INVOICECOW_TOKEN"

receipt_line goes into the receipt or confirmation email that the application sends: add it to that email's template in the repository. stripe_receipt_text goes into the custom message of Stripe's receipts, which only the person can set in Stripe: give it to the person. The tools are set_portal_address and receipt_snippets.

6. Make a test sale

Ask the person to buy the cheapest product once, with a real payment. After the check, ask the person to refund it: the refund issues a credit note, so the numbering stays complete. Never send invented sales to a live connection: each one becomes a numbered invoice of the company.

7. Check the setup

curl -s https://invoicecow.com/api/v1/setup -H "Authorization: Bearer $INVOICECOW_TOKEN"

ready is true when the company, a live connection, the buyer page and a numbered invoice exist; each check says what it found or what is missing. The tool is verify_setup. Then GET /api/v1/payments?limit=1 shows the test sale with its invoice number and portal_url. Tell the person the invoice number and the buyer page's link.

What to ask the person

  • To approve your token, on the page of step 1.
  • To confirm the company's name, address and tax ids.
  • Whether the company already issues invoices, and with which numbers.
  • To authorize the processor: install the Stripe App, or enter the restricted key in the dashboard.
  • To set the Stripe receipt text, if the company sells through Stripe.
  • To make one real test purchase, and to refund it after the check.

What never to do

  • Never write a token, a key or a secret to a file of the repository, a log or a commit.
  • Never ask the person for a Stripe secret key, and never handle a Stripe key yourself.
  • Never send a sale whose seller is a platform or a marketplace.
  • Never send invented or duplicate sales to a live connection.
  • Never calculate tax: send the tax the checkout charged.
  • Never connect the same sales twice, for example through Stripe and through a webhook.

The signed webhook

A checkout that is not Stripe sends each paid sale to its webhook connection. Open the connection with POST /api/v1/connections and "kind": "webhook"; the answer holds its webhook_url and its secret, once.

Send each event as an HTTP POST with a JSON body to the webhook_url. Sign the exact bytes you send:

InvoiceCow-Signature: t=<unix seconds>,v1=<signature>

<signature> is the lowercase hex HMAC-SHA256 of <t>.<body>, with the connection's secret as the key. InvoiceCow refuses a request with no signature, a wrong signature, or a time more than five minutes from its clock (400), and a sale that is not valid (422, with the reason in error and the field in details). It answers 200 when it stored the event. An event id it already has gets 200 and changes nothing, so a retry is safe.

The sale call in Ruby, with the standard library only. Put it in the application's payment path, after the payment succeeds:

require "json"
require "net/http"
require "openssl"

# Sends one paid sale to InvoiceCow. The URL and the secret come from the environment.
def send_sale_to_invoicecow(order_id:, paid_at:, currency:, amount:, buyer:, lines:)
  body = JSON.generate(id: "sale-#{order_id}", type: "sale.paid", sale: {
    merchant_sale_id: order_id, currency:, amount:, paid_at: paid_at.utc.iso8601, buyer:, lines:
  })
  timestamp = Time.now.to_i
  signature = OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("INVOICECOW_WEBHOOK_SECRET"), "#{timestamp}.#{body}")
  response = Net::HTTP.post(URI(ENV.fetch("INVOICECOW_WEBHOOK_URL")), body,
    "Content-Type" => "application/json", "InvoiceCow-Signature" => "t=#{timestamp},v1=#{signature}")
  raise "InvoiceCow refused the sale: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
end

The same call in TypeScript, for Node 18 or later:

import { createHmac } from "node:crypto";

// Sends one paid sale to InvoiceCow. The URL and the secret come from the environment.
export async function sendSaleToInvoiceCow(sale: {
  orderId: string; paidAt: Date; currency: string; amount: number;
  buyer: Record<string, string>; lines: Array<Record<string, unknown>>;
}): Promise<void> {
  const body = JSON.stringify({ id: `sale-${sale.orderId}`, type: "sale.paid", sale: {
    merchant_sale_id: sale.orderId, currency: sale.currency, amount: sale.amount,
    paid_at: sale.paidAt.toISOString(), buyer: sale.buyer, lines: sale.lines } });
  const timestamp = Math.floor(Date.now() / 1000);
  const signature = createHmac("sha256", process.env.INVOICECOW_WEBHOOK_SECRET!)
    .update(`${timestamp}.${body}`).digest("hex");
  const response = await fetch(process.env.INVOICECOW_WEBHOOK_URL!, { method: "POST", body,
    headers: { "Content-Type": "application/json", "InvoiceCow-Signature": `t=${timestamp},v1=${signature}` } });
  if (!response.ok) throw new Error(`InvoiceCow refused the sale: ${response.status} ${await response.text()}`);
}

The client libraries (Planned), the gem invoicecow for Ruby and the npm package invoicecow for JavaScript, wrap the same call, the signature, and the reading of documents through the API. They need no other library.

A sale of Your Company Ltd to a business buyer:

send_sale_to_invoicecow(order_id: "order-1001", paid_at: Time.now, currency: "EUR", amount: 49000,
  buyer: { email: "[email protected]", company_name: "Kessler & Vogt GmbH", tax_id: "DE271828182", country_code: "DE" },
  lines: [ { description: "Pro plan · yearly", quantity: "1", net: 49000, tax: 0, gross: 49000 } ])

The rules of the fields:

Field Rule
id Your event id, unique for the connection.
type sale.paid or sale.refunded. Both carry the whole sale.
processor, processor_account_id, processor_sale_id The sale's key across all connections. Send all three, or none.
merchant_sale_id Your own id. Without the three processor fields, it is the sale's key under this connection.
currency ISO 4217, three letters.
amount, net, tax, gross, unit_amount, refunded Integers in the currency's minor unit (cents for EUR).
paid_at An ISO 8601 time. supply_date and tax_point are optional ISO 8601 dates; both default to the payment date.
buyer Every field is optional text. A company name, a tax id or "type": "business" makes a business buyer. country_code is ISO 3166-1 alpha-2. The buyer completes missing fields on the buyer page.
lines At least one line. For each line net + tax = gross, and the lines' gross add up to amount. The tax is the tax you charged. product is { "id", "name" } or absent.
refunded The cumulative refunded amount. A higher value issues a credit note for the difference.

A sale whose seller is not the merchant's company (a marketplace or a merchant of record) does not go here: InvoiceCow would issue an invoice in the merchant's name.