Developer API · Beta

Do everything the app does — under the same rules — from your own software.

The Kumpara Developer API connects online stores, ERPs, accounting software and marketplace integrators to Kumpara. The /v1 surface calls the same core as the app itself: the same guards, the same ledger entries, the same tax and withholding calculation. The API loosens nothing.

  • Ledger-safe: every money movement in one transaction, every correction a reversing entry
  • e-Documents included: issuing, cancelling, the inbox, taxpayer lookup and credits in the same API
  • Built for Turkey: line-level withholding, VAT exemption codes, multi-currency and rate snapshots

The Developer API is currently in closed beta; general access is not open yet and no date is promised. Beta participants receive their own base address and keys from us.

74
live endpoints From contacts to e-documents, all scope-protected.
28
separate scopes You grant a key only what it needs.
3
append-only ledgers Contacts, cash and stock: no UPDATE/DELETE.
1
key = 1 company Cross-company leakage is architecturally impossible.

What can you build?

Everything below is built with endpoints that work today — not roadmap items.

Run the order chain end to end

Create the order, ship the delivery note, issue and approve the invoice, and settle the payment against it in the same call. Partial shipping and partial invoicing are supported; the remaining quantity is derived from non-cancelled invoices.

Accounting and ERP sync

The GET /v1/changes feed gives you the type and id of what changed; you read the detail from its own endpoint. The feed does not distinguish app, mobile or API — approving an invoice in the interface lands there too.

Set stock levels absolutely

POST /v1/stock-levels/actions/set writes a stocktake result directly; the difference becomes an adjustment movement. You follow movements from the read-only ledger and match variants by barcode.

Issue e-invoices and e-archive

One call turns an approved invoice into an e-document; the scenario is chosen on the server from the taxpayer's status. You sync the inbox, download XML/PDF and answer commercial invoices. The integrator's name never appears in a response.

Read the customer position from one source

A single balance is not enough: ledger entries (why the balance is what it is), open items (which invoices are open) and aging (how overdue) are separate endpoints. You settle payments against open invoices and read the realised FX difference on cross-currency settlements.

Migrate opening balances

Opening balances for contacts and opening stock for products have their own endpoints, each with a reversing counterpart. Since the ledgers are append-only, reversal is the only way to fix a wrong opening.

Your first invoice in five steps

Every write carries three headers: Authorization, Idempotency-Key, Content-Type.

  1. 1

    Create a key

    In the app: Settings → API Keys. Pick a name, a company and the scopes. The plain key is shown only once.

  2. 2

    Verify who you are

    GET /v1/me returns the business, company, environment and scopes the key is bound to.

  3. 3

    Upsert the contact

    Match by tax number or contact code: existing records are updated, missing ones created.

  4. 4

    Issue and approve the invoice

    With approve: true, creation and approval happen in one transaction: number, customer debt and stock issue appear together.

  5. 5

    Settle the payment

    You pass the invoice while creating the payment: the cash ledger, the customer ledger and the allocation are written in one transaction.

Quick start

1. Verify your key

Every request carries Authorization: Bearer. Your first call tells you which business and which company the key is bound to, and what it may do:

curl -s "https://api.kumpara.net/v1/me" \
  -H "Authorization: Bearer kp_live_a1b2c3d4_…"
{
  "data": {
    "tenant":    { "id": "…", "name": "Örnek Ticaret A.Ş.", "plan": "Pro" },
    "company":   { "id": "…", "legalName": "Örnek Ticaret A.Ş.", "baseCurrency": "TRY" },
    "apiClient": { "prefix": "kp_live_a1b2c3d4", "environment": "live",
                   "scopes": ["me:read", "contacts:write", "sales:write", "sales:approve"] }
  },
  "meta": { "requestId": "0HN…" }
}

GET /v1/capabilities then tells you whether e-documents are connected for this company, how many credits are left and what the base currency is — you shape your flow around that.

2. Match the contact by your own key

You do not have to search for a customer every time: upsert by tax number or by contact code. An existing record is updated, a missing one is created.

curl -s -X POST "https://api.kumpara.net/v1/contacts/actions/upsert?matchBy=taxNumber" \
  -H "Authorization: Bearer kp_live_…" \
  -H "Idempotency-Key: contact-4711-2026-09-23" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Örnek Gıda Ltd. Şti.",
        "type": "customer",
        "taxNumber": "1234567890",
        "preferredCurrency": "TRY",
        "paymentTermDays": 30
      }'

The response is { "data": { "id": "…", "name": "…", "created": true } }. The created flag tells you whether a record was opened or an existing one updated. matchBy accepts only taxNumber or code.

3. Issue and approve the invoice

An invoice line has no amount field. You send quantity, unit price, discount and VAT rate; the line total, the withheld VAT, the base amount and the document number are produced on the server.

curl -s -X POST "https://api.kumpara.net/v1/sales-invoices" \
  -H "Authorization: Bearer kp_live_…" \
  -H "Idempotency-Key: order-88213-invoice" \
  -H "Content-Type: application/json" \
  -d '{
        "contactId": "6f1c…",
        "currency": "TRY",
        "approve": true,
        "lines": [
          { "productId": "9a3e…", "quantity": 10, "unitPrice": 250, "vatRate": 20 },
          { "description": "Shipping", "quantity": 1, "unitPrice": 150, "vatRate": 20 }
        ]
      }'
{
  "data": { "id": "c41b…", "number": "SF-2026-000412", "status": "approved", "approved": true },
  "meta": { "requestId": "0HN…" }
}

approve: true does creation and approval in one transaction: the document number, the customer's debt and the stock issue appear together. That is a money mutation, so the key needs sales:approve alongside sales:write — otherwise you get 403 insufficient_scope.

4. Record the payment and close the invoice

curl -s -X POST "https://api.kumpara.net/v1/payments" \
  -H "Authorization: Bearer kp_live_…" \
  -H "Idempotency-Key: payment-88213" \
  -H "Content-Type: application/json" \
  -d '{
        "contactId": "6f1c…",
        "cashAccountId": "2b77…",
        "direction": "incoming",
        "amount": 3060,
        "invoiceType": "sales_invoice",
        "invoiceId": "c41b…"
      }'

The cash ledger, the customer ledger and the allocation are written in the same transaction. Use allocations[] to split across several invoices, or POST /v1/payments/{id}/actions/allocate to settle later.

5. Pull the changes

To keep the other system current you poll the feed. It gives you the type and id of what changed; you read the detail from its own endpoint.

curl -s "https://api.kumpara.net/v1/changes?since=2026-09-23T06:00:00Z&types=sales_invoice,payment" \
  -H "Authorization: Bearer kp_live_…"
{
  "data": [
    { "type": "sales_invoice", "id": "c41b…", "action": "created", "occurredAt": "2026-09-23T07:12:44Z" },
    { "type": "payment",       "id": "77af…", "action": "created", "occurredAt": "2026-09-23T07:13:02Z" }
  ],
  "meta": { "checkpoint": "2026-09-23T07:13:02Z", "nextCursor": null, "hasMore": false }
}

On the next call you pass meta.checkpoint as since — the bound is exclusive, so the same record does not come twice. We recommend a five-minute overlap against the gap between commit order and timestamp order; reads are idempotent, so a repeated record is harmless.

The core contract

Identity and permissions

The key is sent as Authorization: Bearer kp_live_… (or X-Api-Key). The plain value is never stored — Kumpara keeps only the SHA-256 hash and the display prefix. Every key is locked to one company; another company's data cannot come back, architecturally. A key's scopes can never exceed the role of the user who created it.

Scopes are {resource}:{action} and there are twenty-eight of them:

Action Meaning Example
read List / view contacts:read, stock:read
write Create / edit products:write, sales:write
approve Approve / cancel / reverse sales:approve, purchase:approve
issue, cancel, incoming Issue, cancel, inbox for e-documents edocuments:issue

An endpoint you are not allowed to call returns 403 insufficient_scope and names the missing scope.

Idempotency

Every write requires an Idempotency-Key. The key is stored inside the same transaction that writes the record — there is no dual write. A second call with the same key does not repeat the work. The response takes one of two shapes: 409 idempotency_replayed (you read the created record with GET), or 200 carrying the body of the first response plus a replayed: true flag. Both mean "already applied"; your client should treat either one as success. The key's scope is the quadruple (business, API key, endpoint, key), so two integrators cannot collide on the same value.

Money, exchange rates and numeric types

Amounts are stored as numeric(19,4) and returned as decimal strings in JSON, so no floating-point rounding happens. An amount is never separated from its currency.

Every financial line carries its own rate snapshot: originalAmount, originalCurrency, exchangeRate, baseAmount, baseCurrency. Even if the rate list is updated later, a past document does not move. You may supply the rate yourself in exchangeRate; if you omit it, 1 is used — fill it in when you issue a cross-currency document.

Pagination and filters

Lists are paginated by cursor; there is no offset. The response carries meta.nextCursor and meta.hasMore. Page size is given by pageSize: default 50, maximum 200. For deltas use the updatedAfter query parameter or the /v1/changes feed.

Errors

Errors use RFC 9457 application/problem+json and carry a stable code. The dictionary is frozen: a documented code does not change.

{
  "type": "https://docs.kumpara.net/errors/insufficient_scope",
  "title": "Yetki yok",
  "status": 403,
  "detail": "Bu uç `sales:approve` yetkisi ister; anahtarınızda yok.",
  "code": "insufficient_scope",
  "requestId": "0HN…"
}
Status Meaning Common codes
400 The request could not be read invalid_request, invalid_value, missing_parameter
401 Key invalid or revoked unauthorized
403 The key lacks the required scope insufficient_scope
404 No such record, or not in this company not_found
409 Conflict idempotency_replayed, duplicate_code, duplicate_tax_number
422 Business rule violated validation_failed, currency_not_enabled, warehouse_required
429 Rate limit exceeded rate_limit_exceeded

Field-level failures add an errors[] array; each entry carries pointer, code and detail.

Rate limits

Reads and writes use separate buckets — pulling a report does not block invoicing. The defaults are 600 reads and 120 writes per minute per key; every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Over the limit you get 429 plus Retry-After.

Request id

Every response carries X-Request-Id, and the same value appears as requestId in an error body. Share it in a support request. To supply your own, send an X-Request-Id header (up to 64 characters) and the response echoes it.

Discovery

The whole contract is published as OpenAPI 3: https://api.kumpara.net/v1/openapi.json for machines, https://api.kumpara.net/v1/docs to browse. The spec needs no authentication — it is the contract, not data.

Resources

The table below is the whole surface that is live today. Full parameters, bodies and response schemas live in OpenAPI: https://api.kumpara.net/v1/docs.

Area Main endpoints Scopes
Context GET /v1/me, GET /v1/capabilities me:read
Contact card GET · POST /v1/contacts, PATCH /v1/contacts/{id}, POST /v1/contacts/actions/upsert contacts:read · contacts:write
Contact position GET /v1/contacts/{id}/balance, …/ledger-entries, …/open-items, GET /v1/aging contacts:read
Opening balance POST /v1/contacts/{id}/actions/set-opening-balance, …/actions/reverse-opening-balance contacts:write
Products and variants GET · POST /v1/products, PATCH /v1/products/{id}, POST /v1/products/actions/upsert, POST /v1/products/{id}/variants, PATCH /v1/product-variants/{id} products:read · products:write
Stock GET /v1/stock-levels, POST /v1/stock-levels/actions/set, GET /v1/stock-movements, GET /v1/products/{id}/stock stock:read · stock:write
Opening stock POST /v1/products/{id}/actions/set-opening-stock, …/actions/reverse-opening-stock stock:write
Orders GET · POST /v1/orders, PUT /v1/orders/{id}, POST /v1/orders/{id}/actions/cancel sales:read · sales:write
Delivery notes GET · POST /v1/delivery-notes, PATCH /v1/delivery-notes/{id}, …/actions/ship, …/actions/cancel sales:read · sales:write
Sales invoices GET · POST /v1/sales-invoices, …/{id}/actions/approve, …/{id}/cancel-eligibility, …/{id}/actions/cancel, …/{id}/ledger-entries sales:read · sales:write · sales:approve
Purchase invoices GET · POST /v1/purchase-invoices, …/{id}/actions/approve purchase:read · purchase:write · purchase:approve
Payments GET · POST /v1/payments, POST /v1/payments/{id}/actions/allocate payments:read · payments:write
Cash and bank GET /v1/cash-accounts, GET /v1/cash-accounts/{id}/ledger-entries cash:read
Cheques and notes GET /v1/cheques cheques:read
e-Documents (out) POST /v1/sales-invoices/{id}/actions/issue-e-document, GET /v1/e-documents, …/{id}/xml, …/{id}/pdf, …/{id}/actions/cancel edocuments:read · edocuments:issue · edocuments:cancel
e-Documents (in) GET /v1/incoming-e-documents, POST …/actions/sync, …/{id}/actions/answer, …/{id}/actions/mark-read, …/{id}/actions/ignore edocuments:incoming
Taxpayer and credits POST /v1/taxpayers/actions/lookup, GET /v1/e-credits edocuments:read
Reference catalogue GET /v1/warehouses, GET /v1/categories, GET /v1/reference/{type} catalog:read
Change feed GET /v1/changes changes:read

How the document chain links up. A delivery note body takes orderId; an invoice body takes orderId and deliveryNoteIds[], and lines match through orderLineId and deliveryNoteLineId. The remaining quantity is not a stored counter — it is derived from non-cancelled invoices, so a cancelled invoice frees the order up to be invoiced again.

Stock leaves in one place only. Shipping and invoice approval never deduct the same goods twice: total sales-side issue is max(Σ shipped, Σ approved invoices) and each document writes only its own delta. If the invoice was approved first, shipping may produce no movement at all — that is correct behaviour.

Not yet — on the roadmap

We list what is missing too, so you do not build on the wrong assumption.

Webhooks Coming soon

Today the working path is polling GET /v1/changes; the webhooks:manage scope exists in the catalogue but the endpoint is not live.

An isolated sandbox Coming soon

The kp_test_ prefix is only a label today and writes to your real data. Open a separate company to experiment.

ETag / If-Match Coming soon

Concurrency control is not half-added without the version column it depends on: the impression of protection is worse than none.

OAuth 2.1 and an app catalogue Coming soon

During the beta your customer creates a narrow key for you from their own panel; a consent-screen flow is on the roadmap.

Converting an incoming e-document to a purchase invoice Coming soon

Reading, answering and ignoring the inbox works; line-to-product matching is still done in the app.

Report and export endpoints Coming soon

Aging (GET /v1/aging) works; P&L, VAT and asynchronous file exports do not exist yet.

Security, isolation and data protection

Isolation lives in the database. What separates one business from another is not a filter in the application but PostgreSQL's own row-level security. An API request connects with a least-privilege role and the context is written to the database as the connection opens, so even a badly written query cannot return another business's row. Company separation sits on top of that: a key is locked to one company.

The key is not stored. Only the SHA-256 hash and the display prefix (kp_live_a1b2c3d4) are kept. The plain value appears once, in the creation response; if you lose it, it cannot be recovered — you issue a new one. Keys are never written to logs; only the prefix is. Revocation takes effect immediately.

There is no privilege escalation. A key's scopes can never exceed the role of the user who created it. Creating an invoice with approve: true needs sales:approve as well as sales:write; if it is missing the request is refused, not quietly saved as a draft.

Everything leaves a trail. Every write through the API is recorded in the audit log with the key prefix as the actor (api:kp_live_a1b2c3d4). Who did what and when can always be asked afterwards; the app, mobile and the API all land in the same trail.

Personal data is scoped narrowly. Fields such as national ID numbers, IBANs, names and addresses come back masked to keys without the pii:read scope — the last four characters stay visible and the endpoint still works. If your integration does not need personal data, never grant it; the best protection is data that never leaves.

How to store the key. Keep it server-side, in an environment variable or a secret manager; never embed it in source code, a browser or a mobile app. Issue a separate key per integration so revoking one does not affect the others. When you open a support request, share the requestId and the Idempotency-Keynever your key.

Questions developers ask

Which plan includes the API?

Access is not gated by plan. Plans differ in rate limits and quotas, which are adjustable. During the beta, access is granted on application.

Can I calculate the amounts myself?

No — the server is the single authority. You send quantity, unit price, discount rate, VAT rate, withholding code and currency. Line totals, withheld VAT, base amount and the document number are produced on the server; an invoice line has no amount field.

Will a repeated request create a duplicate?

No. Every write requires an Idempotency-Key; the key is stored inside the same transaction that writes the record. A second call with the same key does not repeat the work: you either get 409 idempotency_replayed and read the created record with GET, or 200 with the body of the first response and a replayed: true flag. Treat both as success.

Can I edit an approved invoice?

No, an approved document is immutable. The correction path is cancel + new document; cancelling writes reversing entries. The customer, cash and stock ledgers have no UPDATE/DELETE. Before cancelling, GET /v1/sales-invoices/{id}/cancel-eligibility tells you what blocks it.

Can I also catch changes made in the app?

Yes. The GET /v1/changes feed does not care where a record changed: the app, the mobile app and the API all land in the same feed. The feed gives only the record's type and id; you read the detail from its own endpoint.

Is there a separate sandbox environment?

Not yet — and we say so plainly. A kp_test_ key is only a label today and writes to your real data. To experiment, open a separate company (or a separate business account); an isolated sandbox is on the roadmap.

Are there webhooks?

Not yet. The working path today is polling GET /v1/changes: pass the meta.checkpoint you last received as since on the next call. Webhooks are on the roadmap.

What about multi-company businesses?

Each key is locked to one company and sees only that company's data. For a multi-company integration you create one key per company. Switching company with a single key is on the roadmap.

Does creating an invoice spend credits?

No. Credits are only used by e-document traffic: each outgoing and each incoming e-document costs one credit. Invoices, orders or delivery notes you create in the system cost nothing. You read the balance with GET /v1/e-credits.

What if I lose my key?

It cannot be recovered. Kumpara stores only the SHA-256 hash and the display prefix; the plain value is shown once, in the creation response. If you lose it, revoke the old key and create a new one — revocation takes effect immediately.

Join the beta programme

We accept online-store platforms, ERP and accounting software, accountancy practices and marketplace integrators. Participants get access, advance notice of contract changes and a direct feedback channel. Tell us your integration type and which flow you plan to build.