---
title: "Agent quickstart"
description: "Start here: authenticate with an API key, find the Organisation you act on, and run the Lilith loop end to end"
section: start
order: 0
---

# Agent quickstart

An Agent is a program, such as a script or an AI assistant, that calls the Sustifi API with a User's API key. It has no identity of its own: it acts as the Member that User is in their active Organisation, and everything it does is recorded as that Member's act. An Agent does the Member's carbon accounting and reporting. It never administers the User's account or the platform, and never handles the Member's credentials for another system.

The API base URL is `https://app.susti.fi/api`. Every response is JSON unless a section says otherwise.

The docs live at `https://docs.susti.fi`. Any page is also plain Markdown at `https://docs.susti.fi/api/docs/<slug>`, for example `https://docs.susti.fi/api/docs/agents/lilith`, and an index of every page is at `https://docs.susti.fi/llms.txt`. Links in these pages such as `/agents/lilith` are relative to `docs.susti.fi`, not to the app host.

## What you can do today

| Task | Guide | Plan needed |
|------|-------|-------------|
| Bring in the Member's Lilith bookkeeping, categorise what needs it, and read back the Footprint | [Lilith](/agents/lilith), [Categorising](/agents/categorising), [Reading results](/agents/reading-results) | Professional for the Lilith calls; categorising and reading results work on any plan |
| Prepare a VS Report: create a draft, fill it, finalise it, export the EFRAG Digital Template, archive it | [VS Reports](/agents/vs-reports) | Professional |
| Upload bank statements, shopping reports, receipts and Helen bills; attach a receipt to an Accounting Transaction; list and create projects and clients | [Agent API reference](/features/agent-api) | Any plan |
| Sync connected AI providers, import a usage CSV, list AI usage records. Connecting a provider needs the User's own session | [AI Integrations API](/features/ai-integrations-api) | Professional; listing usage records works on any plan |
| Record an Information Request from a customer and draft its answers | [Information Requests](/agents/information-requests) | Professional |
| Publish, unpublish and check the Organisation's carbon.txt file | [Agent API reference](/features/agent-api#organisation-details-and-carbontxt) | Professional to publish and check; unpublishing works on any plan. Publishing needs an issued certificate |

[The Lilith loop, end to end](#the-lilith-loop-end-to-end) below walks through it step by step.

## Authenticate

The Member creates the key in the browser. You cannot create one with a key.

1. The Member signs in and goes to **Profile → API Keys**.
2. They click **Create API Key**, name it (for example "Claude agent") and copy it. It is shown only once.
3. A User can have at most 3 active keys. They revoke one before creating a fourth.

Keys look like `sk_live_a1b2c3d4e5f6...`. Send the key on every request:

```
Authorization: Bearer sk_live_your_key_here
```

A missing, unknown or revoked key answers `401 {"error": "Unauthorized"}`. So does a key whose User has deleted their account. Never paste the key into a third-party service, a shared document or a prompt you do not control.

## What a key cannot do

The rule: an Agent does the Member's carbon accounting and reporting. It never administers the User's account or the platform, and never handles the Member's credentials for another system. The calls that break that rule answer an API key with:

```
HTTP/1.1 403 Forbidden

{"error": "This action requires the User's own session; an API key cannot do it"}
```

Those calls are:

| Call | What it does |
|------|--------------|
| `GET /api/user/api-keys`, `POST /api/user/api-keys`, `DELETE /api/user/api-keys/{id}` | List, create and revoke API keys |
| `POST /api/user/delete`, `POST /api/user/restore`, `GET /api/user/export-data` | Delete, restore or export the account |
| `GET /api/auth/session` | Read who is signed in to the browser session |
| `POST /api/auth/sign-out` | Sign the browser session out |
| `GET /api/auth/mfa`, `POST /api/auth/mfa/enrol`, `POST /api/auth/mfa/verify`, `DELETE /api/auth/mfa` | Read, set up, confirm or turn off the User's two-factor authentication |
| `POST /api/organizations/switch` | Change the active Organisation |
| `PATCH /api/csrd/profile` | Save the CSRD reporting setup (company size, sector code, reporting wave and year) |
| `POST /api/organizations/members`, `PATCH /api/organizations/members/{memberId}`, `DELETE /api/organizations/members/{memberId}`, `POST /api/organizations/members/accept` | Invite, change or remove a member, accept an invitation |
| `POST /api/lilith/connect`, `POST /api/accounting/connect`, `POST /api/ai-integrations` | Connect Lilith, an accounting system or an AI provider (they take the Member's credentials) |
| `POST /api/lilith/import-receipts`, `GET /api/lilith/import-receipts/jobs/active`, `POST /api/lilith/import-receipts/jobs/{id}/cancel` | Start, follow or cancel the import of the Lilith expense claims' receipt files (it downloads them with the Member's Lilith session) |
| `GET /api/vsme/reports/{reportId}/disclosures/{disclosureId}/field-notes`, `POST /api/vsme/reports/{reportId}/disclosures/{disclosureId}/field-notes`, `DELETE /api/vsme/reports/{reportId}/disclosures/{disclosureId}/field-notes` | List, attach or detach the Field Notes cited as a Disclosure's evidence (Field Notes are the Member's own free text) |
| `GET /api/emissions/all` | The browser's emissions charts. Read the Footprint with `GET /api/emissions/summary` instead (see [Reading results](/agents/reading-results)) |

When you need one of these, ask the Member to do it in the browser. Reading whether a connector is connected, syncing it and disconnecting it are allowed.

Some inputs have no API call at all, so a key cannot set them either: the lease facts of the Organisation's premises, and a row's activity (a freight weight and distance, or a product footprint). Ask the Member to enter them in the browser. See [Categorising](/agents/categorising#what-only-the-browser-can-set).

## Which Organisation you act on

Every call acts on the User's **active Organisation**: the one the browser has selected. A few calls also accept `?organization_id=` naming another Organisation the User belongs to: the VS Report list and create calls (see the [VS Reports](/agents/vs-reports) guide), the Information Request list and create calls (`GET` and `POST /api/information-requests`), and the project and client lists (`GET /api/projects`, `GET /api/clients`). An Organisation the User does not belong to answers `403 {"error": "You are not a member of this organization"}`. No other call takes an Organisation from you.

Read it with `GET /api/organizations/switchable`:

```bash
curl https://app.susti.fi/api/organizations/switchable \
  -H "Authorization: Bearer sk_live_your_key_here"
```

```json
{
  "organizations": [
    { "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "name": "Aino Design", "role": "owner", "isActive": true }
  ]
}
```

- The one with `isActive: true` is where your writes land. `role` is `owner`, `admin`, `editor` or `viewer`.
- If the User belongs to several Organisations, read this again before each sequence of writes. When the User switches Organisation in the browser, your next call acts on the new one.
- A User with no Organisation yet gets an empty list here. Their personal Organisation is created by the first call that works on Organisation data, such as `GET /api/accounting-transactions` or `POST /api/calculations/calculate`; this call does not create it. A call answers `404 {"error": "No organisation found for this user"}` when the User's active Organisation is one they no longer belong to. This list then has no entry with `isActive: true`: ask the Member to choose an Organisation in the browser.

## Plans

Each call is open to every plan unless its guide says otherwise. Lilith, VS Report and Information Request calls, certificate generation, AI usage syncs and imports, and publishing or checking carbon.txt need **Professional** (or Teams, or Enterprise). On a Free plan they answer:

```
HTTP/1.1 403 Forbidden

{"error": "Professional subscription required"}
```

A plan change can take up to a minute to reach the API.

## Patterns

**Streamed uploads.** The bank statement, shopping report and Helen uploads answer with a Server-Sent Events stream, not JSON. Read it to the last event, whose `status` is `completed` or `error`. A failure such as a duplicate file arrives as an `error` event inside a `200` stream, so the status code alone does not tell you it worked. The [Agent API reference](/features/agent-api) has the details.

**Recalculate after some writes.** CO₂e figures (Actuals) are written by the Calculation Job, not by every call. Run `POST /api/calculations/calculate` (no body) after:

- a shopping report or Helen upload,
- a bank statement upload whose `completed` event has `data.calculation: null`. The bank statement upload runs the Calculation Job itself: its stream sends a `calculating` event, and `data.calculation` in the `completed` event is the job's summary `{created, updated, skipped, resolved, excluded, removed}`, or `null` when the job failed,
- `PATCH /api/transactions/{transactionId}/categorize` and `POST /api/transactions/bulk-categorize`,
- `POST /api/lilith/push-to-transactions` when its `calculation` is `null`, or when it answered only `{"imported": 0, "skipped": 0}`.

Categorising an Accounting Transaction, pushing from Lilith and uploading a bank statement recalculate on their own. Before reporting figures, check that the rows you included have an `actual` (see the [Categorising](/agents/categorising) guide).

**Paging.** List calls page with `offset` and `limit` (Accounting Transactions, bank transactions) or `page` and `pageSize` (Lilith transactions). A `limit` above the maximum is clamped, not refused. Each guide gives the limits and whether the response tells you there is more.

**Dates.** Query dates are `YYYY-MM-DD`. VS Report responses give dates as full ISO timestamps.

## Errors and limits

| Status | Meaning | What to do |
|--------|---------|------------|
| 400 | Invalid query or body. The body names the problem, usually as `{"error": "...", "details": ...}`. The streaming uploads answer this as plain text | Fix the request; do not retry it unchanged |
| 401 | Missing, unknown or revoked key. From the Lilith calls, `{"error": "session_expired"}` means the Member's Lilith login expired, not your key | Ask the Member for a new key, or to reconnect Lilith in the browser |
| 403 | The plan does not include the call, or the call needs the User's own session | See [Plans](#plans) and [What a key cannot do](#what-a-key-cannot-do) |
| 404 | Not found, or not in the active Organisation | Check the id and the active Organisation |
| 409 | The request conflicts with the data's state. See below | Read `reason` when the body has one |
| 422 | Valid request the data cannot satisfy, for example Micro Relief on a Disclosure it does not cover | Read `error` |
| 429 | A rate limit or the daily AI quota | Wait `Retry-After` seconds |
| 500 | Server error | Retry after a short delay |
| 502 | Lilith did not answer | Retry later |
| 503 | A VS Report's Final Calculation could not read its data, or the service is briefly unavailable | Retry after `Retry-After` seconds, or a short delay |

**The two 409s you will meet most:**

- `{"error": "...", "reason": "not_draft"}`: you tried to change a VS Report, or one of its Disclosures, that is complete or archived. Only a draft can change. If the Member wants to amend a complete report, ask them; archiving it is their decision.
- `{"error": "These records belong to a finalised VS Report. Archive the report before changing them."}`: a cancel or delete would remove records whose Actuals a complete VS Report has locked. Nothing was deleted. Ask the Member whether to archive the report first.

**Rate limits.** Each endpoint limits how many requests it accepts; most allow 100 requests per 15 minutes. The [Agent API reference](/features/agent-api#rate-limits) lists the typical limits. Read the `X-RateLimit-*` headers, and after a `429` wait `Retry-After` seconds.

**Daily AI quota.** Calls that run an AI model also spend the User's daily AI quota, shared with the browser: 10 calls a day by default. When it is spent they answer `429 {"error": "Daily AI call limit reached", ...}`. Reading a receipt spends a quota of its own (100 a day, 500 a month) and answers `"Monthly AI call limit reached"` when the month is used up. The [Agent API reference](/features/agent-api#daily-ai-quota) lists those calls. The Lilith loop and the VS Report loop spend none of it.

## Act for the Member

Your acts are the Member's acts, and the product cannot tell them apart. Some calls cannot be undone, or remove the Member's data. Make them **only when the Member has told you to**, in so many words, for that report, job or record. Never infer it from a goal such as "get the report done".

- Finalising a VS Report: `POST /api/vsme/reports/{reportId}/finalise`. It runs the Final Calculation and locks the period's figures.
- Archiving a VS Report: `POST /api/vsme/reports/{reportId}/archive`.
- Deleting a VS Report or a Disclosure: `DELETE /api/vsme/reports/{reportId}`, `DELETE /api/vsme/reports/{reportId}/disclosures/{disclosureId}`.
- Cancelling an upload, which deletes everything it imported: `DELETE /api/financial-documents/cancel`, `DELETE /api/shopping-reports/cancel`, `DELETE /api/helen/cancel`, `DELETE /api/shopping-reports/sessions`.
- Deleting records: `DELETE /api/transactions/delete`, `DELETE /api/receipts/{receiptId}`, `DELETE /api/predictions/{predictionId}`, `DELETE /api/projects/{projectId}`, `DELETE /api/clients/{clientId}`, `DELETE /api/categorization-rules/{ruleId}`, `DELETE /api/information-requests/{requestId}`.
- Disconnecting a connector, which only the Member can reconnect: `DELETE /api/lilith/connect`, `DELETE /api/accounting/{integrationId}`, `DELETE /api/ai-integrations/{provider}`.

When a call refuses, report the refusal and its `reason` to the Member. Do not work around it, for example by archiving a report so that a delete goes through.

## The Lilith loop, end to end

The Member has connected Lilith in the browser, and their plan is Professional. Each step below is described in full in its guide.

**1. Check Lilith is connected.**

```bash
curl https://app.susti.fi/api/lilith/connect \
  -H "Authorization: Bearer sk_live_your_key_here"
```

```json
{ "connected": true, "username": "aino", "sessionExpired": false, "lastSyncedAt": "2026-09-30T08:12:00Z" }
```

If `connected` is `false` or `sessionExpired` is `true`, stop and ask the Member to connect Lilith in the browser.

**2. Pick the cost pool and fiscal year.**

```bash
curl https://app.susti.fi/api/lilith/cost-pools \
  -H "Authorization: Bearer sk_live_your_key_here"
```

```json
{ "costPools": [ { "uuid": "5f0c2a9e-7d41-4c3b-8f2e-1a9b6c4d3e20", "nid": 1234, "name": "Aino Design", "fiscalYears": ["2025-2026"] } ] }
```

**3. Sync it.** Pass the `uuid` and `name` exactly as returned.

```bash
curl -X POST https://app.susti.fi/api/lilith/sync \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"costPoolUuid": "5f0c2a9e-7d41-4c3b-8f2e-1a9b6c4d3e20", "costPoolName": "Aino Design", "fiscalYear": "2025-2026"}'
```

```json
{ "synced": 143, "errors": [], "timings": { "totalMs": 8120, "steps": {} } }
```

**4. Push it to Accounting Transactions.** Use the same `costPoolName` and `fiscalYear`.

```bash
curl -X POST https://app.susti.fi/api/lilith/push-to-transactions \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"costPoolName": "Aino Design", "fiscalYear": "2025-2026"}'
```

```json
{ "imported": 143, "skipped": 0, "errors": [], "calculation": { "created": 120, "updated": 0, "skipped": 0, "resolved": 0, "excluded": 0, "removed": 0 }, "timings": { "totalMs": 2400, "steps": {} } }
```

Pushing again is safe: rows already imported count as `skipped`. If `calculation` is `null`, run `POST /api/calculations/calculate`.

**5. Find what needs a category.**

```bash
curl "https://app.susti.fi/api/accounting-transactions?uncategorized=true&limit=50" \
  -H "Authorization: Bearer sk_live_your_key_here"
```

```json
{
  "transactions": [
    { "id": "8d1e0c52-4b7a-4f0e-9a51-2f6d3c8b7a10", "date": "2025-11-04", "amount": -84.5, "currency": "EUR", "description": "Train Helsinki–Tampere", "counterparty": "VR", "source": "lilith_import", "emission_category": null, "emission_subcategory": null, "factor_identifier": null, "actual": null }
  ],
  "total": 23,
  "hasMore": false
}
```

**6. Read the categories and choose a factor.**

```bash
curl https://app.susti.fi/api/emission-categories \
  -H "Authorization: Bearer sk_live_your_key_here"
```

The answer is a tree of categories, each with the spend factors you may pick. Choose with your own judgement, from the description and counterparty.

**7. Categorise.** `emission_category` is a top-level category's `slug` and `emission_factor_identifier` a factor's `identifier`, both from step 6; never make one up. This recalculates the transaction's Actual.

```bash
curl -X PATCH https://app.susti.fi/api/accounting-transactions/8d1e0c52-4b7a-4f0e-9a51-2f6d3c8b7a10/categorize \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"include_in_emissions": true, "emission_category": "transport", "emission_factor_identifier": "exiobase_382_other_land_transport_fi_eur"}'
```

```json
{ "success": true, "actualRecomputed": true }
```

Repeat steps 5 to 7 until the Member is satisfied. See the [Categorising](/agents/categorising) guide for what `uncategorized=true` keeps listing.

**8. Read the Footprint** by month and GHG scope.

```bash
curl "https://app.susti.fi/api/emissions/summary?from=2025-05-01&to=2026-04-30" \
  -H "Authorization: Bearer sk_live_your_key_here"
```

```json
{
  "from": "2025-05-01",
  "to": "2026-04-30",
  "totals": [
    { "month": "2025-11", "ghg_scope": 3, "scope3_category": 6, "co2_kg": 4.31, "event_count": 1 }
  ]
}
```

`from` and `to` are both required and inclusive, and `to` must fall in a calendar month fewer than 36 months after the month of `from`: `2024-01-01` to `2026-12-31` works, `2024-01-01` to `2027-01-01` does not. `ghg_scope` and `scope3_category` can be `null`. The [Reading results](/agents/reading-results) guide explains each field.
