/clients
Clients
The people and companies you invoice.
- Filters
- clientTypecountryemail
Developers
Connect your shop, CRM or data warehouse to your books. One predictable REST interface for invoices, clients, payments and more — with scoped keys, signed webhooks and Swiss QR-bill PDFs.
Request
curl https://acme.v-ledge.vestryx.com/api/v1/invoices \
-G -d status=sent -d limit=1 \
-H "Authorization: Bearer $VLEDGE_API_KEY"Response200 OK
{
"data": [
{
"id": "9d3f6a10-52c4-4b8e-a0d7-6e1b2c3f4a58",
"invoiceNumber": "INV-0143",
"status": "sent",
"issueDate": "2026-10-14",
"dueDate": "2026-11-13",
"currency": "CHF",
"amount": 4918.55,
"amountPaid": 0
}
],
"pagination": { "limit": 1, "offset": 0, "total": 18 }
}The API gives your own software the same access to a workspace that your team has in the app. Every endpoint lives under one base URL and speaks JSON over HTTPS.
Base URL
https://acme.v-ledge.vestryx.com/api/v1
Replace acme with your workspace’s subdomain.
REST + JSON
Predictable resource URLs, standard HTTP verbs and JSON in both directions.
Workspace-scoped
Every key belongs to exactly one workspace. No organization IDs in your requests.
Scoped keys
Grant read, write or both, and revoke a key at any time.
OpenAPI 3.1
Generated from the same catalogue as the endpoints, so it never drifts.
From zero to your first invoice in three steps.
In v-ledge, open Settings → Company → Developer and choose New API key. Name it, pick its scopes and copy the secret. It starts with vlk_live_ and is shown only once, so store it right away:
export VLEDGE_API_KEY="vlk_live_…"List the five most recent clients to check that everything is wired up.
curl "https://acme.v-ledge.vestryx.com/api/v1/clients?limit=5" \
-H "Authorization: Bearer $VLEDGE_API_KEY"Send the client, dates and line items. v-ledge assigns the next invoice number and calculates subtotal, VAT and total for you.
curl https://acme.v-ledge.vestryx.com/api/v1/invoices \
-H "Authorization: Bearer $VLEDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientId": "2b0e8f4c-7d1a-4e3b-8c5f-1a9d6e2b7c40",
"issueDate": "2026-10-14",
"dueDate": "2026-11-13",
"currency": "CHF",
"taxRate": 8.1,
"items": [
{ "description": "Consulting, 24 h", "quantity": 24, "unitPrice": 150 },
{ "description": "Implementation", "quantity": 1, "unitPrice": 950 }
]
}'{
"data": {
"id": "9d3f6a10-52c4-4b8e-a0d7-6e1b2c3f4a58",
"invoiceNumber": "INV-0143",
"docType": "invoice",
"clientId": "2b0e8f4c-7d1a-4e3b-8c5f-1a9d6e2b7c40",
"status": "draft",
"issueDate": "2026-10-14",
"dueDate": "2026-11-13",
"currency": "CHF",
"subtotal": 4550,
"discountAmount": 0,
"taxRate": 8.1,
"taxAmount": 368.55,
"amount": 4918.55,
"amountPaid": 0,
"items": [
{ "position": 0, "description": "Consulting, 24 h", "quantity": 24, "unitPrice": 150, "kind": "service" },
{ "position": 1, "description": "Implementation", "quantity": 1, "unitPrice": 950, "kind": "service" }
]
}
}Send your secret key with every request in the Authorization header, using the Bearer scheme.
Authorization: Bearer vlk_live_…readwriteScopes don’t include each other. An integration that creates invoices and reads them back needs both.
Keep your keys secret
401.The same small set of conventions applies to every resource.
Content-Type: application/json.issueDate, clientId, unitPrice.YYYY-MM-DD for calendar dates. createdAt is an ISO 8601 timestamp.clientId must point to a record in the same workspace.4918.55. Currencies are ISO 4217 codes like CHF.id or amountPaid are ignored.// One record
{ "data": { "id": "9d3f6a10-…", … } }
// A list
{ "data": [ … ], "pagination": { "limit": 50, "offset": 0, "total": 128 } }
// After DELETE
{ "ok": true }
// Any error
{ "error": "items[0].quantity must be greater than zero" }Send only what changes. Everything else stays as it is.
curl -X PATCH https://acme.v-ledge.vestryx.com/api/v1/invoices/9d3f6a10-52c4-4b8e-a0d7-6e1b2c3f4a58 \
-H "Authorization: Bearer $VLEDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "dueDate": "2026-11-30", "notes": "Thank you for your business." }'List endpoints return 50 records by default. Page through larger collections with limit and offset.
limitoffset?status=overdue&clientId=…. See Resources for the full list.pagination.total holds the number of matching records, so you know when to stop. Documents and transactions come back newest first by date, clients and suppliers by creation date, and items alphabetically.
curl "https://acme.v-ledge.vestryx.com/api/v1/invoices?status=overdue&limit=200&offset=200" \
-H "Authorization: Bearer $VLEDGE_API_KEY"v-ledge uses conventional HTTP status codes, and every error body carries a readable message.
400Bad request
The body isn’t a JSON object, a field failed validation, or a referenced ID doesn’t exist in your workspace. The message names the field.
401Unauthorized
The Authorization header is missing, or the key is unknown, revoked or expired.
403Forbidden
The key is valid but lacks the scope this method needs.
404Not found
Unknown resource, malformed ID, or no such record in this workspace.
409Conflict
A document with the number you supplied already exists.
500Server error
Something went wrong on our side. Safe to retry with backoff.
Branch on the status code rather than the message text — messages are written for people and may be refined. Retry 500 responses and network failures with exponential backoff; a 4xx fails the same way until the request changes.
Eight resources share the same set of endpoints. Replace {resource} with the resource’s path.
/{resource}List records, paginatedread/{resource}Create a recordwrite/{resource}/{id}Retrieve a record, incl. line itemsread/{resource}/{id}Update the fields you sendwrite/{resource}/{id}Delete a recordwrite/invoices/{id}/pdfDownload as PDF (also estimates)read/clients
The people and companies you invoice.
/invoices
Invoices and credit notes, set with docType invoice or credit_note.
Line itemsAuto-numberedPDF
/estimates
Quotes you send before the work starts.
Line itemsAuto-numberedPDF
/payments
Incoming payments, optionally linked to an invoice and client. The amount must be greater than zero.
/expenses
Business expenses with category, vendor and payment method.
/suppliers
The vendors you buy from, with IBAN, VAT number and payment terms.
/items
Your product and service catalogue, with prices, tax rates and stock. stockQuantity is read-only.
/vendor-bills
Supplier bills and credit notes, set with docType bill or credit_note, incl. expense accounts and cost centers.
Line itemsAuto-numbered
The OpenAPI spec lists every writable field per resource.
Invoices, estimates and vendor bills accept an items array. Send the positions and let v-ledge do the arithmetic.
descriptionstringquantitynumberunitPricenumberkind"service" | "discount"discountMode"flat" | "percent"taxRatenumberInvoice items also take regionCode, estimate items serviceItemId, and vendor-bill items expenseAccount, costCenterId and serviceItemId.
subtotal is the sum of quantity × unitPrice over all service lines, minus discountAmount.taxAmount is the subtotal × the document’s taxRate.amount is subtotal + taxAmount, rounded to two decimals.Whenever you send items, these derived values replace any totals in the same request. On PATCH, items replaces every existing position, so always send the complete list. A document holds up to 200 positions.
{ "description": "Loyalty discount", "kind": "discount", "discountMode": "percent", "unitPrice": 10 }Documents get readable, sequential numbers and render to PDF in your workspace’s design.
Leave out invoiceNumber, estimateNumber or billNumber and v-ledge assigns the next free number. Supply your own to mirror another system — if it’s taken, you’ll get 409. Invoices (not credit notes) automatically receive a QR reference for the Swiss QR-bill.
GET /invoices/{id}/pdf and GET /estimates/{id}/pdf return application/pdf, rendered with your saved document design — including the Swiss QR-bill where it applies.
curl https://acme.v-ledge.vestryx.com/api/v1/invoices/9d3f6a10-52c4-4b8e-a0d7-6e1b2c3f4a58/pdf \
-H "Authorization: Bearer $VLEDGE_API_KEY" \
-o INV-0143.pdfWebhooks push events to your server the moment they happen, so you don’t have to poll.
whsec_…). Like an API key, it’s shown only once.order.createddelivery_note.createdinvoice.createdReserved events
These can already be selected on an endpoint but aren’t emitted yet.
invoice.sentinvoice.paidinvoice.overduepayment.createdclient.createdexpense.createdbill.createdbill.paidpayment_run.exported
Each delivery is a POST with a JSON body and these headers:
Content-Typeapplication/jsonX-Vledge-EventThe event name, e.g. invoice.createdX-Vledge-Signaturet=<unix time>,v1=<HMAC-SHA256 hex>{
"id": "5f2c9a71d0e84b3c6a1f9e27",
"event": "invoice.created",
"createdAt": "2026-10-14T09:41:00.000Z",
"organizationId": "c4a1e2d3-6b7f-4e8a-9c0d-1f2e3a4b5c6d",
"data": {
"id": "9d3f6a10-52c4-4b8e-a0d7-6e1b2c3f4a58",
"invoiceNumber": "INV-0143",
"amount": 4918.55,
"currency": "CHF",
"salesOrderId": "7e6d5c4b-3a29-4180-9f7e-6d5c4b3a2918"
}
}Compute an HMAC-SHA256 of {t}.{raw body} with your signing secret and compare it to v1 in constant time. Reject timestamps older than five minutes to block replays, and always use the raw body — re-serialized JSON won’t match.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.VLEDGE_WEBHOOK_SECRET;
// express.raw keeps the exact bytes that were signed.
app.post("/hooks/vledge", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Vledge-Signature") ?? "";
const { t, v1 = "" } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = createHmac("sha256", secret).update(`${t}.${req.body}`).digest("hex");
const valid = v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!valid || !fresh) return res.sendStatus(400);
res.sendStatus(200);
void handleEvent(JSON.parse(req.body)); // your own async processing
});The whole API is described in an OpenAPI 3.1 document, generated from the same catalogue that powers the endpoints.
Import it into Postman or Insomnia to explore the API interactively, or generate a typed client. Point the generated client at your workspace’s base URL.
# TypeScript types
npx openapi-typescript https://vestryx.com/api/v1/openapi.json -o vledge-api.d.ts
# A client for Python, Go, Java, …
npx @openapitools/openapi-generator-cli generate \
-i https://vestryx.com/api/v1/openapi.json -g python -o ./vledge-clientA short checklist before you switch your integration on.
One key per integration. Give each system its own key with the smallest scope it needs, so you can revoke one without touching the rest.
Keep secrets on the server. Load API keys and webhook secrets from environment variables or a secret manager — never from browser or mobile code.
Rotate without downtime. Create the new key, deploy it, then revoke the old one.
Retry the right things. Back off and retry 5xx responses and timeouts. Fix and resend 4xx requests.
Always paginate. Never assume a list fits on a single page.
Verify every webhook. Check the signature and timestamp, and deduplicate on the event id.
Tell us what you’re building — we’re happy to help you plan it.