API
Propel API · v0

Your projects,
as an API.

Read projects, budgets and invoices from your own tools and AI agents — the same data the Propel app shows, with the same access rules.

Base URL https://sandbox.api.propel-industrial.com · read-only · JSON · llms.txt for agents

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://sandbox.api.propel-industrial.com/v0/projects \
  -H "Authorization: Bearer $PROPEL_KEY"

# a project's budget: structure + approved / invoiced / paid per node
curl -s https://sandbox.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://sandbox.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 ✔ ✔ –
accounting accounting codes (e.g. Yardi, DATEV) on budget lines and records ✔ ✔ –
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. A nodeId comes from the company's budget structure, so projects on the same structure share it (0 Site has the same nodeId on every development project): when you combine projects, key by projectId + nodeId.

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. invoiced is not the payment queue: invoices still in setup count too. invoicedByStatus splits it (setup, approvalPending, paymentPending, paid — together = invoiced); the payment queue is paymentPending. invoicedGross and paidGross give the same totals gross (what leaves the bank); budget, invoiced and paid stay net. 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://sandbox.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
cashDateFrom, cashDateTo payment date range, inclusive, YYYY-MM-DD — records without a payment date are left out. Needs the payment field group.
hasCashDate true = only records with a payment date, false = only without one (e.g. status=paid&hasCashDate=false). Needs payment.
source invoice = supplier invoices only, accrual = accrual entries only
accountingCode records whose code in an accounting system equals the value, written system:code — e.g. accountingCode=yardi:1100-0010. Needs the accounting field group.
fields return only these top-level fields, comma-separated; id is always included (e.g. fields=status,net,cashDate). Field groups still apply.
count true = also return page.total, the number of matching records across all pages

Example — what was paid in 2026, light rows and a total:

curl -s "https://sandbox.api.propel-industrial.com/v0/records?status=paid&cashDateFrom=2026-01-01&cashDateTo=2026-12-31&fields=projectId,net,gross,cashDate&count=true&limit=200" \
  -H "Authorization: Bearer $PROPEL_KEY"

Dates on records — what to expect:

Accounting codes. Every budget line carries its codes per accounting system (yardi, datev, sap, bmd, other) in accountingCodes; a line without its own code uses its parent's (inherited: true). A record carries the codes it was approved with; before approval, the codes of its budget line. Codes are strings — leading zeros stay.

Accruals are records too: source: "accrual" (and workflowStep: "Accrual"), usually without a document. source=invoice returns supplier invoices only.

Document names are labels, not workflow state. A file named "Approved Invoice" (kind: "approved") doesn't prove the record is approved; use status.

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://sandbox.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 (all filters in 5a; paginated)
GET /v0/records/{recordId} one record
GET /v0/contractors contractors linked to your projects (core + tax; no bank data)
GET /v0/contracts contracts with committed, invoiced and open amounts (?projectId=; paginated)
GET /v0/contracts/{contractId} one contract with its budget lines and amendments
GET /v0/projects/{projectId}/cashflow cash out per budget line and month (actual, forecast)
GET /v0/projects/{projectId}/forecast/snapshots approved month-end forecasts (…/{month} for one)
GET /v0/projects/{projectId}/units lettable units
GET /v0/projects/{projectId}/rent-roll leases in force, passing rent, vacancy, WAULT
GET /v0/projects/{projectId}/facilities loan facilities (/v0/facilities/{id}/schedule, …/drawdowns)

Full request/response schemas: /docs.

Contracts and commitments

A contract commits money to one or more budget lines. Its figures, all net and in integer minor units:

On the budget (/v0/projects/{projectId}/budget), every line carries the same figures: committed, openCommitment, overInvoiced, pendingChanges, rolled up like the other amounts. An amendment counts on the line it is booked on, so totals match the contracts endpoint. Guests see a contract's name, status and dates only.

Forecast and month-end snapshots

Every budget line carries a forecast, net and rolled up like the other amounts:

At month end the forecast is frozen into a snapshot and approved by the project's forecast approver. GET /v0/projects/{projectId}/budget?asOf=YYYY-MM returns the approved month exactly as approved (404 when that month has no approved snapshot); GET /v0/projects/{projectId}/forecast/snapshots lists the months, and …/snapshots/{month} returns the totals and the change per line against the previous approved month, with the reason given.

Cash flow

GET /v0/projects/{projectId}/cashflow?from=YYYY-MM&to=YYYY-MM returns, per budget line and month, actual (paid, by payment month, past months) and forecast (the cost still to come, spread from the current month: contract payment plans first, then the line's own phasing — linear, S-curve, lump sum or custom months). undated is money that can't be placed in time yet because the line has no dates.

Units and rent roll

GET /v0/projects/{projectId}/units lists the lettable units with type and size (area in m², parking in spaces). GET /v0/projects/{projectId}/rent-roll?asOf=YYYY-MM returns the signed and active leases in force that month with their passing rent (12 × the rent payable that month, after rent-free months, steps and indexation), plus vacancy and WAULT to expiry and to first break. Rents need the amounts field group; guests see units and sizes only.

Financing

GET /v0/projects/{projectId}/facilities lists the loan facilities with commitment, drawn, requested and available amounts, pricing and the next interest payment date. GET /v0/facilities/{facilityId}/schedule returns the interest periods (rate, interest, commitment fee, balance; a period whose rate isn't fixed yet is marked projected) and …/drawdowns the drawdowns and repayments. Financing needs the financial field group, which keys don't have unless it was granted, and access to every project the facility finances.


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://sandbox.api.propel-industrial.com/v0/openapi.json -o propel.d.ts

(Python/others: openapi-generator-cli generate -i https://sandbox.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://sandbox.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.
2026-10-02 0.1.0 Guide only: dates on records, accruals, document names, invoiced vs. payment queue, nodeId across projects, full filter list in section 7.
2026-10-05 0.1.4 Forecast on budget lines (uncommitted, eac, variance, costToComplete), ?asOf= approved months, forecast snapshots, cash flow, units, rent roll, loan facilities (schedule, drawdowns; financial field group). All additions.
2026-10-03 0.1.3 Contracts: /v0/contracts and /v0/contracts/{contractId} (committed, invoiced, paid, open, over-invoiced, pending changes, budget lines, amendments). Budget nodes: committed, openCommitment, overInvoiced, pendingChanges. All additions.
2026-10-03 0.1.2 Accounting codes: accountingCodes on budget lines and records (new accounting field group), filter accountingCode=system:code. All additions.
2026-10-02 0.1.1 Records: filters cashDateFrom, cashDateTo, hasCashDate, source; fields (projection) and count (page.total); source: "accrual"; documents[].kind. Budget nodes: invoicedByStatus, invoicedGross, paidGross. Link: rel="feedback" header on every response. Request access page. All additions — nothing existing changed.