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

# REST API (v1)

> Read-only access to your Ledgitify projects, reports, allocations, and transactions.

The Ledgitify REST API gives programmatic, read-only access to a single organisation's
data. It is versioned under `/api/v1` and returns JSON.

**Base URL**

```
https://ledgitify.com/api/v1
```

## Authentication

Every request must carry an API key as a Bearer token:

```
Authorization: Bearer lk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

* Create keys in **Settings → API keys** (Owner or Admin). The full key is shown **once**
  at creation — only a hash is stored, so copy it then.
* API access is a **Professional+** feature (`api_access`). Keys on a plan without it return `403`.
* The key alone determines which organisation you're reading. There is **no `tenantId`
  parameter** on any endpoint — you cannot read another organisation's data with your key.
* Revoke a key at any time from the same screen; a revoked key immediately returns `401`.

## Scopes

Each key is granted one or more scopes at creation. An endpoint requires the scope shown
against it; a key lacking that scope returns `403 insufficient_scope`.

| Scope                | Grants                                                         |
| -------------------- | -------------------------------------------------------------- |
| `projects:read`      | List and read projects                                         |
| `reports:read`       | Budget-vs-actual report for a project                          |
| `allocations:read`   | Allocations for a project                                      |
| `transactions:read`  | The organisation's transactions                                |
| `transactions:write` | Push transactions into the organisation (the only write scope) |

## Rate limiting

Each key is limited to **120 requests per minute**. Exceeding it returns `429` with a
`Retry-After` header (seconds) and `X-RateLimit-Limit` / `X-RateLimit-Remaining`.

## Errors

Errors are JSON with a stable `code`:

```json theme={null}
{ "error": { "code": "insufficient_scope", "message": "This key lacks the transactions:read scope." } }
```

| Status | `code`               | Meaning                                           |
| ------ | -------------------- | ------------------------------------------------- |
| 401    | `unauthorized`       | Missing, malformed, unknown, or revoked key       |
| 403    | `forbidden`          | Plan does not include API access                  |
| 403    | `insufficient_scope` | Key lacks the required scope                      |
| 404    | `not_found`          | Project not found in your organisation            |
| 405    | —                    | HTTP method not allowed on this route (see below) |
| 429    | `rate_limited`       | Per-key rate limit exceeded                       |

### HTTP methods

All v1 endpoints are **read-only (`GET`)** except **`POST /api/v1/transactions`** (the
transaction push). Any unsupported method on a route returns `405 Method Not Allowed` with an
`Allow` header listing the permitted methods, before any authentication or data access occurs.

## Endpoints

### List projects

```
GET /api/v1/projects
```

Scope: `projects:read`. Returns the organisation's projects.

```bash theme={null}
curl https://ledgitify.com/api/v1/projects \
  -H "Authorization: Bearer $LEDGITIFY_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "665f1a...",
      "name": "Community Garden Grant",
      "type": "grant",
      "status": "active",
      "startDate": "2025-01-01T00:00:00.000Z",
      "endDate": "2025-12-31T00:00:00.000Z",
      "currency": "AUD",
      "budgetMode": "expense_only",
      "parentProjectId": null,
      "funder": { "name": "ARENA", "reference": "G-123" },
      "createdAt": "2024-12-01T00:00:00.000Z",
      "updatedAt": "2025-01-15T00:00:00.000Z"
    }
  ]
}
```

### Get a project

```
GET /api/v1/projects/{id}
```

Scope: `projects:read`. Returns a single project, or `404` if it isn't in your organisation.

### Project report

```
GET /api/v1/projects/{id}/report?granularity=monthly
```

Scope: `reports:read`. Budget-vs-actual for the project. `granularity` is `monthly`
(default) or `quarterly`. Each cell carries `budgeted`, `actual`, and `variance`.

```bash theme={null}
curl "https://ledgitify.com/api/v1/projects/$ID/report?granularity=quarterly" \
  -H "Authorization: Bearer $LEDGITIFY_API_KEY"
```

```json theme={null}
{
  "granularity": "quarterly",
  "data": {
    "totalLabel": "Total",
    "columns": [{ "label": "Q1 2025", "periodStart": "2025-01-01T00:00:00.000Z", "periodEnd": "2025-03-31T00:00:00.000Z" }],
    "rows": [
      { "budgetLineId": "b1", "budgetLineName": "Salaries", "depth": 0, "isLeaf": true,
        "cells": [{ "budgeted": 30000, "actual": 28500, "variance": -1500 }],
        "total": { "budgeted": 30000, "actual": 28500, "variance": -1500 } }
    ],
    "columnTotals": [{ "budgeted": 30000, "actual": 28500, "variance": -1500 }],
    "grandTotal": { "budgeted": 30000, "actual": 28500, "variance": -1500 }
  }
}
```

### Project allocations

```
GET /api/v1/projects/{id}/allocations
```

Scope: `allocations:read`. The allocations recorded against the project's budget lines.

```json theme={null}
{
  "data": [
    { "budgetLineId": "b1", "amount": 1200, "date": "2025-02-14T00:00:00.000Z",
      "description": "February payroll", "contact": "Jane Smith" }
  ]
}
```

### Transactions

```
GET /api/v1/transactions?from=2025-01-01&to=2025-06-30&status=unreviewed&type=bank_transaction&page=1&pageSize=50
```

Scope: `transactions:read`. Transactions are **organisation-level** (an allocation links a
transaction to a project), so this endpoint is not nested under a project.

| Query param   | Description                                                                            |
| ------------- | -------------------------------------------------------------------------------------- |
| `from` / `to` | ISO date bounds on the transaction date (inclusive)                                    |
| `status`      | `unreviewed` · `excluded` · `partially_allocated` · `fully_allocated` · `needs_review` |
| `type`        | `bank_transaction` · `bill` · `invoice` · `journal_line`                               |
| `page`        | 1-based page number (default `1`)                                                      |
| `pageSize`    | Rows per page, 1–200 (default `50`)                                                    |

```bash theme={null}
curl "https://ledgitify.com/api/v1/transactions?pageSize=100" \
  -H "Authorization: Bearer $LEDGITIFY_API_KEY"
```

```json theme={null}
{
  "data": [
    { "id": "77aa...", "provider": "xero", "providerTransactionId": "BT-9",
      "type": "bank_transaction", "status": "unreviewed", "date": "2025-08-15T00:00:00.000Z",
      "description": "Office supplies", "contact": "Officeworks", "totalAmount": 249.5,
      "currency": "AUD", "accountCode": "400", "trackingCategories": ["Marketing"] }
  ],
  "page": 1,
  "pageSize": 100,
  "total": 1,
  "hasMore": false
}
```

The full provider payload (`rawData`) and internal exclusion audit fields are never exposed.

### Push transactions (batch)

```
POST /api/v1/transactions
```

Scope: `transactions:write`. Push cash transactions from an external finance system (SAP,
Oracle, PeopleSoft, or any ETL). This is the inbound counterpart to the Xero sync — for
organisations that can't use a native pull integration. Pushed transactions participate in
the full allocation workflow (workspace, exclude, allocate, reports) exactly like synced or
CSV-imported ones, and are tagged `provider: "api"`.

The organisation is determined **solely by the API key** — there is no tenant parameter.

**Request body** — a JSON object with a `rows` array (1–500 rows):

| Field                | Required | Description                                                                                                                                |
| -------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `externalId`         | yes      | Your stable id for the transaction. Idempotency key — re-pushing the same `externalId` updates the existing row instead of duplicating it. |
| `date`               | yes      | ISO date (e.g. `2026-08-01`).                                                                                                              |
| `amount`             | yes      | Signed, in your base currency. **Money-in is negative**, money-out positive. Must be non-zero.                                             |
| `description`        | no       | Free text (≤1000 chars).                                                                                                                   |
| `contact`            | no       | Payee / counterparty name.                                                                                                                 |
| `currency`           | no       | 3-letter ISO code; defaults to `AUD`.                                                                                                      |
| `type`               | no       | Only `bank_transaction` (the default). Cash movements only — journal lines are not accepted.                                               |
| `accountCode`        | no       | Your chart-of-accounts code.                                                                                                               |
| `trackingCategories` | no       | Array of tracking category names.                                                                                                          |

Each row is validated **independently**: valid rows are written even if others in the batch
are rejected. A row whose `amount` changes on re-push and that already has allocations is
flagged `needs_review` (same as an edited Xero transaction).

```bash theme={null}
curl -X POST "https://ledgitify.com/api/v1/transactions" \
  -H "Authorization: Bearer $LEDGITIFY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rows": [
      { "externalId": "GL-2026-0001", "date": "2026-08-01", "description": "Office supplies",
        "contact": "Officeworks", "amount": 249.50, "accountCode": "400" },
      { "externalId": "GL-2026-0002", "date": "2026-08-02", "description": "Grant receipt",
        "amount": -50000 }
    ]
  }'
```

```json theme={null}
{
  "results": [
    { "externalId": "GL-2026-0001", "status": "created", "id": "77aa..." },
    { "externalId": "GL-2026-0002", "status": "created", "id": "77ab..." }
  ],
  "summary": { "total": 2, "created": 2, "updated": 0, "rejected": 0 }
}
```

Each result carries `status` of `created`, `updated`, or `rejected` (with an `errors` array).
The response is `200` when at least one row succeeded, `422` when every row was rejected, and
`400` when the envelope itself is malformed (missing `rows`, empty, or over 500 rows).
