The Sunventory API.

This is a courtesy translation. Only the German version is legally binding. Read the German version

Your data belongs to you, and your systems can collect it: the Sunventory API serves sites, bills of materials and deliveries as a REST interface. Your ERP can additionally write the bill of materials of a site and get notified by webhook as soon as deliveries arrive. Capture happens on site with the app, analysis happens wherever you want it.

The pattern is deliberately the one integrators already know: REST with an OpenAPI specification, API keys with scopes, an idempotent BOM upsert via external_ref, and signed webhooks with at-least-once delivery and event deduplication, the way Stripe and GitHub do it.

https://app.sunventory.de/api/v1

Authentication

Every request carries an API key of your account as a bearer token. Keys are created and revoked by site management in the web platform under Settings, API. A key applies to the whole account, is shown exactly once when it is created, and stops working the moment it is revoked.

When creating a key you choose its access: read only, or read and write. The write endpoints, meaning BOM import and webhook management, require a key with write access. A read-only key receives 403 insufficient_scope there. Existing keys stay read-only, unchanged.

curl https://app.sunventory.de/api/v1/sites \
  -H "Authorization: Bearer svk_your_key"

Endpoints

Reading is open to every key, writing only to keys with write access. Lists are paginated (limit up to 500, offset); deliveries can additionally be filtered by site_id, supplier, from, to (each YYYY-MM-DD) and status (pending or confirmed).

  • GET /sites All sites of the account.
  • GET /sites/{id} One site.
  • GET /sites/{id}/bom The bill of materials with planned, confirmed and open quantity per line item.
  • PUT /sites/{id}/bom Import the bill of materials: create or update positions, idempotent via external_ref. Needs write access.
  • GET /deliveries Deliveries with line items and delivery note photos, filterable by site, supplier, period and status.
  • GET /deliveries/{id} One delivery.
  • GET /deliveries.csv The same deliveries as CSV, one row per line item.
  • GET/POST/DELETE /webhooks Manage webhooks. Needs write access.
  • POST /webhooks/{id}/test Send a signed test event to the endpoint.

The full description of every field comes from the machine itself: openapi.json in OpenAPI 3.1 format, available without a key.

Examples

Delivery progress of a site

curl "https://app.sunventory.de/api/v1/sites/{id}/bom" \
  -H "Authorization: Bearer svk_…"
{
  "data": [{
    "supplier": "Canadian Solar",
    "product": "HiKu7 CS7L-590MS 590W bifacial",
    "planned_quantity": 169500,
    "delivered_quantity": 8297,
    "pending_quantity": 1188,
    "last_delivery_at": "2026-05-27",
    …
  }]
}

Confirmed deliveries of one month as CSV

curl "https://app.sunventory.de/api/v1/deliveries.csv?status=confirmed&from=2026-07-01&to=2026-07-31" \
  -H "Authorization: Bearer svk_…" -o deliveries.csv

Every delivery carries its delivery note photos as signed, time-limited URLs. Whoever needs the document for posting downloads it straight from the response.

Importing the BOM

PUT /sites/{id}/bom takes order lines from your ERP into the site's bill of materials, up to 1000 positions per request, all in one transaction. The anchor is external_ref, for example purchase order and line from Dynamics 365: positions with a known reference are updated, new ones are created. Give every position a stable external_ref and the same import can run any number of times without ever producing duplicates; only positions without a reference are created anew on every run. An update takes over the position as a whole, so empty optional fields overwrite what is stored.

curl -X PUT "https://app.sunventory.de/api/v1/sites/{id}/bom" \
  -H "Authorization: Bearer svk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "positions": [
      { "external_ref": "PO-4711/10", "supplier": "Canadian Solar",
        "product": "PV module CS7N 700 MB-T", "category": "PV modules",
        "planned_quantity": 196000, "unit": "Stück" },
      { "external_ref": "PO-4711/20", "supplier": "LAPP",
        "product": "Solar cable H1Z2Z2-K 6 mm²",
        "planned_quantity": 84000, "unit": "m" }
    ]
  }'
{ "created": 2, "updated": 0, "data": [ … ] }

The response lists the touched positions with the same fields as GET /sites/{id}/bom, including quantities already delivered. On the second identical run it reports "created": 0, "updated": 2. Two rules make the import safe: positions with linked deliveries keep their id, so the delivery proof stays intact. And the API never deletes. Positions missing from the import are left untouched; removing is a deliberate act in the app.

Webhooks

Instead of polling, get notified: a webhook calls your system as soon as a delivery is captured on site (delivery.created) or confirmed by site management (delivery.confirmed). Register with a write key:

curl -X POST https://app.sunventory.de/api/v1/webhooks \
  -H "Authorization: Bearer svk_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://erp.example.com/hooks/sunventory",
       "events": ["delivery.created", "delivery.confirmed"] }'

The response contains the signing secret (whsec_…) exactly once. Every event arrives as a POST with this envelope; data is the delivery with the same fields as GET /deliveries/{id}, including freshly signed photo URLs:

{
  "event_id": "8f14e45f-…",
  "event": "delivery.created",
  "timestamp": "2026-08-26T09:41:00.000Z",
  "webhook_id": "6512bd43-…",
  "data": { … }
}

Delivery is at-least-once, the way Stripe and GitHub do it: the same event can arrive more than once, but then carries the same event_id. Your consumer therefore deduplicates on the event_id and answers with a 2xx within 10 seconds. On failure, Sunventory retries three more times, after 1, 10 and 60 minutes. After 20 consecutive failures the webhook is deactivated; you can see its state under Settings, API and via GET /webhooks.

Every delivery is signed. The header X-Sunventory-Signature: t=<unix>,v1=<hmac> carries an HMAC-SHA256 over t + "." + raw body with your secret. Verify the signature and reject timestamps older than about 5 minutes, which protects against replays:

# Python
import hmac, hashlib, time

def verify(header, secret, raw_body):
    t, v1 = (part.split("=", 1)[1] for part in header.split(","))
    mac = hmac.new(secret.encode(), (t + ".").encode() + raw_body, hashlib.sha256)
    fresh = abs(time.time() - int(t)) < 300
    return fresh and hmac.compare_digest(mac.hexdigest(), v1)

To set things up, POST /webhooks/{id}/test sends a signed test event and reports straight back whether your endpoint accepted it.

Business Central, SAP and others

The API is deliberately plain: HTTPS, JSON, CSV. That means it talks to any system that can record goods receipts, without a dedicated connector.

  • Microsoft Dynamics 365 Business Central reads the endpoints via Power Automate or directly from AL through the HttpClient.
  • SAP connects the interface through the Integration Suite (CPI) or any other HTTP-capable middleware.
  • For everything else, the CSV export is the shortest route: one row per line item, UTF-8, importable as is.

Planning an integration and would rather not build it yourself? Write to us, and we set it up together as part of a pilot project.

Reliability

  • Version 1 is a contract: fields are added, but never renamed and never removed.
  • Only keys with write access can write, and only the bill of materials. Capture and confirmation stay in the app and the web platform, nothing is ever deleted via the API, and the proof stays unchanged.
  • Each key is allowed 120 requests per minute, enough for syncing every minute. Responses carry the usual RateLimit headers.
  • Operated in a data centre in Germany, like the whole platform. Details in the privacy policy.