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.
- Base URL:
https://sandbox.api.propel-industrial.com(the address you are reading this from) - Reference (every endpoint and field): /docs · machine-readable: /v0/openapi.json
- For scripts and AI agents: the machine-readable spec /v0/openapi.json (complete, no key needed) · this guide as plain text: /llms.txt, /docs/guide.md
- No key yet? Request access — every new organisation starts with a free 14-day trial.
- Something wrong, missing or confusing? Tell us with
POST /v0/feedback(how) — agents are welcome to send it on their own. Every API response carries the address in itsLinkheader (rel="feedback").
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:
- projects — optionally limited to a list of projects;
- field groups — by default
core, amounts, budget, documents(no bank, tax, payment or notes unless granted explicitly).
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:
- Each PDF becomes one record with
status: setupandintake.channel: email. - The document is read automatically:
intake.statusgoesqueued → processing → completed(usually within a minute) orfailed. Until it is matched,counterparty.namemay benull. - 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.
- Senders: emails are processed from the project's approved senders. Mail from any other sender is held for review by the project team and only becomes records once released, so a PDF may appear later or not at all.
- Duplicates: the same PDF sent to the same project again is processed once (unless the first was rejected).
- Wrong address: mail to an address that isn't a project's intake address is rejected.
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:
cashDateis the recorded payment date. It can be empty on apaidrecord (the payment was marked without a date), and it is not guaranteed to fall on or afterrecognitionDate.recognitionDatecan be empty on a few records (mostly entries still insetup).- For "paid in a given year", decide which date you mean and handle empty dates explicitly.
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:
committed= the contract amount + approved amendments (change orders). Pending increases are reported separately aspendingChangesand are not committed yet; rejected amendments count nowhere.openCommitment= committed − invoiced, never negative. If invoices exceed the contract, the excess isoverInvoiced— it is never netted against another contract.- Only
signedandcompletedcontracts count on the budget. A contract without budget lines yet returnscommitted: null. locked: truemeans the base amount and budget lines are final (after the first approved amendment or linked invoice); later changes appear as amendments.
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:
uncommitted— still to be contracted. By default the rest of the budget, max(0, budget − committed); a project team can enter a different value (always with a reason).uncommittedDefaultedtells you which.eac(estimate at completion) = committed + pendingChanges + uncommitted. A line nobody has touched equals its budget — there are no accidental savings.variance= budget − eac (negative = overrun);costToComplete= eac − invoiced.
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
- The version is in the path (
/v0). v0 is the first version of the API. - We avoid breaking changes: new endpoints, new optional fields and new enum values are added without notice — treat unknown fields and enum values gracefully. If a breaking change is unavoidable in v0, it is announced in advance in the changelog.
- From v1 on, changes within a version are additive only; anything breaking goes to the next version.
- Not available in v0: write endpoints (except feedback), webhooks, PDF downloads, bank data.
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. |