# 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://api.propel-industrial.com` (the address you are reading this from)
- **Reference (every endpoint and field):** [/docs](https://api.propel-industrial.com/docs) · machine-readable: [/v0/openapi.json](https://api.propel-industrial.com/v0/openapi.json)
- **For scripts and AI agents:** the machine-readable spec [/v0/openapi.json](https://api.propel-industrial.com/v0/openapi.json) (complete, no key needed) · this guide as plain text: [/llms.txt](https://api.propel-industrial.com/llms.txt), [/docs/guide.md](https://api.propel-industrial.com/docs/guide.md)

---

## 1. Quickstart

```bash
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**:

- **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 | ✔ | ✔ | – |
| `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`**.

**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:

```bash
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:

```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` |

**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://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](https://api.propel-industrial.com/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:

```bash
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](https://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://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. |
