# 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. - **Base URL:** `https://sandbox.api.propel-industrial.com` (the address you are reading this from) - **Reference (every endpoint and field):** [/docs](https://sandbox.api.propel-industrial.com/docs) · machine-readable: [/v0/openapi.json](https://sandbox.api.propel-industrial.com/v0/openapi.json) - **For scripts and AI agents:** the machine-readable spec [/v0/openapi.json](https://sandbox.api.propel-industrial.com/v0/openapi.json) (complete, no key needed) · this guide as plain text: [/llms.txt](https://sandbox.api.propel-industrial.com/llms.txt), [/docs/guide.md](https://sandbox.api.propel-industrial.com/docs/guide.md) - **No key yet?** [Request access](https://sandbox.api.propel-industrial.com/access) — every new organisation starts with a free 14-day trial. - **Something wrong, missing or confusing?** Tell us with `POST /v0/feedback` ([how](#7b-feedback)) — agents are welcome to send it on their own. Every API response carries the address in its `Link` header (`rel="feedback"`). --- ## 1. Quickstart ```bash 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 `. 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___` | 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: ```json { "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: ```bash 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: ```json { "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: ```bash 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:** - `cashDate` is the recorded payment date. It can be empty on a `paid` record (the payment was marked without a date), and it is not guaranteed to fall on or after `recognitionDate`. - `recognitionDate` can be empty on a few records (mostly entries still in `setup`). - 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](https://www.rfc-editor.org/rfc/rfc9457)): ```json { "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/` 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](#5a-filtering-records); 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](https://sandbox.api.propel-industrial.com/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 as `pendingChanges` and are not committed yet; rejected amendments count nowhere. - `openCommitment` = committed − invoiced, never negative. If invoices exceed the contract, the excess is `overInvoiced` — it is never netted against another contract. - Only `signed` and `completed` contracts count on the budget. A contract without budget lines yet returns `committed: null`. - `locked: true` means 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). `uncommittedDefaulted` tells 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: ```bash 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](https://sandbox.api.propel-industrial.com/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: ```bash 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](https://www.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](/access) page. All additions — nothing existing changed. |