# 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](https://invoicecow.com/api/v1/openapi.json) 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:

```bash
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`:

```bash
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:

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

Cursor, in `.cursor/mcp.json`:

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

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

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

```bash
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:

```bash
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:

```bash
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](#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:

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

```bash
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:

```ruby
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:

```ts
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:

```ruby
send_sale_to_invoicecow(order_id: "order-1001", paid_at: Time.now, currency: "EUR", amount: 49000,
  buyer: { email: "billing@kessler-vogt.example", 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.

