> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sintropix.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sintropix API: Programmatic Access to the ERP Backend

> Reference for the Sintropix ERP HTTP API: base path, request and response format, authentication headers, error envelope, and per-Entity scoping.

The Sintropix API is a JSON over HTTPS interface to the ERP backend that owns your ledger, your invoices, your bank movements, and your reports. Every business route is scoped to a single Entity (a legal company with its own books), authenticated with a Better Auth credential (a session cookie in the app, an API key from an integration), and validated end-to-end by a shared Zod contract in `@sintropix/api-contract`.

<Warning>
  This reference documents the ERP backend as implemented in `apps/erp-backend`. The API is currently used by Sintropix's own web client and by Erwin (the AI assistant); a public, versioned, externally supported surface has not been announced. Endpoint paths, request shapes, and response shapes may change without notice.
</Warning>

## Base URL

All routes are mounted under a global `/api` prefix (see `app.setGlobalPrefix('api')` in `apps/erp-backend/src/main.ts`). Sintropix has not published a stable customer-facing base URL; substitute your deployment's origin.

```text theme={null}
https://<your-erp-backend-host>/api
```

The API is not versioned in the URL path. Contract-breaking changes are coordinated through `@sintropix/api-contract` version bumps and the frontend that consumes it.

## Route Shape

Most business resources live under a single Entity:

```text theme={null}
/api/entities/{entityId}/<resource>
```

For example:

```text theme={null}
GET  /api/entities/{entityId}/ledger/accounts
POST /api/entities/{entityId}/invoices
GET  /api/entities/{entityId}/reports/balance-sheet
```

A handful of routes are Entity-independent: [`/api/entities`](/api-reference/entities) (list and create), [`/api/fx-rates`](/api-reference/fx-rates), [`/api/countries` and `/api/currencies`](/api-reference/reference-data), [`/api/onboarding/*`](/api-reference/onboarding) (staff only), [`/api/agents/models`](/api-reference/authentication), and `/api/health`.

`{entityId}` is always a UUID. The tenant guard rejects a request where the caller has no Entity Membership to that Entity with a `404`; it never returns `403`, so probing does not reveal whether an unknown Entity exists.

## Authentication

Every non-anonymous request carries one of two credentials:

* **Session cookie** issued by Better Auth after a browser sign-in. Same-site only (the SPA and API share a registrable domain).
* **API key** in the `x-api-key` header. Keys are minted by the owner from the settings UI; the API management surface is barred to key holders (a request to `/api/auth/*` with `x-api-key` returns `403`).

Every mutating request (`POST`, `PUT`, `PATCH`, `DELETE`) authenticated with an API key must also send a Declared Actor:

```http theme={null}
x-api-key: <secret>
x-audit-actor: <your-agent-label>
```

`x-audit-actor` is a self-reported software label (for example `claude-opus-4.8`) that lands verbatim in the [Audit Trail](/api-reference/audit-trail). Omitting it on a key-authenticated mutation returns `400`.

See [Authentication](/api-reference/authentication) for the full model, including read-only keys, the Erwin Key Lease, and staff versus client roles.

## Request Format

Requests are plain JSON. Include `Content-Type: application/json` on any request with a body.

```bash theme={null}
curl "https://<your-erp-backend-host>/api/entities/$ENTITY_ID/ledger/accounts" \
  -H "x-api-key: $SINTROPIX_API_KEY"
```

Path parameters that look like UUIDs are validated by `ParseUUIDPipe`; a malformed id returns `400`.

## Response Format

Responses are the resource itself as JSON. There is no top-level `data` wrapper, no `meta` envelope, and no `pagination` block on non-paginated endpoints. The exact shape of every response is the Zod schema exported from `@sintropix/api-contract` and enforced by `nestjs-zod`'s `@ZodSerializerDto` at serialization time, so the compiled schema is the source of truth for field names and types.

A single resource comes back as an object:

```json theme={null}
{
  "id": "…",
  "name": "…"
}
```

A list endpoint returns a bare JSON array:

```json theme={null}
[
  { "id": "…" },
  { "id": "…" }
]
```

A page endpoint (endpoints whose path ends in `/pages`, plus a handful of reports) returns a `{ rows, nextCursor }` object. `nextCursor` is `null` on the last page.

```json theme={null}
{
  "rows": [ /* … */ ],
  "nextCursor": "…"
}
```

<Note>
  Pagination is opt-in per endpoint, not sitewide. Endpoints without `/pages` in their path return every row and take no `cursor` argument. Check the individual reference page for the endpoint you are calling.
</Note>

## Errors

Non-2xx responses are JSON. Application errors thrown as `HttpException` return the exception body; unhandled errors return `"Internal server error"`. Every response, success or failure, carries a `x-request-id` header for support (`REQUEST_ID_HEADER` in `@sintropix/observability`).

| Status | Meaning                                                                        |
| ------ | ------------------------------------------------------------------------------ |
| `200`  | Request succeeded.                                                             |
| `201`  | Resource created.                                                              |
| `204`  | Request succeeded with no response body.                                       |
| `400`  | Body, query, or path failed validation; a required header is missing.          |
| `401`  | No valid session or API key.                                                   |
| `403`  | Read-only key attempted a mutation; API key used against the auth surface.     |
| `404`  | Resource does not exist, or caller has no membership to the requested Entity.  |
| `409`  | Domain conflict (for example an already-booked Ledger Entry, a duplicate row). |
| `429`  | Rate limit exceeded on an API key.                                             |
| `500`  | Unhandled server error; Sentry event captured.                                 |

Domain-level errors carry a stable string `code` in the body — for example `API_KEY_READ_ONLY_ERROR` from `apps/erp-backend/src/auth/error-codes.constants.ts`. Match on `code`, not on the human-readable message.

## Rate Limits

Better Auth's `apiKey` plugin rate-limits each key at **5,000 requests per hour**, sliding window (`rateLimit` in `apps/erp-backend/src/auth/auth.ts`). Session-cookie traffic is not rate-limited at the application layer. Exceeding the limit returns `429`; there is no `X-RateLimit-*` header response contract.

## CORS and Same-Site

The API enables CORS only for origins listed in the `CORS_ORIGINS` deployment env var, with credentials enabled. Browser clients must share a registrable domain with the API (for example `app.sintropix.com` and `api.sintropix.com`); the session cookie is same-site and Safari drops it across sites. Server-to-server API-key traffic is unaffected.

## What Is Not Documented Here

The following are not part of the current API surface, even though the previous version of this documentation referenced them:

<Warning>
  There is **no webhook system**. Sintropix does not push event notifications to external URLs. The only outbound HTTP is a Slack incoming webhook used for internal operational reporting.
</Warning>

<Warning>
  There is **no cursor pagination on most endpoints**. Only endpoints whose path ends in `/pages` (plus the general-ledger, open-item-lines, journal, and ledger-entries pagers) return `{ rows, nextCursor }`. Others return the full list.
</Warning>

<Warning>
  There is **no `Authorization: Bearer` scheme**. API keys go in `x-api-key`, sessions in a same-site cookie.
</Warning>

<Warning>
  There is **no `/v1` prefix**. The base path is `/api`; contract versioning is handled through `@sintropix/api-contract`.
</Warning>
