Skip to main content
Documentation

Invoices

invoices.record_payment

Record money already received against one invoice
WritesSensitiveWrite budget

REST

Shipping
POST /api/v1/invoices/record_payment

MCP tool

Live
invoices.record_payment

Exposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Invoices domain.

Operating contract

WRITES DOWN MONEY THE SHOP HAS ALREADY BEEN HANDED — cash at the counter, a cheque, an e-transfer that landed, a card taken on somebody else's terminal. It CANNOT charge anybody: no card is touched, no payment is requested, nothing reaches the customer.

Only ever call this when a person has told you the money arrived. A model that records a payment because a customer SAID they would pay has written a shop's balance down against money that does not exist, and the customer stops being chased for it.

method says how it came in and it must be true — the reconciliation between the ledger and the shop's bank reads it. received_on is the civil date in the shop's own timezone; omit it and today is used.

tip rides ALONGSIDE the payment and never comes off the balance. A $100 payment with a $20 tip settles $100 of the bill.

Refused when the amount is more than the balance due, when the invoice is still a draft (issue it first), and when it is void or refunded. Read invoices.get for the current balance rather than assuming the one you last saw.

IDEMPOTENCY IS none: calling this twice records the money twice, and there is no key that makes it not. Read payments.list for this invoice before recording anything you are not certain about.

Who may call it

Permission
invoices.accessThe caller must hold Invoices at the ACT level. A read-only dashboard grant on the same section is refused.
Plan
Every planNo plan gate. Available on every Service VIN plan.
Retries
noneA repeat genuinely does it twice and no key can make it not. Read before you write; the operating contract says what to read.
Rate class
writeCounted against the write budget, which is tighter than a read.

Input

FieldTypeDescription
invoice_idrequiredstringuuid

The issued invoice, as returned by invoices.list or invoices.get.

amountrequirednumbermax 10000000

How much was received, in the shop's currency. Must not exceed the balance.

methodrequiredstring

How the money arrived. Must be the truth — the bank reconciliation reads it.

One of: cash, card, cheque, etransfer, ach, other
tipnumber0–10000000

A gratuity received alongside. Never applied to the balance.

received_onstring

The civil date it arrived, YYYY-MM-DD in the shop's timezone. Defaults to today.

notestringmax 500 chars

A short note for the ledger — a cheque number, who took it.

Output

FieldTypeDescription
invoice_idstring

recorded_amountnumber

already_recordedboolean

statusstring

balancenumber

Examples

Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.

curl
export SERVICEVIN_API_KEY=svk_live_…

curl -X POST https://www.servicevin.com/api/v1/invoices/record_payment \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"invoice_id":"9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20","amount":1,"method":"cash"}'

TypeScript (fetch)
const res = await fetch("https://www.servicevin.com/api/v1/invoices/record_payment", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "invoice_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
    "amount": 1,
    "method": "cash"
  }),
});

// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;

Python (requests)
import os, requests

res = requests.post(
    "https://www.servicevin.com/api/v1/invoices/record_payment",
    headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
    json={
    "invoice_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
    "amount": 1,
    "method": "cash"
},
    timeout=30,
)
payload = res.json()
if not res.ok:
    raise RuntimeError(payload["error"]["code"])
data = payload["data"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "invoices.record_payment",
    "arguments": {
      "invoice_id": "9b2f1c6e-4a77-4d2b-9f31-0f1c9a8e5d20",
      "amount": 1,
      "method": "cash"
    }
  }
}

Refusals

The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.

StatusCodeWhen
404not_foundThe id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both.
403forbiddenThis login does not hold invoices.access.
403insufficient_scopeThe credential is read-only and this capability writes.
422validation_errorAn argument was wrong. The message names the field.
429rate_limitedToo many write calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.