API & integrations

The Blina Desk API

Blina Desk doesn't have to be your only software. Through the REST API your other systems read and write the same data you see in the interface, under the same rules.

What you can do with it

Documents & search

List, download and upload documents, walk through folders, search full text. What you upload goes through the same path as any upload: virus scan, OCR, indexing.

Customers & contacts

Read, create and update companies and people, same required fields and same history as the CRM.

Orders & invoices

Create them, add line items, set status. The document number comes from the server and the totals from the database: no client can overwrite them.

Read incoming invoices

You can read supplier invoices and their lines. They are created by EN 16931 validation with duplicate detection, so every invoice takes the same path as it does inside the software.

BASE="https://api.blina-desk.com/api/v1"

curl -s "$BASE/documents?limit=5" -H "Authorization: Bearer $BDK"
curl -s "$BASE/accounts?q=gmbh"   -H "Authorization: Bearer $BDK"
curl -s "$BASE/search?q=vertrag"  -H "Authorization: Bearer $BDK"

See it instead of reading it

Recorded in the running product, with real data — not an animation.

Keys and permissionsThe company creates its own keys, in clear text exactly once, with permissions picked one by one.
Webhooks, archive, usageSigned events with the timestamp inside the signature, and a queue that tries again.

How to start

  1. In the admin area, under "API and integrations", create a key and give it the permissions it should have. You see the key in clear text exactly once, we store only its hash.
  2. Send the key as a bearer token. The first call that answers tells you which company it belongs to.
  3. Read, write, and for everything else subscribe to webhooks instead of polling.
# header, the server assigns the number
curl -s -X POST "$BASE/invoices" \
  -H "Authorization: Bearer $BDK" -H "Content-Type: application/json" \
  -d '{"account_id":"...","issue_date":"2026-07-29"}'
# {"id":"...","invoice_no":"INV-000042","status":"draft","total":0.0}

# line items, the database computes the totals
curl -s -X POST "$BASE/invoices/$ID/lines" \
  -H "Authorization: Bearer $BDK" -H "Content-Type: application/json" \
  -d '{"name":"Beratung","qty":3,"unit_price":100,"vat_rate":19}'
# {"id":"...","total":357.0}   <- 3 x 100 + 19%

Webhooks: you find out without asking

Instead of checking whether something changed, be told. Every delivery is signed, verify the signature before you use the content.

  • The address must be public https: private, loopback and link-local addresses are refused, at registration and again at every delivery.
  • Answer 2xx as soon as you've stored the event. Anything else counts as a failure and is retried: 0s → 30s → 5m → 30m → 2h.
  • After five exhausted deliveries in a row the endpoint is suspended, and you reactivate it.
  • A delivery can arrive more than once: use the event id to decide whether you've seen it already.

Verify the signature, not optional

Compute it over the raw body, before any JSON parsing: re-serialising changes the bytes and the comparison fails. Reject what doesn't verify, and what is older than five minutes, even with a correct MAC.

import hashlib, hmac, time

def verify(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = int(parts["t"]), parts["v1"]
    if abs(time.time() - ts) > tolerance:      # anti-replay
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)  # confronto a tempo costante

Quotas and safety

  • 120 calls per minute and 20,000 per day per key. Every successful response tells you where you stand; beyond that you get 429 with Retry-After.
  • A key carries only the permissions you give it, documents, search, CRM, sales, purchasing, webhooks, granted one by one.
  • Every response carries an X-Request-Id: quote it and we can reconstruct exactly what happened.
X-RateLimit-Limit-Minute: 120
X-RateLimit-Remaining-Minute: 118
X-RateLimit-Limit-Day: 20000
X-RateLimit-Remaining-Day: 19863