Developers

The v-ledge API.

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.

acme.v-ledge.vestryx.com/api/v1

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 }
}

Overview

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.

Quickstart

From zero to your first invoice in three steps.

  1. 1

    Create an API key

    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:

    Terminal
    export VLEDGE_API_KEY="vlk_live_…"
  2. 2

    Make your first request

    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"
  3. 3

    Create an invoice

    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 }
        ]
      }'
    Response · 201 Created (abridged)
    {
      "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" }
        ]
      }
    }

Authentication

Send your secret key with every request in the Authorization header, using the Bearer scheme.

HTTP header
Authorization: Bearer vlk_live_…

Scopes

read
List and retrieve records, and download PDFs. Required for every GET.
write
Create, update and delete records. Required for POST, PATCH and DELETE.

Scopes don’t include each other. An integration that creates invoices and reads them back needs both.

Keep your keys secret

Use keys only from your server — never in browser or mobile code, and never in version control. v-ledge stores just a SHA-256 hash, so a lost key can’t be shown again: revoke it under Settings → Company → Developer and create a new one. Revoked or expired keys receive 401.

Requests & responses

The same small set of conventions applies to every resource.

Bodies
JSON objects, sent with Content-Type: application/json.
Field names
camelCase, e.g. issueDate, clientId, unitPrice.
Dates
YYYY-MM-DD for calendar dates. createdAt is an ISO 8601 timestamp.
IDs
UUIDs. References such as clientId must point to a record in the same workspace.
Amounts
Plain numbers in the document currency, e.g. 4918.55. Currencies are ISO 4217 codes like CHF.
Updates
PATCH changes only the fields you send. Unknown and read-only fields such as id or amountPaid are ignored.
Response shapes
// 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" }

Updating a record

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." }'

Pagination & filters

List endpoints return 50 records by default. Page through larger collections with limit and offset.

limit
Records per page, from 1 to 200. Defaults to 50.
offset
Records to skip. Defaults to 0.
Filters
Exact-match query parameters per resource, e.g. ?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"

Errors

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.

Resources

Eight resources share the same set of endpoints. Replace {resource} with the resource’s path.

GET/{resource}List records, paginatedread
POST/{resource}Create a recordwrite
GET/{resource}/{id}Retrieve a record, incl. line itemsread
PATCH/{resource}/{id}Update the fields you sendwrite
DELETE/{resource}/{id}Delete a recordwrite
GET/invoices/{id}/pdfDownload as PDF (also estimates)read

/clients

Clients

The people and companies you invoice.

Filters
clientTypecountryemail

/invoices

Invoices

Invoices and credit notes, set with docType invoice or credit_note.

Line itemsAuto-numberedPDF

Filters
statusclientIddocType
Status
draftsentpartially_paidpaidoverduevoid

/estimates

Estimates

Quotes you send before the work starts.

Line itemsAuto-numberedPDF

Filters
statusclientId
Status
draftsentaccepteddeclinedexpiredconverted

/payments

Payments

Incoming payments, optionally linked to an invoice and client. The amount must be greater than zero.

Filters
invoiceIdclientId

/expenses

Expenses

Business expenses with category, vendor and payment method.

Filters
statuscategory

/suppliers

Suppliers

The vendors you buy from, with IBAN, VAT number and payment terms.

Filters
isActive

/items

Items

Your product and service catalogue, with prices, tax rates and stock. stockQuantity is read-only.

Filters
categoryisActive

/vendor-bills

Vendor bills

Supplier bills and credit notes, set with docType bill or credit_note, incl. expense accounts and cost centers.

Line itemsAuto-numbered

Filters
statussupplierId
Status
draftawaiting_approvalapprovedpartially_paidpaidoverduevoid

The OpenAPI spec lists every writable field per resource.

Line items & totals

Invoices, estimates and vendor bills accept an items array. Send the positions and let v-ledge do the arithmetic.

descriptionstring
The text shown on the document.
quantitynumber
Greater than 0. Defaults to 1.
unitPricenumber
0 or greater. On a percentage discount line, the percentage (max. 100).
kind"service" | "discount"
Defaults to service. Discount lines reduce the subtotal.
discountMode"flat" | "percent"
How a discount line’s unitPrice is applied.
taxRatenumber
0–100. Stored per line; totals use the document’s taxRate.

Invoice items also take regionCode, estimate items serviceItemId, and vendor-bill items expenseAccount, costCenterId and serviceItemId.

How totals are calculated

  1. subtotal is the sum of quantity × unitPrice over all service lines, minus discountAmount.
  2. taxAmount is the subtotal × the document’s taxRate.
  3. 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.

A 10 % discount line
{ "description": "Loyalty discount", "kind": "discount", "discountMode": "percent", "unitPrice": 10 }

Numbers & PDFs

Documents get readable, sequential numbers and render to PDF in your workspace’s design.

Invoices
INV-001CN-001Invoice / credit note
Estimates
EST-001Estimate
Vendor bills
BILL-001BCN-001Bill / credit note

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.

Download a PDF

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

Webhooks

Webhooks push events to your server the moment they happen, so you don’t have to poll.

Set up an endpoint

  1. Open Settings → Company → Developer and choose New endpoint under Webhooks.
  2. Enter an HTTPS URL on port 443 that resolves to a public address.
  3. Pick the events you want — or none, to receive every event.
  4. Copy the signing secret (whsec_…). Like an API key, it’s shown only once.

Events

order.created
A sales order was created.Live
delivery_note.created
A delivery note was issued for a sales order.Live
invoice.created
An invoice was generated from a sales order.Live

Reserved 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

Payload

Each delivery is a POST with a JSON body and these headers:

Content-Type
application/json
X-Vledge-Event
The event name, e.g. invoice.created
X-Vledge-Signature
t=<unix time>,v1=<HMAC-SHA256 hex>
invoice.created
{
  "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"
  }
}

Verify signatures

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
});

Delivery

One attempt
Each event is sent once with a 10-second timeout. There are no automatic retries.
Success
Any 2xx response. Acknowledge right away and do the work in the background.
Monitoring
The last HTTP status appears next to each endpoint in the Developer tab.
Missed events
After downtime, reconcile through the API, e.g. by listing recent orders.
Duplicates
Deduplicate on the event id before acting on it.

OpenAPI & tooling

The whole API is described in an OpenAPI 3.1 document, generated from the same catalogue that powers the endpoints.

openapi.jsonvestryx.com/api/v1/openapi.json

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.

Terminal
# 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-client

Going live

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

Questions about your integration?

Tell us what you’re building — we’re happy to help you plan it.