Ownware
Home›Commissa›API
Commissa · API

Commissa API, as shipped in the download

Commissa REST API, MCP & Webhooks (v3.0)

Commissa ships a JSON REST API and signed webhooks so deals can arrive from the system that already knows about them — a CRM, an order form, a nightly export — and so payout figures can be pulled into payroll instead of retyped from a PDF.

Money is integer cents everywhere in this API. No floats, no currency strings to parse.

Authentication

Create a key in API & Webhooks. A key belongs to a user and can do exactly what that user can do in the app. Send it on every call:

Authorization: Bearer cmk_...

The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.

/api/* is CSRF-exempt by design — the key is the credential, and browsers never send it implicitly.

Endpoints

Method & pathWhat it does
GET /api/pingVerify a key
GET /api/dealsNewest 50. ?rep_id=, ?status=, ?category=, ?from=, ?to=
POST /api/dealsRecord a deal
GET /api/deals/{id}One deal
GET /api/repsThe rep list
GET /api/periodsPay periods
GET /api/periods/{id}/statement?rep={id}One rep's statement, penny-exact
GET /api/periods/{id}/payoutsEvery active rep's net for the period, one call
GET /api/openapi.jsonOpenAPI 3 spec (no auth)
GET /healthz{"ok":true,"app":"commissa","version":"2.0.0"} (no auth)

Record a deal

curl -X POST https://your-install/api/deals \
  -H "Authorization: Bearer cmk_..." -H "Content-Type: application/json" \
  -d '{"rep_id": 3, "amount": "4500.00", "deal_date": "2026-03-04",
       "product": "Enterprise annual", "category": "software", "status": "won"}'

amount is decimal in, integer cents out. status is won or pending; only won deals reach a statement. Validation is not re-implemented for the API — the request is passed through _deal_from_post() and _deal_refusal(), the exact pair the browser form uses, so a rule can never apply in the UI and not over HTTP:

{"error": "unprocessable", "detail": "Choose which rep closed the deal."}

Statements are the core's arithmetic, not the API's

/api/periods/{id}/statement returns whatever Commission::payout() computed. **Nothing is re-added, re-rounded or re-derived in the API layer** — the JSON, the HTML statement, the CSV and the PDF are the same numbers to the penny.

That matters most under a tiered scheme. Per-deal commission is defined as the *delta of the cumulative function*, so the deal lines telescope to the whole-period figure exactly rather than drifting a cent at each band boundary. A worked case, and one the suite asserts:

5% up to 100,000.00, then 8%. Three deals of 40,000.00 → volume 120,000.00.
5% of 100,000.00 = 5,000.00, plus 8% of 20,000.00 = 1,600.00 → gross 6,600.00, and the three
line commissions sum to exactly that.

Because a tiered deal can straddle two bands, a line carries a human rate_label ("5% → 8%") rather than a single rate:

{"statement": {
   "gross_cents": 660000, "adj_total_cents": -25000, "net_cents": 635000,
   "volume_cents": 12000000, "deal_count": 3,
   "rule": {"kind": "tiered", "name": "Tiered", "effective_from": "2026-01-01"},
   "lines": [{"deal_id": 1, "amount_cents": 4000000, "rate_label": "5%",
              "commission_cents": 200000, "running_volume_cents": 4000000,
              "running_commission_cents": 200000}, ...],
   "adjustments": [{"kind": "clawback", "amount_cents": -25000, "reason": "Returned order"}]}}

Adjustments are signed and move net_cents only — gross_cents stays the computed commission, so a bonus or clawback never hides inside the rate.

MCP (Model Context Protocol) — new in 3.0

Commissa speaks MCP over stateless streamable HTTP at POST /mcp. Authentication is the same revocable API key as the REST API (Authorization: Bearer <key>), and a key carries its owner's role, so an agent can never do more than the person whose key it holds.

GET /mcp answers 405 with Allow: POST — deliberately without the key check, so a probing client is told "right endpoint, wrong verb" rather than a bare 401 that reads as "no MCP here".

POST /mcp   {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
POST /mcp   {"jsonrpc":"2.0","id":2,"method":"tools/list"}
POST /mcp   {"jsonrpc":"2.0","id":3,"method":"tools/call",
             "params":{"name":"statement_preview","arguments":{"period_id":1,"rep_id":1}}}
ToolWrites?What it does
list_repsnoReps and their active flag
rep_detailnoOne rep, their deals, and the commission rule that applies today
record_dealyesRecords a deal. Only "won" deals earn commission
statement_previewnoA rep's payout for a period: deals, rate or tier applied, adjustments, net
period_reportnoEvery rep's payout for a period, with totals
list_disputesnoDisputes, newest first, optionally filtered by status

Deciding a dispute is deliberately NOT a tool. Upholding one moves money; that stays a human decision on the Disputes screen, where the decide-once rule and the audit trail live.

Argument hygiene. Tool arguments take the same scalar guard as the REST body — a value may not be an array or object unless the tool allowlists it, unknown arguments are refused by name, and an out-of-range status is refused with the list of what is accepted (won, pending).

One computation. statement_preview and period_report return Commission::payout() and Commission::periodSummary() — the same functions behind the HTML statement, the CSV and the PDF. Verified live: gross, adjustments, net and volume are identical across all five surfaces.

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http commissa https://your-install/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "commissa",
  "server_url": "https://your-install/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "commissa", "name": "Commissa", "url": "https://your-install/mcp",
      "token": "apk_xxxx", "enabled": true }
  ]
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://your-install/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Commissa implements the protocol, not an integration with a particular vendor.

Webhooks

Add receiver URLs in API & Webhooks. Event:

  • deal.recorded — a deal was entered, by the form, the API or the AI tool. A CSV import records its rows without sending an event per row.

Every delivery is signed:

X-Commissa-Event: deal.recorded
X-Commissa-Signature: sha256=<hmac_sha256(body, your endpoint secret)>

Verify by recomputing the HMAC over the raw body with the endpoint's secret. Deliveries are logged (newest 200 per endpoint) with the response code, so "did it fire?" is answerable from the admin page.

Delivery is best-effort with a short timeout: a slow receiver must never block a deal entry.

Documented limits

  • There is no period.closed event. From 3.1.4 a period can be closed in the app: its statements are then stored as they stood, GET /api/periods reports closed and closed_at, and GET /api/periods/{id}/statement and /payouts return the stored statements. An open period is still computed live from the deals inside it. No webhook fires on a close.
  • Deals are create-and-read over the API. Editing and deleting stay in the UI — changing a recorded deal changes a paid commission, and that should have someone's eyes on it.
  • Reps, rules, periods and adjustments are read-only over the API (adjustments are visible on a statement). A commission scheme is a contract; it is not set by an integration.
  • The CSV import never creates a rep. A row naming someone absent from the rep list is skipped and reported — inventing a salesperson from a typo'd column would silently reroute a payout.

← Back to Commissa · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →