Propel API — Guide

The Propel API gives read access to your projects, their budgets and the records ledger (the money that moves on a project). It is the same data the Propel app shows, with the same access rules.


1. Quickstart

export PROPEL_KEY="pk_test_…"          # an API key (see §2)

# projects you can access
curl -s https://api.propel-industrial.com/v0/projects \
  -H "Authorization: Bearer $PROPEL_KEY"

# a project's budget: structure + approved / invoiced / paid per node
curl -s https://api.propel-industrial.com/v0/projects/{projectId}/budget \
  -H "Authorization: Bearer $PROPEL_KEY"

# fully approved records (payment pending or paid) of one project, 200 per page
curl -s "https://api.propel-industrial.com/v0/records?projectId={projectId}&status=paymentPending,paid&limit=200" \
  -H "Authorization: Bearer $PROPEL_KEY"

2. Authentication

Every /v0 request needs Authorization: Bearer <token>. Two kinds of token:

Token Who How to get it
Session token a signed-in Propel user (e.g. the Propel app itself) the app's sign-in
API key pk_<env>_<prefix>_<secret> a script, integration or agent your Propel administrator creates it — shown once

API keys act on behalf of a user (their owner). On every request the key gets the owner's current access — if the owner loses access to a project, so does the key, immediately. On top of that, a key has its own ceiling:

Keys are bound to one environment (test or live) and one tenant; a test key never works on production. Keys can be revoked instantly; only a hash is stored. Treat keys like passwords — never put them in code, emails or tickets.


3. Access: projects and field groups

You only ever see projects you are entitled to. A project or record you can't access returns 404, exactly like one that doesn't exist — the API never confirms that something exists.

Within a project, field groups decide which fields you see. Every field in the reference is labelled with its group (x-field-group). Fields you may not see are omitted, never nulled.

Group Contains (examples) admin user guest
core ids, names, status, dates of record, counterparty name ✔ ✔ ✔
amounts net / tax / gross, budget totals ✔ ✔ –
budget budget node, structure, codes ✔ ✔ –
payment due date, payment date ✔ ✔ –
documents document names, the project's intake address ✔ ✔ –
tax contractor tax id, registration number ✔ ✔ –
bank (not exposed on the API)

Totals follow the same rule: if you can't see amounts, budget totals are not returned either, and filtering on a field group you can't see is rejected (400) — so nothing can be inferred.


4. Conventions

Amounts — always an Amount object, never a bare number, never a decimal:

{ "value": 123456, "currency": "EUR" }     // = 1,234.56 EUR

value is an integer in the currency's smallest unit (cents for EUR). Amounts in different currencies are never added together.

Dates — calendar dates are YYYY-MM-DD in Europe/Vienna (e.g. recognitionDate, cashDate). Instants, where they appear, are ISO-8601 in UTC (2026-09-30T14:03:00Z).

IDs — opaque strings; don't parse them. Budget nodes also carry a readable code ("07-0007") which a company can change — store the nodeId, display the code.

Allocations — allocations says where a record's amount is booked: today one entry (the whole net amount on one budget line). An invoice split across budget lines will carry several entries that add up to net; budgetNode is a shortcut for the single-line case and is null when a record is split.

Records — the ledger. Each record has systemKind (cost · income · financing · tax), direction (in / out), basis (actual / forecast) and source (invoice, …). v0 returns cost / actual records from invoices. status is the lifecycle (setup → approvalPending → paymentPending → paid, or rejected); workflowStep is the project's own step name (e.g. "Developer Approval").

Budget structure — three levels: budget → sub-budget → cost item. Each project uses a structure (a company template) and may add project-only nodes or switch template nodes off. /v0/projects/{id}/budget returns the project's active nodes, parents before children, with budget (approved), invoiced (all non-rejected invoices, net, setup included) and paid. A node's amounts include its own records and all its descendants — total the top level only. Every parentId points at a node in the same response. ?level=budget returns just the top lines, ?level=subBudget two levels. Invoiced amounts without a budget node are reported as unallocated, approved budget without one as unallocatedBudget. All totals are in the project's currency; amounts in any other currency are never added in — they are listed per currency under excluded. Budget figures are net; gross is on the record.


4a. Sending invoices

Every project has an intake address (inboundEmail on the project, needs the documents field group). Email invoices there as PDF attachments — one email may carry several PDFs:

  1. Each PDF becomes one record with status: setup and intake.channel: email.
  2. The document is read automatically: intake.status goes queued → processing → completed (usually within a minute) or failed. Until it is matched, counterparty.name may be null.
  3. The record then follows the project's approval workflow like any other invoice.

To follow up on what you sent:

curl -s "https://api.propel-industrial.com/v0/records?projectId={projectId}&channel=email&status=setup&limit=200" \
  -H "Authorization: Bearer $PROPEL_KEY"

Emails without a PDF are ignored. intake.receivedAt is when the record was created.


5. Pagination

Lists return:

{ "data": [ … ], "page": { "nextCursor": "eyJ2Ijox…", "limit": 50 } }

To read a whole list: use limit=200 and follow nextCursor until it is null. limit is 1–200 (default 50). A page may hold fewer items than limit even when more follow — always rely on nextCursor. Records are ordered by id, not by date — filter by date instead of assuming an order. GET /v0/projects always returns all your projects in one page. Cursors are opaque, bound to your credentials and to the same filters — reuse them only with the same query. Pagination is not a frozen snapshot: records added while you page may or may not appear.

5a. Filtering records

Parameter Meaning
projectId one project (404 if you can't access it, like the project itself). Without it, a call spans all your projects — up to 30; above that, pass projectId and query project by project.
status one or more of setup, approvalPending, paymentPending, paid, rejected, comma-separated. The fully approved slice is status=paymentPending,paid.
invoiceNumber exact invoice number
contractorId one counterparty
recognitionDateFrom, recognitionDateTo invoice date range, inclusive, YYYY-MM-DD — e.g. a month or a quarter
budgetNodeId records on exactly that node; add includeDescendants=true for the node and everything below it
channel how the record came in: email, app, chat, api, manual

Unknown or empty parameters are rejected with 400 invalid-query — a typo never silently returns unfiltered data. The API is read-only: any method other than GET returns 405.


6. Errors

Errors are an Error object, sent as application/problem+json (RFC 9457):

{ "type": "/problems/not-found",
  "title": "Not found", "status": 404, "requestId": "8f2c…" }
Status type (suffix) Meaning
400 invalid-query, invalid-body, invalid-cursor, filter-not-allowed, too-many-projects fix the request (unknown/empty parameters included)
405 method-not-allowed data endpoints are read-only; POST /v0/feedback is the one write
401 authentication-required, invalid-credentials missing, wrong, revoked or expired token
404 not-found doesn't exist or you can't access it
429 too-many-requests slow down: at most 300 requests per minute per key or user; wait Retry-After seconds
500 internal-error our fault — quote the requestId

type is a stable, relative URI — branch your code on it; GET https://api.propel-industrial.com/problems/<type> explains each one. Every response carries an x-request-id header; quote it when reporting a problem.


7. Endpoints (v0, read-only)

Method Path Returns
GET /v0/projects projects you can access
GET /v0/projects/{projectId} one project
GET /v0/projects/{projectId}/budget budget structure with approved / invoiced / paid per node
GET /v0/records records (filters: projectId, status, budgetNodeId; paginated)
GET /v0/records/{recordId} one record
GET /v0/contractors contractors linked to your projects (core + tax; no bank data)

Full request/response schemas: /docs.


7a. Client libraries

There are no official client libraries yet. The OpenAPI document is complete and resolvable, so you can generate a typed client in a minute, e.g. for TypeScript:

npx openapi-typescript https://api.propel-industrial.com/v0/openapi.json -o propel.d.ts

(Python/others: openapi-generator-cli generate -i https://api.propel-industrial.com/v0/openapi.json -g python.) The reference at /docs shows copy-paste examples per endpoint (curl, JavaScript, Node, Python).

7b. Feedback

Something wrong, missing or unclear? Tell us directly — it reaches the Propel team immediately:

curl -s -X POST "https://api.propel-industrial.com/v0/feedback" \
  -H "Authorization: Bearer $PROPEL_KEY" -H "Content-Type: application/json" \
  -d '{ "category": "missing", "message": "I need to filter records by contractor name.",
        "endpoint": "GET /v0/records", "agent": { "name": "my-reporting-agent", "model": "…" } }'

category is one of bug, missing, confusing, idea, praise. Add the requestId of a call that went wrong. AI agents: please send feedback when a field or filter you need is missing, a response surprised you, or these docs were unclear. Never include keys or secrets. Up to 20 messages per day.

8. Stability and versioning

Privacy

The API returns your organisation's project data to callers you authorise. How Propel handles personal data: propel-industrial.com/privacy-policy.

Changelog

Date Version Change
2026-10-01 0.1.0 First release: projects, budgets (structure with rolled-up amounts), records, contractors.